Java算法常用基础工具和技巧总结

前言

在算法题中会用到一些很基础,但业务开发中不常用的工具(类/方法)和技巧。时间长不接触可能就会忘记,这里简单总结一下。

字符、字符串

获取字符串第i个字符

s.charAt(i)//大量字符串相关的算法都会用到*
也可以先转成字符数组,再遍历
char[] arr = s.toCharArray();//这个并不常用,因为多了一道工序,还占用了一个数组的空间。
String[] arr = s.split(“,”);//业务中更常用的是这个,但算法中不常用

截取字符串

//注意substring里没有大写字母
s.substring(start, end+1)

转换

//数字字符直接转成int【常用的技巧*】
int a = ‘8’ - ‘0’

//字符转字符串
‘c’ + “”

判断字符是否是数字

char c = ‘3’;
if(Character.isDigit©) {
}
当然,如果忘记了。用(c>=‘0’ && c<=‘9’) 也是可以的

拼接字符串

尽量用StringBuilder, 而不要用StringBuffer。因为算法题基本都是单线程的,即便看起来很复杂的深度、广度优先遍历。所以尽量用“线程不安全”的简单类。
//判断长度
sb.length()>0
//设置长度
sb.setLength(xx)

数组

创建/初始化二维数组

int[][] a = new int[外数组数(行)][内数组数(列)];
int[][] a = {{},{},{}};//初始化一个2行3列的数组

遍历

String[][] arr = {{"aa", "bb", "dd"}, {"cc", "ee"}, {"ff", "gg", "ww", "ww", "ddd"}};
for (int i = 0; i < arr.length; i++) {
    for (int j = 0; j < arr[i].length; j++) {
        System.out.print(arr[i][j]); 
    }
    System.out.println();
}

尽量不要用for(int t : arr) ,因为效率比较差。

填充(初始化)

Arrays.fill(a, 1);//把数组a全填充为1【常用方法*】

比较两个数组是否完全相同

Arrays.equals(grid2[j], grid[i])

数组排序

Arrays.sort(arr)

stream(不建议用:性能很差)

Arrays.stream(arr).sum()
建议:自己遍历相加

链表

大概率用递归。简单好理解。

二叉搜索树

记住要合理利用二叉搜索树的特性:左子节点比根小,右子节点比根大

位运算

异或

符号:^
特性:相同为0,相异为1

技巧

true^1 -----> false
flase^1 ------> true
1^1 -----> 0
0^1 -----> 1
可用于遍历过程中,实现一个来回切换的变量。

十进制—> 二进制的两种方式

  • 数学方法:除以2,取余。重复这个过程。每次的余数就是一个二进制 从右到左的顺序(逆序)
  • 位移方法:(n >> i) & 1【n:目标数。 i把指定的位右移到第一位】

判断x和y是否出现: 一个0一个非0

if ((x==0) ^ (y==0)) {
    //出现了一个0,一个非0
}

Map

虽然在日常业务开发中,Map是继List之后,最常用的集合类。但是在算法中:
能不用map就尽量不要用map。因为map本身过于复杂,会导致占空间 也大大拖慢速度(仅限于算法题目中)。
但有时候 可以参考map的hash思想,如果元素的范围不大,可以把元素的ASCII码作为下标。

取的时候排除空值(在业务开发中也挺常用)

map.getOrDefault(key, 0)

队列

一般使用这个双端队列(开销小一点),注意名称是que,而不是Queue
Queue q = new ArrayDeque();

快慢指针

除了应用在比较好理解的滑动窗口上面(一前一后组成窗口)
还有一个很重要的就是让快慢指针的速度 满足一定的函数关系,比如2倍关系。那么当快指针到头的时候,慢指针刚好能到达一个依赖总长度的位置(比如中间)
但要注意边界点:想要在跳出循环时,慢指针刚好在指定位置,那么快指针的起步位置一般要比慢指针多一位。

二分查找

int left = 0; int right = n;
int mid = 0; 
//这个pos的初始化很关键,pos就是最终要取的值。
//可能遍历完也没给pos赋值,那么最终就是pos的初始值
int pos = right;

while(left <= right) {
    mid = left+(right-left>>1);//这个位运算提升效率明显
    if (potions[mid] > min) {
        pos = mid;//至少要符合要求的位置
        right = mid - 1;
    } else {
        left = mid + 1;
    }
}

mid的计算

  • 使用mid = left + (right - left)/2;//防止数据过大而溢出
  • 把除法改为位运算可提升效率

动态规划

简介

迭代思想,有点类似于数学里的归纳法(为了求出第n项,那么需要知道第n-1项。。。。)
听起来像是递归干的事。核心思想确实都是迭代。只不过动态规划一般使用非递归方式循环遍历。
关键是每一步都可以利用之前存起来的数据进行计算。最好的情况是:第i项刚好依赖第i-1项的数据。那么此时连额外的存储空间都不需要了。
你可能听到,动态规划的核心是:状态转移方程。
这个方程的意思就是你找到的计算第i项的通用公式。并且这个公式依赖前面计算好的数据。可以让整个计算过程,迭代成一条链,从第1项一直连接到第n项,得出最终结果。(不可迭代成一条链的公式就不属于状态转移方程)

优化

当写出状态转移方程,构建出一张“逐行扫描”的表。通过一行一行的填表数据,得到最终的答案。那么此时就可以考虑优化的问题了。优化的思想也很简单:

  • 看看每次处理这一行数据时,真正用到了多少表格数据。如果实际:计算这一行时,只依赖上一行数据。那么就可以把m x n的表格 优化成2 x n的表格。
  • 如果每一格的数据只依赖上一行,同一列的数据。那么可以进一步优化成一行1 x n
  • 如果处理的数据本身就是一维的。如斐波那契数列(nums[i] = nums[i-1] + nums[i-2])。就可以进一步优化到只有三个元素的数组。

前缀和

如果滑动窗口前后端不太好获取,那么转成前缀和(前i项 - 前(i-k)项)或许好计算一些。

易错点

判断条件上有方法直接用到了迭代变量,那么一定要先对迭代变量i进行判断

while(--i>=0 && Character.isDigit(s.charAt(i))) {
    sb.insert(0, s.charAt(i));
}

判空

栈在peek或者pop()前一定要先判断

if(stack.empty()) {
    
}

栈中如果存的是string,别忘了是用equals(经常往栈里存字符,有时候存字符串时就忘了)

while(!stack.peek().equals("]")) {
    sb.append(stack.pop());
}

递归

一旦用递归,就不能共用stringBuilder了,因为每一层都可能处理了一半,就调用下一层了。如果两层都在用stringBuilder就会冲突(如果算法本身就是想让多层,一点点修改stringBuilder,是可以的)。

比较

如果比较一串节点和另外一串节点 是否一样,不要用字符串拼接。因为 1,21 和 12,1拼接出来是一样的。可以用数组。然后用Arrays.equals(arr1, arr2)

特殊数据结构

这个数据结构在处理一些特殊算法时,特别好用巧妙。典型的比如判断括号匹配问题。因为它一边处理,一边甩包袱,而且能实现正进反出的效果。

递归的本质也是栈。用的是方法调用栈。

前缀树

前缀树,Trie树,也叫字典树
结构:自己存自己的数组类。然后加一个isEnd,表示单词结尾
实际生产局限性:虽然算法很巧妙,空间被最大化的利用。但实际生产中 如果要加中文,那么这个数组则会太大,过于浪费空间。还是老老实实存一个字符的更合适。
这个算法只能说是针对条件做的特殊优化。

总结

算法题的条件一般都很简单。在这样的完美条件下,为了追求性能产生了很多被极致优化的算法。在实际生产中可以参考一些思想,但能应用的其实并不多。

你可能感兴趣的:(算法,java,算法)