二分查找法做为一种常见的查找方法,将本来是线性时间提高到了对数时间范围,大大缩短了搜索时间,具备很大的应用场景,而在 LeetCode 中,要运用二分搜索法来解的题目也有不少,可是实际上二分查找法的查找目标有不少种,并且在细节写法也有一些变化。以前有网友留言但愿博主能针对二分查找法的具体写法作个总结,博主因为以前一直很忙,一直拖着没写,为了树立博主言出必行的正面形象,不能再无限制的拖下去了,那么今天就来作个了断吧,总结写起来~ (如下内容均为博主本身的总结,并不权威,权当参考,欢迎各位大神们留言讨论指正)html
根据查找的目标不一样,博主将二分查找法主要分为如下五类:面试
第一类: 需查找和目标值彻底相等的数数组
这是最简单的一类,也是咱们最开始学二分查找法须要解决的问题,好比咱们有数组 [2, 4, 5, 6, 9],target = 6,那么咱们能够写出二分查找法的代码以下:函数
int find(vector<int>& nums, int target) { int left = 0, right = nums.size(); while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] == target) return mid; else if (nums[mid] < target) left = mid + 1; else right = mid; } return -1; }
会返回3,也就是 target 的在数组中的位置。注意二分查找法的写法并不惟一,主要能够变更地方有四处:post
第一处是 right 的初始化,能够写成 nums.size() 或者 nums.size() - 1。url
第二处是 left 和 right 的关系,能够写成 left < right 或者 left <= right。spa
第三处是更新 right 的赋值,能够写成 right = mid 或者 right = mid - 1。指针
第四处是最后返回值,能够返回 left,right,或 right - 1。code
可是这些不一样的写法并不能随机的组合,像博主的那种写法,若 right 初始化为了 nums.size(),那么就必须用 left < right,而最后的 right 的赋值必须用 right = mid。可是若是咱们 right 初始化为 nums.size() - 1,那么就必须用 left <= right,而且right的赋值要写成 right = mid - 1,否则就会出错。因此博主的建议是选择一套本身喜欢的写法,而且记住,实在不行就带简单的例子来一步一步执行,肯定正确的写法也行。htm
第一类应用实例:
第二类: 查找第一个不小于目标值的数,可变形为查找最后一个小于目标值的数
这是比较常见的一类,由于咱们要查找的目标值不必定会在数组中出现,也有多是跟目标值相等的数在数组中并不惟一,而是有多个,那么这种状况下 nums[mid] == target 这条判断语句就没有必要存在。好比在数组 [2, 4, 5, 6, 9] 中查找数字3,就会返回数字4的位置;在数组 [0, 1, 1, 1, 1] 中查找数字1,就会返回第一个数字1的位置。咱们可使用以下代码:
int find(vector<int>& nums, int target) { int left = 0, right = nums.size(); while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] < target) left = mid + 1; else right = mid; } return right; }
最后咱们须要返回的位置就是 right 指针指向的地方。在 C++ 的 STL 中有专门的查找第一个不小于目标值的数的函数 lower_bound,在博主的解法中也会时不时的用到这个函数。可是若是面试的时候人家不让使用内置函数,那么咱们只能老老实实写上面这段二分查找的函数。
这一类能够轻松的变形为查找最后一个小于目标值的数,怎么变呢。咱们已经找到了第一个不小于目标值的数,那么再往前退一位,返回 right - 1,就是最后一个小于目标值的数。
第二类应用实例:
第三类: 查找第一个大于目标值的数,可变形为查找最后一个不大于目标值的数
这一类也比较常见,尤为是查找第一个大于目标值的数,在 C++ 的 STL 也有专门的函数 upper_bound,这里跟上面的那种状况的写法上很类似,只须要添加一个等号,将以前的 nums[mid] < target 变成 nums[mid] <= target,就这一个小小的变化,其实直接就改变了搜索的方向,使得在数组中有不少跟目标值相同的数字存在的状况下,返回最后一个相同的数字的下一个位置。好比在数组 [2, 4, 5, 6, 9] 中查找数字3,仍是返回数字4的位置,这跟上面那查找方式返回的结果相同,由于数字4在此数组中既是第一个不小于目标值3的数,也是第一个大于目标值3的数,因此 make sense;在数组 [0, 1, 1, 1, 1] 中查找数字1,就会返回坐标5,经过对比返回的坐标和数组的长度,咱们就知道是否存在这样一个大于目标值的数。参见下面的代码:
int find(vector<int>& nums, int target) { int left = 0, right = nums.size(); while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] <= target) left = mid + 1; else right = mid; } return right; }
这一类能够轻松的变形为查找最后一个不大于目标值的数,怎么变呢。咱们已经找到了第一个大于目标值的数,那么再往前退一位,返回 right - 1,就是最后一个不大于目标值的数。好比在数组 [0, 1, 1, 1, 1] 中查找数字1,就会返回最后一个数字1的位置4,这在有些状况下是须要这么作的。
第三类应用实例:
Kth Smallest Element in a Sorted Matrix
第三类变形应用示例:
第四类: 用子函数看成判断关系(一般由 mid 计算得出)
这是最令博主头疼的一类,并且一般状况下都很难。由于这里在二分查找法重要的比较大小的地方使用到了子函数,并非以前三类中简单的数字大小的比较,好比 Split Array Largest Sum 那道题中的解法一,就是根据是否能分割数组来肯定下一步搜索的范围。相似的还有 Guess Number Higher or Lower 这道题,是根据给定函数 guess 的返回值状况来肯定搜索的范围。对于这类题目,博主也很无奈,遇到了只能自求多福了。
第四类应用实例:
Split Array Largest Sum, Guess Number Higher or Lower,Find K Closest Elements,Find K-th Smallest Pair Distance,Kth Smallest Number in Multiplication Table,Maximum Average Subarray II,Minimize Max Distance to Gas Station,Swim in Rising Water,Koko Eating Bananas,Nth Magical Number
第五类: 其余(一般 target 值不固定)
有些题目不属于上述的四类,可是仍是须要用到二分搜索法,好比这道 Find Peak Element,求的是数组的局部峰值。因为是求的峰值,须要跟相邻的数字比较,那么 target 就不是一个固定的值,并且这道题的必定要注意的是 right 的初始化,必定要是 nums.size() - 1,这是因为算出了 mid 后,nums[mid] 要和 nums[mid+1] 比较,若是 right 初始化为 nums.size() 的话,mid+1 可能会越界,从而不能找到正确的值,同时 while 循环的终止条件必须是 left < right,不能有等号。
相似的还有一道 H-Index II,这道题的 target 也不是一个固定值,而是 len-mid,这就很意思了,跟上面的 nums[mid+1] 有殊途同归之妙,target 值都随着 mid 值的变化而变化,这里的right的初始化,必定要是 nums.size() - 1,而 while 循环的终止条件必须是 left <= right,这里又必需要有等号,是否是很头大 -.-!!!
其实仔细分析的话,能够发现其实这跟第四类仍是比较类似,类似点是都很难 -.-!!!,第四类中虽然是用子函数来判断关系,但大部分时候 mid 也会做为一个参数带入子函数进行计算,这样实际上最终算出的值仍是受 mid 的影响,可是 right 却能够初始化为数组长度,循环条件也能够不带等号,你们能够对比区别一下~
第五类应用实例:
综上所述,博主大体将二分搜索法的应用场景分红了主要这五类,其中第二类和第三类还有各自的扩展。根据目前博主的经验来看,第二类和第三类的应用场景最多,也是最重要的两类。第一类,第四类,和第五类较少,其中第一类最简单,第四类和第五类最难,遇到这类,博主也没啥好建议,多多练习吧~
若是有写的有遗漏或者错误的地方,请你们踊跃留言啊,共同进步哈~