搜索算法
二分查找模板
当二分查找right取size的时候,偶数时middile是比一半多一个元素,奇数时middle是中间元素
当right取size-1的时候,偶数时middle是一半元素,奇数时middle是中间元素
闭区间查找形式
1 | int left = 0; |
左闭又开查找
1 | int left = 0; |
二维数组中的查找
从右上角或这最小角开始
右上角:
如果右上角元素比目标值大,说明该列都比目标值大,剔除该列,column–
如果右上角元素比目标值小,说明该行都比目标值小,提出该行,row++
左下角:
如果左下角元素比比目标值大,说明该行都比目标值大,剔除该行,row–;
如果最小角元素比目标值小,说明该列都比目标值小,剔除该列,column++;
1 | public boolean Find(int target, int [][] array) { |
数字在升序数组中出现的次数
- 二分查找
利用二分查找查找重复数字的下界:重复数字的第一个 上界最后一个重复数字的下一个
利用如下函数返回左边界,当目标值存在的时候会返回第一个目标值的索引,当目标值不存在的时候
如果目标值比数组最小值小的时候,会一直移动右指针,一直移动到索引0的位置,和left相等,返回left=0
如果目标值比最大值还要大的时候,一直移动右指针,一直移动到left与right相等,返回left=length
1 | private int binarySearch(int[] array, int target) { |
target不存在的时候,说明重复次数为0,要返回零,通过num[left] != k 与 left==length判断出不存在target,但是要先判断left==length,不然num[left]会越界
如果数组中有k+1,会返回第一个k+1,正好是k的下一个
如果数组中没有k+1
k+1比数组元素都大时,返回size
有比k+1大的数时,返回第一个比k+1大的数的索引
1 | public int GetNumberOfK(int [] array , int k) { |
旋转数组的最小值
- 暴力法
- 二分法
思路:把右端点当target与middle比较
旋转数组大小关系如上图
如果midde比右端点大,说明middle在①处,最小值在middle右边,left=middle+1
如果middle比右端点小,说明middle在②处,最小值在middle左边,包括middle,right=middle
如果middle与右端点相等,无法确定,可能在③或④,不能确定最小值在middle的哪一侧,只能让right慢慢移动
1 | int left = 0; |
数字序列中某一位数字
找规律
1~9 9个数
10~99 90个数
100~999 900个数
1000~9999 9000个数
如果写成digit * start * 9L,digit * start仍然可能溢出,不会升级为long,遇到第一个long后才会升级
1 | public int findNthDigit (int n) { |
回溯
回溯算法,全称是回溯搜索算法,通过递归嵌套多层遍历,寻找可能的答案。
回溯三部曲
1.确定函数的返回值和参数,一般为void
void backtrack(参数)
2.确定回溯的终止条件
if(终止条件){
保存结果
return;
}
3.确定回溯搜索的遍历
for(选择本层结集合的元素){
处理节点;
backtrack(路径,选择列表);
回溯,撤销处理结果;
}
组合问题
一般组合
https://leetcode-cn.com/problems/combinations/
注意startIndex是索引,从0开始,for循环遍历i<n-remaining+1,小于号,不含等于
1 | // 定义全局的result和path,result保存最终的结果,path存储单一的结果 |
组合总和Ⅲ
https://leetcode-cn.com/problems/combination-sum-iii/
本题与一般组合题基本一致,只不过集合总大小固定为9,多出了一个总和n,在一开始结题的时候犯了错误,使用path.size()==k && sum==n作为终止条件,这样是不对的,只要当size()==k时,便可以终止遍历,因为已经组合需要的元素个数已经足够了,继续遍历只会增加组合的个数,肯定不满足大小k。所以只要size()==k时,便可以终止遍历。而结果还要求sum==n,所以再进行判断,满足条件加入结果,不满足不加入,但仍要返回。
由于有剪枝判断,i<9-remaining+1有越界并无限递归的风险,即,当path.size()>=k时,
k-path.size()<=0,9-remaining+1>=10,且随着递归深度增加,path尺寸增加,9-remaining+1也在增加,每次递归startIndex为i+1,只增加1,与9-remaining+1增加的速度相同,for循环无法停止,无限递归
但是幸好前面有递归终止条件,一定要写对,当path达到要求的组合数目后要返回
1 | private List<List<Integer>> result = new LinkedList<>(); |
组合总和Ⅰ
https://leetcode-cn.com/problems/combination-sum/
不含重复元素,但是同一个元素可以重复出现多次
- 未剪枝
思路:仍然根一般组合问题差不多,不过终止条件改为了路径和是否达到了目标值,已经startIndex 不再是i+1,而是i,因为当前数字可以无限重复,即使被选择了,仍然可以再次选择,不过由于是组合问题,不能再选之前选过的元素了
1 | private List<List<Integer>> result = new LinkedList<>(); |
- 剪枝
回溯搜索以前,对数组进行了排序处理,这是可以剪枝的关键
for循环条件不满足时,会进行break操作,即立即结束循环,不是continue。
未进行剪枝前,不满足条件时,需要递归到下一层,然后立即返回,同样也会消耗时间与空间,可以在进入下一层之前,进行判断。对数组排序,如果target-candidates[i] < 0 后续candidates逐渐增大,同样不满足条件,所以可以结束循环,target-candidates[i]>=0不满足时相当于进行了break
1 | private List<List<Integer>> result = new LinkedList<>(); |
组合总和Ⅱ
https://leetcode-cn.com/problems/combination-sum-ii/
给定的数组中有重复元素,结果中不能有重复组合
- 可以利用set去重,但是效率太慢
- 利用used数组,去重
思路:先对给定的数组排序,相同元素就一定时相邻元素,相同元素在同一层上被选择过了,就不能再次选择了,需要使用continue跳过,相同元素再同一层上使用过的特征为
candidates[i]==candidates[i-1] && used[i-1]==false
前者确定了是相同元素,后者确定了是同一层的不是第一个出现的元素
该做法类似于分类求组合,比如按照第一个数是1,第一个是2,第一个数是3分成三大类,然后第二个数是1,第二个数是2,第二个数是三继续分类
1 | private List<List<Integer>> result = new LinkedList<>(); |
- 利用startIndex判断是否是第一个出现的重复元素
candidates[i]==candidates[i-1] && i>startIndex
关键为上述判断条件,前者确定了是重复元素,而且不是第一次出现,关键是区分开是在下一层再次出现了,还是本层再次出现了,下一层再次出现的化都有一个特定,那就是i与startIndex相等,如果不想等就说明是在本层再次出现了
1 | private List<List<Integer>> result = new LinkedList<>(); |
排列问题
不重复元素的全排列
https://leetcode-cn.com/problems/permutations/
思路:与组合问题的区别是,需要used数组记录哪些元素是用过的,而且每次循环都是从0开始,而不需要从i+1遍历
1 | // 全局属性,用于保存path与result |
- 使用set去重
思路:
结果寻找和无重复元素一样,不过利用set去重,去重的时候要注意remove()之后,后续元素会整体向前移动,记得i–
1 | private StringBuilder path = new StringBuilder(); |
- 使用used数组
使用used数组去重,str.charAt(i)== str.charAt(i-1) && used[i-1]==false
前者代表不是首次出现的重复元素,后者表示在同一层中再次出现的重复元素
重点步骤:排序,利用used不能重复使用同一个元素,以及去重
used数组有两个作用,首先是不能使用已经使用过的元素,还要去重
1 | private StringBuilder path = new StringBuilder(); |
矩阵中的路径
- 回溯
思路:回溯法,遍历矩阵的每个元素,当成起点,进行搜索。每个元素下一个前进的方向都有4种,按逆时针上左下右,依次选择。选择的时候要求没有越界,并且没有被访问过,进一步可以加上剪枝操作,待选择的字符是否和下一个字符相等
1.回溯函数返回值与参数
待搜索的矩阵,要匹配的字符串,
void backtrack(char[][] matrix, String word, int row, int column, boolean[][] used)
2.确定终止条件
a. 当元素的四周都无法访问时终止
b. 当路径的长度和字符串的长度相等时需要终止,然后如果匹配上了,要把result设置为true
c. 为了在找到结果后,终止不必要的搜索,可以在递归函数的第一行判断是否找到了结果,但是这其实会多一层递归,可以在每种情况的递归执行完判断,不用递归到下一层判断。
经分析发现a条件可以不用写,隐藏在了下面的if判断中
3.确定回溯搜索的遍历
上左下右四周的搜索不好使用for循环遍历,只有四种情况,可以直接罗列出来。每种情况是否可以选择时,需要满足没有越界,并且没有被访问过,还要加入剪枝操作,待加入的字符是否和下一个字符匹配
1 | private StringBuilder path = new StringBuilder(); |
- 算法改进
发现只要path的长度等于word的长度,就表示已经找到了,因为只有相等时才加入path,无需path与word比较,那也不需要path记录了
终止条件变为了
1 | if (this.counter == word.length()-1) { |
另外,可以利用短路或剪枝,不再进行后续搜索
写代码的时候要全神贯注,认真仔细,不然一个bug找半天
row-1 写成了row+1,递归不好调试,利用好print,打印查看每次调用的path
1 | private int rows; |
机器人的运动范围
- dfs
思路:使用visited数组标记到过的位置,不需要回溯,因为是求总和,需要记录所有路径的总和,而不是记录一种路径。从(0,0)开始移动,每个位置都有4个方向供选择,只要选择的方向没有越界,且没有被到访过,就可以向此方向移动
递归三部曲
1.确定返回类型与参数
void dfs(int threshhold, int row, int col, int rows, int cols, boolean[][] visited)
2.确定终止条件
终止条件是四周都没有路可以走了,通过单层递归逻辑if即可实现,不用单独写终止条件
3.确定单层递归逻辑
如果没有越界,且没有被访问过,总和增加1,标记当前位置,向周围四个方向递归
本层只包含处理当前节点的逻辑,而不提前处理下一层节点,这样逻辑比较清晰,代码比较简洁
1 | // 定义全局变量 |
美团笔试-路径中最少未探明的区域
最少的未探明区域
oxo
oox
ooo
从(0,0)到(m, n)经过最短的x的数量
回溯法不是最优的,但是在本题使用回溯法,对回溯法又有了新的感受
在下方的backtrack中,在本层递归中处理了下一层递归访问的元素,代码逻辑复杂
在改进的backtrackImproved中,本层递归中只处理本层当前元素,不同于一般的回溯问题,使用for横向遍历,本问题横向只有固定两种情况,可以直接写出来,而且横向的所有情况当前
元素的值相同,可以只用回溯一次即可。在横向遍历前更改状态,在横向遍历后复原状态。
另外还可以把path当成参数,path + currentValue,这样每次调用函数,都会回溯一次,相当于
path += currentValue
backtrack()
path -= currentValue
path += currentValue
backtrack()
path -= currentValue
1 | import java.util.*; |