常见算法及技巧总结一
- 1.前言
- 2.整数运算
- 技巧一:类快速幂的加法
- 技巧二:判断二进制数中'1'的位数
- 技巧三:利用位与运算判断字符串中相同的字母
- 技巧四:使用异或运算
- 3.数组
- 技巧一:有序数组的二分法
- 技巧二:结合双指针的滑动窗口
- 技巧三:先排序再运算
- 技巧四:前缀和
- 4.字符串
- 技巧一:结合双指针的滑动窗口
- 技巧二:使用数组作为哈希表保存字符串各字母数量
- 技巧三:使用标识标识字符串
- 5.链表
- 技巧一:快慢指针
- 技巧二:递归遍历链表
- 6.单调栈
- 思路
- 单调栈模板
- 7.二分查找
- 思路一:有序数组
- 思路二:遍历某个范围内的数据找到符合条件的一个值
- 二分查找模板
- 技巧一:Z字形查找
1.前言
这篇算是我自己对《剑指offter》的总结吧,外加一些其他的算法。
2.整数运算
关于整数运算,常涉及的知识点有整数溢出的处理,位运算,十进制与二进制。
技巧一:类快速幂的加法
问题:计算两数相除的商,但不能使用乘法与除法或者取余。
可以使用类似于快速幂的加法技巧。
举个例子16除以2,暴力思路是用2累加并计算累加次数,直至累加和大于或等于16,计算次数为8次。
使用类似于快速幂的加法则为:2加2等于4,此时累加次数为2 。再使用4进行加法计算4加4等于8,注意此时累加次数为2加2等于4,最后8加8为16,累加次数为4加4等于8 ,计算次数为3次。计算结束16除以2的商为8。
力扣链接:
技巧二:判断二进制数中’1’的位数
利用 x=x&(x-1) 。 具体原理为,先把数字二进制的最后一位’1’变为0,如果此时数字数值不为0,则继续下去,直至数字数值为0,在此过程的同时保存’1’的位数。
具体代码:
int main(void) {
int tmp = 16;
int count = 0;
while (tmp) {
tmp = tmp & (tmp - 1);
count++;
}
cout << "tmp二进制中'1'的位数为" << count << endl;
system("pause");
return 0;
}
其实很好理解,一个数二进制中只含有一个1,如 0000 1000 。那么它减一为 0000 0111 。
0000 1000
0000 0111
再进行位与运算为0 。
技巧三:利用位与运算判断字符串中相同的字母
问题:如果有很多字符串(只包含小写字母),那么在比较其中两个字符串时如何确定它们之中存在相同的字母。
可以利用32位整数的掩码表示整个字符串。比如如果一个字符串中有a字母,那么表示该字符串的整数的掩码的第一位则置为1,如果还有c字母,则第三位也置为1,以此类推。不存在的字母掩码位都置为0。这样把所有字符串都用一个整数表示后,它们在两两判断是否存在相同字母时只需要这两个整数进行位与运算,如果结果不为0,则存在相同字母,为0,则不存在相同字母。
力扣链接:
技巧四:使用异或运算
首先明确异或运算的一些运算性质:
摘自力扣:
- 任何数与0做异或运算结果都是它本身。
- 任何数与自身做异或运算结果都是0。
- 异或运算满足交换律与结合律。
力扣链接:
3.数组
在处理数组时,常能用到的思路与技巧有有序数组的二分法,双指针法,滑动窗口,前缀和
技巧一:有序数组的二分法
对于已经有序的数组,查找某个数,最先应该想到的可能也是效率最高的方法就是二分法。
技巧二:结合双指针的滑动窗口
对于一个已经存在的数组,如果题目要你求满足条件的子数组或子数组长度(注意子数组与子序列的区别,子数组需连续,子序列不需要是连续的),可以考虑滑动窗口结合双指针去做。
力扣链接:
一般思路是,利用双指针维护一个滑动窗口,移动滑动窗口遍历整个数组,在移动的过程中根据题目要求扩张或者缩短滑动窗口。直至遍历完数组,返回滑动窗口或者滑动窗口大小。
关于滑动窗口,有一种思路是对于确定的左窗口或者右窗口,是否能找到唯一确定的满足题目要求的右窗口或左窗口。以数组中的每一个元素作为左窗口,找到所有符合条件的右窗口,遍历完数组,就找到了所有的解。同理以右窗口开始也是一样的。
关于滑动窗口,还有一种经典题型是看题目中是否出现了最大或最长等词。如果有,可能只需要一开始确定一个符合条件的滑动窗口,在遍历所有数据的过程中单调的扩增滑动窗口,直至遍历完所有数据,就得到了符合条件的最大的数据集合。
如果是求最小或最短的数据集合或数据集合长度,则可能是在一开始先确定一个符合条件的,再依次找出所有符合条件的同时保存最小的那个数据集合,直至遍历完整个数据集合,就得到了最小或最短的。
关于滑动窗口与双指针的应用情景有很多,这里只是说了大致的思路,还有很多不同的情况,需要具体问题具体分析。
技巧三:先排序再运算
对于暴力解法需要遍历数组中每一个数,让这个数与数组中其他数依次比较的情况。先排序可以极大的提高效率。
看这题: 最小时间差
技巧四:前缀和
当题目中要求在某些限制条件下求一个数据集合的和或者题目要求可以转换成求数据集合的和时,可以考虑使用前缀和。
前缀和的核心在于使用哈希表分别保存数据集合第一个数到之后各个数的和,然后通过连续作差来得出中间连续子数组的和。
举个例子,有一个数组,其中元素分别为1,3,5,6,8,10 。分别以第一个元素到各个元素的和作为哈希表的键值,并分别设置其哈希值为1 。
为
【1,1】,
【1+3,1】,
【1+3+5,1】,
【1+3+5+6,1】,
【1+3+5+6+8,1】,
【1+3+5+6+8+10,1】 ,即
【1,1】,
【4,1】,
【9,1】,
【15,1】,
【23,1】,
【33,1】,
现在要求该数组中是否存在一个连续的子数组,使得和为24 。我们可以使用各键值分别减去24,如果有一个哈希值为1 。则存在一个连续的子数组和为24 。最后我们可以发现使用33-24为9,以9为键值的哈希值为1(33为所有元素的和。) 。
其含义是什么呢?即该数组第一个元素到最后一个元素的和减去24为9,以9为键值的哈希值为1,说明存在从第一个元素到第n个元素的和为9,也说明存在从第n个元素到最后一个元素的和为24。所以原数组中存在一个连续子数组的和为24,即【6,8,10】。
力扣链接:
4.字符串
常涉及的知识点有,字符串匹配问题,双指针法,滑动窗口,
关于字符串匹配问题,一般有两种情况,一种是匹配排列。即有两个字符串,一个更长,要在更长的字符串中找到更短字符串的一种排列(字符串的排列是指把原字符串字母顺序打乱重新组成的新的字符串)。关于这个问题,可以看这题: 字符串的排列
还有一种是严格匹配的(即短字符串不能变化),即在更长字符串中找到更短字符串。这种一般是用KMP算法。
技巧一:结合双指针的滑动窗口
不仅在处理数组时能用到这种方法,在处理字符串时也能用到。
技巧二:使用数组作为哈希表保存字符串各字母数量
如果字符串中只存在小写字母,则只需要定义一个大小为26的数组,使用字母的ascll码值作为键值同时作为数组的下标,而数组值表示的是该字母在字符串中出现的次数。
看下面代码:
int main(void) {
string str = "helloworld";
int map[26] = { 0 }; //作为哈希表的数组
for (auto& ch : str) {
++map[ch - 'a']; //字母计数加一
}
for (int count = 0; count < 26; ++count) { //输出字符串中每个字母的个数
cout << (char)(count+'a') << "=" << map[count] << endl;
}
system("pause");
return 0;
}
注意:如果字符串中含有所有字符,那就把数组大小改为128 。
技巧三:使用标识标识字符串
以字符串作为哈希表的键值时,未必一定要直接以字符串作为键值,换一种思路。用一个可以唯一标识字符串的标记也能代替字符串作为键值。(本质上就是哈希)
在这里有两种不同的情况,一是字符串所含字母及字母数量都相同的两个字符串使用相同的标识,如abc与cba就是所含字母及字母数量都相同,所以它们的标识是一样的。
一:这种情况可以使用上一个技巧提到的使用数组作为哈希表的方法。
二:也可以使用乘积和的方法,即不同的字母使用不同的数字表示,而整个字符串就用这些数字的乘积和表示。不过这种方法可能要考虑哈希冲突与整数溢出的问题。
如果只是为了判断两个字符串是否含有相同字母及字母数量的话,只需要将字符串排序后再比较是否相同就行了。关于这个问题,可以看这题:字母异位词分组
还有一种情况是严格匹配的,即必须字符串所含字母,字母数量,字母顺序都相同才能使用相同标识。如上文的abc与cba在这种情况下就不能使用相同标识。
关于这个问题,目前我只知道一种方法。即不同字母用不同数字表示,字母的顺序用进位来表示。
举个例子,如a b c d e f 分别用数字1 2 3 4 5 6 表示。则字符串abcde用数字12345表示。字符串fabe用数字6125表示。
这种方法在某种情况下能极大的提高效率,感兴趣的可以看我这篇博客: 对于Rabin-Karp算法的一些理解
5.链表
链表作为一种最基本数据结构,掌握链表要求我们能够独立实现一个具有完善功能的链表,这个完善的功能包括链表的创建,增加元素,插入元素,删除元素,按顺序输出元素。
技巧一:快慢指针
即使用两个指针,一个在循环中一次向链表尾走一步,另一个一次走两步。直至其中一个走到链表尾为止。
常用来判断链表是否成环,或用于取链表中点。
力扣链接:
技巧二:递归遍历链表
关于递归遍历链表值得一提的一点是可以使用一个外部指针,当递归至链表尾开始回升时,外部指针同时往下遍历。这样处理链表问题时可以两路进行,即递归的逆序遍历与外部指针的正向遍历。在某些情况下很好用。
力扣链接:回文链表
6.单调栈
思路
关于单调栈,难的不在于如何使用单调栈,而在于判断题目是否能使用单调栈来优化。
如果题目要求需要找到数组中每一个元素第一个大于或小于当前元素的数。从暴力思路出发当然是对于每一个元素从当前位置出发,直至找到第一个大于或小于当前元素的数。但这种情况往往能使用单调栈优化。
力扣链接:
单调栈模板
单调栈里可以存数组值,也可以存数组值的下标。具体存什么看题目要求,涉及滑动窗口的一般就存数组值的下标。
stack<int> sta;
for(int count=0;count<k;++count){
while(sta.size() && nums[count]>=nums[sta.top()]){
nums[sta.top()]=count;
sta.pop();
}
sta.push(count);
}
7.二分查找
二分查找的代码实现也不难,关键是要判断什么时候可以使用二分查找,这里提供两种思路。
思路一:有序数组
对于有序的数组,查找某个数,首先考虑二分查找。
思路二:遍历某个范围内的数据找到符合条件的一个值
如果需要从一个限定的范围(这个范围是连续的)内找到一个符合条件的值,暴力思路是只能一个一个试看符不符合条件,那么可能可以使用二分查找优化。
可以看看这题:爱吃香蕉的珂珂
二分查找模板
while (l < r) {
int mid = l + (l + r) / 2; //取中值
if (nums[mid] < target ) l = mid + 1; //往左边找
else if(nums[mid] == target ) return left; //找到了要找的元素,返回
else r = mid; //往右边找
}
或者这种形式
while (l < = r) { //不同
int mid = l + (l + r) / 2; //取中值
if (nums[mid] < target ) l = mid + 1; //往左边找
else if(nums[mid] == target ) return left; //找到了要找的元素,返回
else r = mid - 1; //往右边找 不同
}
技巧一:Z字形查找
对于一个二维数组,如果它的行与列的元素都是按升序或者降序排列的。那么在这样的数组中查找一个元素,可以使用 Z 字形查找。以充分利用行与列都是有序的特性。
给个力扣链接:搜索二维矩阵 ||