二分查找是计算机科学中最基本、最有用的算法之一,在基础算法的学习中是非常重要的。
二分查找的最基本问题是在有序数组里查找一个特定的元素(「力扣」第 704 题:二分查找)。
二分查找虽然思想很简单,但事实上很多时候编码【边界的细节处理是魔鬼】很容易出错。
通常情况下,只要给定的一个问题的答案解满足【二段性】那么我们就可以考虑是否可以二分。
二段性
- 例如集合中的元素有存在分界线,给定某一个条件可以将集合中元素分为两部分,一部分满足条件,一部分不满足条件。
- 或者一段时间内某一个时刻满足题目的解,不妨设为 t ,此时 大于等于 t 的都满足 小于等于 t 的都不满足条件
在二分查找中,也存在分界线,分界线以前的元素满足某个条件,分界线以后的元素不满足该条件。我们每一轮搜索就是要确定一个分界线。从而我们每次就可以利用【分冶的思想】减少搜索的区间范围
二分查找的时间复杂度为
举个例子 上面的二分查找某个target元素 先贴出AC代码
class Solution {public int search(int[] nums, int target) {// 定义搜索的下界 和上届int left = 0,right = nums.length -1;// 结束搜索的条件while(left < right){// 每次选择一个中间的数字int mid = (left + right) >>> 1;// 符合条件直接返回if(nums[mid] == target){return mid;}// 判断条件 这里就是二段性的表现,nums[mid] < target 那么意味着 [0,mid] 区间的数都小于targer// 可以确定下一轮搜索范围为 [mid+1,right]if(nums[mid] < target){left = mid + 1;}else{// 这里代表[mid,right] 整个区间数都大于 target 所以我们将下一轮搜索范围改为 [left,mid] 即 right = midright = mid;}}// 结束时有 left == right 成立// 视情况,是否需要单独判断 left(或者 right)这个下标的元素是否符合题意return nums[left] == target ? left:-1;}}
上面我们每次选择中间数字的时候采用的是 (left + right) >>> 1; 这样写是没问题的。但是如果直接写成(left+right)/ 2 是有问题的,因为这样可能溢出int最大值。还有一种写法是left+(right-left)/2。
二分模板
模板一
选择中位数的时候下取整
public int search(int[] nums, int left, int right, int target) {while (left < right) {// 选择中位数时下取整int mid = left + (right - left) / 2;if (check(mid)) {// 下一轮搜索区间是 [mid + 1, right]left = mid + 1} else {// 下一轮搜索区间是 [left, mid]right = mid}}// 退出循环的时候,程序只剩下一个元素没有看到。// 视情况,是否需要单独判断 left(或者 right)这个下标的元素是否符合题意}
选择中位数的时候上取整
public int search(int[] nums, int left, int right, int target) {while (left < right) {// 选择中位数时上取整int mid = left + (right - left + 1) / 2;if (check(mid)) {// 下一轮搜索区间是 [left, mid - 1]right = mid - 1;} else {// 下一轮搜索区间是 [mid, right]left = mid;}}// 退出循环的时候,程序只剩下一个元素没有看到。// 视情况,是否需要单独判断 left(或者 right)这个下标的元素是否符合题意}
模板二
class Solution {public int search(int[] nums, int target) {// 特殊用例判断int len = nums.length;if (len == 0) {return -1;}// 在 [left, right] 区间里查找 targetint left = 0;int right = len - 1;while (left <= right) {// 为了防止 left + right 整形溢出,写成如下形式int mid = left + (right - left) / 2;if (nums[mid] == target) {return mid;} else if (nums[mid] > target) {// 下一轮搜索区间:[left, mid - 1]right = mid - 1;} else {// 此时:nums[mid] < target// 下一轮搜索区间:[mid + 1, right]left = mid + 1;}}return -1;}}
注意事项
二分的模板非常多,不同的模板写法退出循环的边界条件,以及确定下一轮二分搜索的范围处理方式都不一样。细节非常多。
- 结束搜索的条件是
left < right还是left <= right - 确定中间数字的时候是上取整 还是 下取整
(left+right) >>> 1或者 上取整left + (right - left + 1) / 2 - 下一轮搜索的边界怎么确定。
left = mid + 1还是left = midright = mid还是right = mid-1 - 退出搜索后,
left是否等于right? 或者需不需要再对left和right做特殊判定
建议不要死背模板,要自己去理解,多去调试几个例子跑一下,先写出大体的框架,再去考虑是否需要上取整,以及确定边界。
