一、kmp算法
kmp算法详解
kmp算法
可和滑动窗口算法一起对比记录。
kmp算法为解决字符串匹配问题,即给你两个字符串,寻找其中一个字符串是否包含另一个字符串,如果包含,返回包含的起始位置。
1、传统暴力求解模式串在主串位置时,往往使用双指针在两个字符串之间进行查找,若i和j存在不匹配的情况,则移动主串上的指针i,即i++,j则回到模式串的起始位置,即j=0。
kmp算法则是在此基础上进行优化。这里主要在子串的定位上做出一定的变化。该算法主要是next数组的求解,next数组中存储子串下标j的下一趟位置。即整个KMP的重点就在于当某一个字符与主串不匹配时,我们应该知道j指针要移动到哪。
2、下图是模式串的next位置求解,一般来说,next存储的位置存在着这样的性质:最前面的k个字符和j之前的最后k个字符是一样的。即p[0~k-1]=p[j-k,j-1]。next[j] = k,表示当T[i] != P[j]时,j指针的下一个位置。
其中next数组计算过程还不明确,后面再仔细看看。主要还是当主串和模式串不匹配时,主串的下标不变,只改变模式串的下标,这里模式串的下标指向next数组存储的下标。
二、滑动窗口
滑动窗口算法总结
滑动窗口算法参考tcp滑动窗口机制,使用双指针进行窗口的选定,一般解决查找满足一定条件的连续区间的问题,比如在一个数组中查找和为s的连续正序列。
滑动窗口算法思路:
1、初始化left=right=0,窗口为闭区间[left,right].
2、不断增加right,直到窗口中的字符满足条件后,不再增加right
3、开始增加left,直到窗口中的字符不满足条件时停止增加
4、重复2、3步骤直到right到达字符串边界
三、回溯算法
1、回溯和递归的区别:我们在路上走着,前面是一个多岔路口,因为我们并不知道应该走哪条路,所以我们需要尝试。尝试的过程就是一个函数。
我们选择了一个方向,后来发现又有一个多岔路口,这时候又需要进行一次选择。所以我们需要在上一次尝试结果的基础上,再做一次尝试,即在函数内部再调用一次函数,这就是递归的过程。
这样重复了若干次之后,发现这次选择的这条路走不通,这时候我们知道我们上一个路口选错了,所以我们要回到上一个路口重新选择其他路,这就是回溯的思想。
2、回溯其实也是穷举,只是为了少一点for循环用了递归,可以看成树的结构,且还可以加入减枝,但回溯算法效益还是比较小。当把回溯看成树结构时,可以看作是先纵向递归后再横向遍历。
*组合问题:N个数⾥⾯按⼀定规则找出k个数的集合
*排列问题:N个数按⼀定规则全排列,有⼏种排列⽅式
*切割问题:⼀个字符串按⼀定规则有⼏种切割⽅式
*⼦集问题:⼀个N个数的集合⾥有多少符合条件的⼦集
*棋盘问题:N皇后,解数独等等
3、回溯算法的模板:
void backtracking(参数) {
if (终⽌条件) {
存放结果;
return;
}
for (i=startindex/0选择:本层集合中元素(树中节点孩⼦的数量就是集合的⼤⼩)) {
处理节点;
backtracking(i/i+1路径,选择列表); // 递归
回溯,撤销处理结果
}
}
注:1)三步走:返回值以及参数的确定、终止条件的确定、遍历过程的确定
2)、一般没有返回类型,即函数类型为void。
3)注意每次回溯时当前位置的传递。
例如如上图所示,在数组1-n中查找两个数的组合,里面的递归是从上往下遍历的,外层的for循环是从左到右遍历,for循环中i的范围就是没层遍历时的候选集和。
a、一个数组不能重复取时:startindex,i+1
b、一个数组(无重复元素的数组)可以重复取但结果不能重合时(即结果中的数是无顺序的):startindex,i
c、多个数组不可重复取时:0,i+1(如电话号码,一个数对应不同的字母组合)
d.一个数组(有重复元素的数组)不可以重复取时(leetcode40组合总和II):创建一个记录一个数是否使用过的数组used进行减枝,startindex,i+1
4、注:注意递归中可能暗藏着回溯的思想,写的时候注意这点,如:
if (cur->left) traversal(cur->left, path + "->", result); // 左 回溯就隐藏在这⾥
//相当于
if (cur->left) {
path += "->";
traversal(cur->left, path, result); // 左
path.pop_back(); // 回溯
path.pop_back();
}
5、中间结果去重问题分析
树枝去重or树层去重
可以用数组或者set进行去重
1)如果像全排列问题中,一个集合中(无重复元素)所有元素都需要出现一遍,则需要在每次回溯中传入used数组记录根节点下每一树数的使用情况。backtracking(nums, used);
2)若一个集合中有重复元素,这时可能需要同一树层去重,对于同一树层去重主要有两种去重方案:a、对于数值范围确定的可以直接使用一个数组记录数字每层的使用情况,这里数值范围[-100,100]:
int used[201] = {0}; // 这⾥使⽤数组来进⾏去重操作,题⽬说数值范围[-100, 100]
for (int i = startIndex; i < nums.size(); i++) {
if ((!path.empty() && nums[i] < path.back())
|| used[nums[i] + 100] == 1) {
continue;
}
used[nums[i] + 100] = 1; // 记录这个元素在本层⽤过了,本层后⾯不能再⽤了
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
b、根据下标记录数的使用情况
强调⼀下,树层去重的话,需要对数组排序!
可以看出在nums[i] == nums[i - 1]相同的情况下:
used[i - 1] == true,说明同⼀树⽀nums[i - 1]使⽤过
used[i - 1] == false,说明同⼀树层nums[i - 1]使⽤过
待续。。。
6、棋盘类问题如何利用回溯解决
对棋盘格进行上下左右的回溯。
四、二分查找
1、 二分查找要求:线性表是有序表,即表中结点按关键字有序,并且要用向量作为表的存储结构。
2、优点:折半查找的时间复杂度为O(logn),远远好于顺序查找的O(n)。缺点:虽然二分查找的效率高,但是要将表按关键字排序。而排序本身是一种很费时的运算。既使采用高效率的排序方法也要花费O(nlgn)的时间。
3、二分查找的框架
int binarySearch(int[] nums, int target) {
int left = 0, right = ...;
while(...) {
int mid = (right + left) / 2;//或者是mid=left+(right-left)/2,防止溢出
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = ...
} else if (nums[mid] > target) {
right = ...
}
}
return ...;
}
五、关于树的一些算法思想
1、遍历树通常可以使用深度优先搜索,可以用递归法也可以用迭代,如果是搜索某条路径返回值可能不为void,如果是搜索整棵树,返回值往往是void。
六、位运算
1、java支持的位运算
注:将a,b两个数转换成补码才能进行位运算。符号位0为正,1为负。
- 位与(&):二元运算符,两个为1时结果为1,否则为0
- 位或(|):二元运算符,两个其中有一个为1时结果就为1,否则为0
- 位异或(^):二元运算符,两个数同时为1或0时结果为1,否则为0
- 位取非(~):一元运算符,取反操作,按位取反,如
~5=-6
- 左移(<<):一元运算符,按位左移一定的位置。高位溢出,低位补符号位,符号位不变。
- 右移(>>):一元运算符,按位右移一定的位置。高位补符号位,符号位不变,低位溢出。
- 无符号右移(>>>):一元运算符,符号位(即最高位)保留,其它位置向右移动,高位补零,低位溢出。
2、注意事项
1)左移运算有乘以2的N次方的效果。一个数向左移动1位,就相当于乘以2的1次方,移动两位就相当于乘以2的2次方,也就是乘以4。位移操作在实际运算时远远快于乘法操作,所以在某些对运算速度要求非常高的场合,可以考虑用左移代替乘以2的N次方的乘法操作。如5<<2为20.
2)当位移的位数很多时,导致最左边的符号位发生变化,就不再具有乘以2的N次方的效果了。如果这个负数是“2的N次方”的整数倍,那么带符号右移N位的效果也等于除以2的N次方。而如果这个负数不是“2的N次方”的整数倍,那么右移N位之后,是在除以2的N次方的结果之上还要减去1。
3)对于byte/short/int三种类型的数据,Java语言最多支持31位的位移运算。如果位移数超过31,则虚拟机会对位移数按连续减去32,直到得到一个小于32并且大于等于0的数,然后以这个数作为最终的位移数。例如对int型变量进行位移97位的操作,虚拟机会首先对97连续减去3个32,最终得到数字1,实际进行位移运算时只对变量位移1位。而对于long类型的数据而言,最多支持63位的位移运算,如果位移数超过63,则连续减去64,以最终得到的小于64并且大于等于0的数作为位移数。小伙伴们可以试一下数字5左移32位是什么结果。
4)对于正数而言,带符号右移之后产生的数字确实等于除以2的N次方,
5)对于正数而言,无符号右移和带符号右移没有什么区别,而对于负数而言,经过无符号右移会产生一个正数,因为最左边的符号位被0填充了。
3、通过位运算实现加减乘除
注:减法可以由加法获得,除法可以由乘法获得。
位运算实现加减乘除java
//加法
public int add(int a,int b) {
int carry;
while(b!=0) {
carry=(a&b)<<1;
a=a^b;//相当于不考虑进位的加法
b=carry;
}
return a;
}
//减法(反码加1)
public int subtraction(int a,int b) {
b=~b+1;
return this.add(a, b);
}
//乘法
public int multipliction(int a,int b) {
int x = a>=0?a:~(a-1);
int y = b>=0?b:~(b-1);
int i=0;
int res=0;
while(y!=0) {
if((y&1)==1) {
res=res+(x<<i);
y=y>>1;
i++;
}
else {
y=y>>1;
i++;
}
}
return (a^b)<0?(~res)+1:res;
}
//除法(求a可以由多少个b组成。那么由此我们可得除法的实现:求a能减去多少个b,做减法的次数就是除法的商。)
public int division(int a,int b){
int x = a>=0?a:~(a-1);
int y = b>=0?b:~(b-1);
int res;
if(x<y){
return 0;
}else{
res=division(subtraction(x, y), y)+1;
}
return (a^b)<0?(~res)+1:res;
}
七、关于几数之和问题的总结
1、如果是问几数之和各个数的组合数,如之前的三数之和那题,一般是用到二分或者双指针这种能够得到具体数字情况的方法来降低循环的层数。
2、如果是问几数之和中符合情况的个数,一般都是用hash表降低循环层数,此时我们只需要关注答案的次数即可。
*比如对于四数之和问题,原本需要四个循环才能枚举出所有的四数之和情况,此题需要满足的条件为:
A[i]+B[j]+C[k]+D[l]=0 =>A[i]=-(B[j]+C[k]+D[l]) =>A[i]+B[j]=-(C[k]+D[l])
对于第一种表达式,我们需要套用四层循环枚举 O(n^4)
对于第二种需要两个并列的循环枚举(一层和三层循环) O(n^3)
对于第三种需要两种并列的循环枚举(两层和两层循环) O(n^2)
*二分查找:是在有序数组中利用左右下标查找一个target的最佳方法,O(logn).
双指针:在有序数组(不局限于数组)中利用左右指针所指的两个数来查找两数之和是否等于target的最佳方法,O(n).
单调栈、单调队列、优先级队列
单调栈、单调队列(leetcode题举例)