刷题笔记
滑动窗口
滑动窗口可以解决子结构中求最值问题,例如:
解决滑动窗口问题包含几个步骤:
- 确定窗口大小
- 移动右指针使得窗口最大,更新结果
- 移动左指针缩小窗口,使得窗口保持最大,更新结果
要理解滑动窗口不难,难点在于如何正确的把所有细节都写出来,以 无重复字符的最长子串为例:
给定一个字符串
s
,请你找出其中不含有重复字符的 最长子串 的长度。
这类问题是找求最大的窗口大小,难度不高,不过要注意是不含重复字符的,因此我们可以一个HashSet来实时存储当前窗口中的无重复字符。
何时移动右指针增大窗口?当HashSet中不存在当前字符时,增大窗口。
何时移动左指针缩小窗口?当HashSet中存在当前字符时,缩小窗口。
每一次遍历,我们都需要更新最大窗口的值,遍历完之后得到的最大窗口值即为结果。
1 |
|
二分查找
一般看到有序就要考虑二分,通常不会直接让你写二分查找,大部分都是变形题。
正常的二分查找
二分查找的原理很简单,但细节很多,这里有几个需要注意的:
1 |
|
如果把右边界定为数组长度 - 1
,那么循环的进入条件就是 left <= right
。
对于mid的算法,使用left + (right - left) / 2
的方式可以防止left + right
可能溢出的问题,当然如果题目给出的数据范围是不可能溢出的话正常写就行 。
求左右边界
一般来说算法中不会直接让我们直接套二分查找,而是要求我们利用二分查找来寻找重复数据的左右边界。
对于左边界,只需要对上面的代码一些小改动即可:
1 |
|
为了寻找左边界,当我们发现mid对应的数等于target时,我们不能直接返回,而是缩小搜索区域为mid左边,即right = mid - 1
。
这里有几点需要特别注意:
第一点,搜索结束时,应该返回left而不是right。
为什么返回left,只需要考虑最后一次循环即可快速理解。最后一次循环时,left、mid、right三者相等,此时将会进入第一个if分支中,
right = mid - 1
,使得right会小于我们所求的边界,因此left才是最终答案。第二点,如果搜索不到对应的target时,需要考虑越界问题。
如果target不在待搜索数组中,有三种情况:
- target小于数组中的所有数
- target小于数组中的部分数,大于数组中的另一部分数
- target大于数组中的所有数
对于第一种和第二种情况,当循环结束时,left都不会越界,因此只需要考虑nums[left]与target是否相等就知道target存不存在数组中。
对于第三种情况,经过最后一次循环时,left将会被赋值为mid + 1,导致最终left为数组长度,此时使用nums[left]将会有越界问题,所以需要单独判断。
最终通过
left == nums.length || nums[left] != target)
这个表达式我们可以判断出数组中是否是不存在target的。
对于右边界,代码与左边界是类似的,如下所示,这里不再赘述:
1 |
|
回溯算法
回溯算法即带有撤销操作的DFS算法,用于处理需要求出所有可能的结果的问题。
算法基本逃不出这个框架:
1 |
|
难点在于如何去除重复项,即剪枝。
剪枝要考虑清除题目要求的不可重复项,例如组合2中要求不出现重复的数字,而子集2中要求不能出现重复的子集。
对于无法确定的要先画出决策树,再考虑应该减去同一层重复的还是同一路径重复的。
对于同一路径去重,即组合2问题,只需要对源数据进行排序后,判断前后数字是否相同即可。
对于同一层去重,即子集2问题,需要引入一个visited的boolean标志位数组,选择当前数字后,在撤销选择时将其visited[i]标记为true,表示此层使用此数字完成了一此回溯选择,同一层的重复数字遇到此标记位为true时就可以不用继续了,因为必定重复。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!