导语:二分查找是对一个有序数组的搜索算法,由于每次都是折半搜索,所以时间复杂度未O(LogN)
,但是在编写二分查找时里面蕴含了一些细节,下面将给大家讲述一下如何利用二分查找找到目标值、第一个比目标值大的值和最后一个比目标值小的值。
一、判断是否包含目标值(简单二分查找)
比如现在给你一个有序的数组nums
,问你数组内是否包含目标值target
,这个可以套用最基础的模板
public boolean binary_search(int[] arr) {
int left = 0, right = arr.length - 1; // 细节1
while (left <= right) { // 细节2
int mid = (right - left) >> 2 + left;
if (arr[mid] == target) {
return true
} else if (arr[i] > target) {
right = mid - 1; // 细节3
} else if (arr[i] < target) {
left = mid + 1; // 细节3
}
}
return false; // 细节4
}
挡你刷的二分查找题目比较多,看的题解比较多的时候可能会有下面的疑问
- 到底是
right
的初始值是arr.length
还是arr.length - 1
? while
的条件到底是<
还是<=
?right
和left
什么时候需要加1减1?- 如果要返回坐标,应该返回啥
后面两个问题其实是第一个问题引出的,我们先解决第一个问题
right
是arr.length - 1
还是arr.length
,上面的例子是arr.length - 1
,取arr.length - 1
可以认为是在对区间[left, right]
进行搜索(两边闭合),如果让right
等于arr.length
,那么right
已经越界了,所以可以认为他是在对[(left, right)
进行搜索(左闭右开)注意
right
的取值会影响框架的其他位置while
中<
和<=
主要体现在终止条件的不同,如果是<
,他的终止条件是left == right
,如果是<=
,则是left + 1 == right
,不难发现<
不会处理left == right
的情况,如果你使用了<
,需要多考虑left==right
的情况如数组{1, 2, 3, 4, 5}, target = 5, 如果while修的是 <,返回的是false
如果硬是要用
<=
,其实我们已经知道他漏了判断left==right
的情况了,我们只要在最后补上就行return arr[left] == target;
最后就是
left
和right
加1减1的时了,需不需要加一减一取决于你的right
是怎么定义的- 如果是两边闭合的,当你检测完
mid
不是你想要的,下一步应该检查[left, mid - 1]
和[mid + 1, righjt]
呀, - 如果是做闭右开,当你检查完
mid
不是你想要的值,下一步应该检查[left, mid)
和[mid + 1, right)
呀,所以right
不用加1
- 如果是两边闭合的,当你检测完
所以二分查找怎么写是由right
的取值决定的
二、找到目标值坐标否则返回-1
有了上面等到基础,我们可以很快的写出代码
right = arr.length - 1
代码如下:
public int binarySearch(int[] nums, int target) { int left = 0; right = nums.length - 1; while (left <= right) { int mid = (left + (right - left) >> 1); if (nums[mid] == target) { return mid; } else if (nums[mid] < target) { left = mid + 1; } else { right = mid + 1; } return -1 } }
right = nums.length
代码如下
public int binarySearch(int[] nums, int target) { int left = 0, right = nums.length; while (left < right) { int mid = (left + (right - left) >> 1); if (nums[mid] == target) { return mid; } else if (nums[mid] < target) { left = mid + 1; } else { right = mid; } } return -1; }
这里还是比较容易理解的
三、找到第一次出现目标元素的下标
例如给定一个数组[11, 22, 33, 44, 44, 44, 55]
,若target
等于44,二分查找的结果应该为3,这个时候我们可以先用上面第二点的代码找到44,然后往左边一直找,我们也可以再二分查找模板上进行少量修改实现该功能
同样分成两种情况:
right = arr.length - 1
这时是对两边闭合的区间进行搜索,我们只需要改一下原来的模板就行了
public int leftBound(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left <= right){ int mid = left + ((right - left) >> 1); if (nums[mid] == target) { right = mid - 1; } else if(nums[mid] < target) { left = mid + 1; } else { right = mid - 1; } } return left; }
为什么最后返回的是
left
呢,大家可以分析一下:如果target
存在于数组中,最后一次循环时left
和right
都指向target
,循环结束后right
经过mid(left) - 1
,所以left
才是目标值,如果目标值不在数组里,最后一次循环left
和right
都会指向第一个比目标值大的数,right
被减去1后不再循环,最终返回的left
可以理解为数组中比目标值的数的个数,也可以认为是寻找大于等于目标值的数的索引,则个语义很重要如果题目要求找不到返回-1,那么就需要加上一些判断。根据上面的分析,
left
的取值范围应该是[0, len]
,因此我们需要判断left
等于len
(当target
大于这个数组所有的数,最后left = mid + 1
所导致),或者left = 0
但是target != nums[0]
(当target
小于所有数,最后left = 0
所导致),又或者目标值在数组范围里却不在数组中,那就是target != nums[left]
,所以我们徐娅加上两个特殊判断完整代码如下:
public int left_bound(int[] arr, int target) { int left = 0, right = arr.length - 1; while (left <= right) { int mid = ((right - left) >> 1) + left; if (arr[mid] == target) { right = mid - 1; } else if (arr[mid] > target) { right = mid - 1; } else { left = mid + 1; } } if (left >= arr.length || arr[left] != target) { return -1; } return left; }
right = arr.length
如果对返回值没有作要求,代码如下:
public int leftBound(int[] nums, int target) { int left = 0, right =nums.length; while (left < right) { int mid = left + ((right - left) >> 1); if (nums[mid] == target) { right = mid; } else if (nums[mid] < target) { left = mid + 1; } else { right = mid; } } return left; }
最后为什么返回
left
呢?如果目标值在数组中,最终left = right
且会指向第一个target
,如果目标值不在数组中,最后一次循环时left
和right
会指向第一个比目标值大的数,这其实和上面的情况相同,最终返回的left
可以理解为数组中比目标值的数的个数如果要返回-1呢?我们也要考虑
keft
的取值范围,应该是[0, len]
,所以当目标值比数组元素大时,left = right = len
,当目标值比所有制小的时候left = right = 0
,所以这次的判断和上面的一样public int left_bound(int[] arr, int target) { int left = 0, right = arr.length; while (left < right) { int mid = ((right - left) >> 1) + left; if (arr[mid] == target) { right = mid; } else if (arr[mid] > target) { right = mid; } else { left = mid + 1; } } if (left == arr.length) { return -1; } return arr[left] == target ? left : -1; }
大家要思考当目标值比数组所有值都大的情况left
和right
指向哪里,最后应该是走mid = left
,走arr[mid] < target
的分支,letf = mid + 1
,left
等于right
等于arr.length
四、找到最后一次出现目标元素的下标
同样的,我们来找一下最后一个目标元素
right = arr.length - 1
代码如下
public int rightBound(int[] nums, int target) { int left = 0, right =nums.length - 1; while (left <= right) { int mid = left + ((right - left) >> 1); if (nums[mid] == target) { left = mid + 1; } else if (nums[mid] < target) { left = mid + 1 ; } else { right = mid - 1; } } return left - 1 or right; }
大家分析一下,最后我们应该返回的是什么,应该是
left - 1
,如果目标值在数组里,最后一次遍历left = right = 最后一个数字
,然后left
被+1,所以是left - 1 or right
。如果目标值不在这个数组中,最后一次迭代left = right=第一个比目标值大的数的索引
,最后饭返回的是left - 1
,可以理解成比目标值小的最后一个元素的索引如果要返回 - 1,我们可以看看
right
的取值范围应该是[-1, len - 1]
,如果目标值太小,right
应该是-1,如果太大,right
应该是len
,所以完整代码为private int right_bound(int[] arr, int target) { int left = 0, right = arr.length - 1; while (left <= right) { int mid = ((right - left) >> 1) + left; if (arr[mid] == target) { left = mid + 1; } else if (arr[mid] < target) { left = mid + 1; } else { right = mid - 1; } } // left = right + 1, right指向的如果越界了 if (right < 0 || arr[right] != target) { return -1; } return right; }
right = arr.length
如果是这种情况,代码应该是
public int rightBound(int[] nums, int target) { int left = 0, right =nums.length; while (left < right) { int mid = left + ((right - left) >> 1); if (nums[mid] == target) { left = mid + 1; } else if (nums[mid] < target) { left = mid + 1 ; } else { right = mid; } } return left - 1 or right - 1; }
为什么是
left - 1
呢,想象一下现在mid
指向目标值,现在left = mid, right = mid + 1
,mid趋势是目标值,最后left = mid + 1
,所以目标值在left - 1
。如果目标值不在数组中,left = right = 第一个比目标值大的数
,所以返回的是最后一个比目标值小的数的索引如果你希望返回-1,
left
的取值范围是[0, len]
,如果目标值太大,这时候left应该是len·
,如果太小,left = right = 0
,综上,代码如下
private int right_bound(int[] arr, int target) {
int left = 0, right = arr.length;
while (left < right) {
int mid = ((right - left) >> 1) + left;
if (arr[mid] == target) {
left = mid + 1;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
if (left == 0) {
return -1;
}
return arr[left - 1] == target ? left - 1 : - 1;
}
五、实际应用
形如:
for i in range(num1):
for j in range(num2)
这种时间复杂度为O (N2)
的找最目标值问题,都可以用二分查找解决,其中寻找左边界的二分查找用的最多,下面是LeetCode中常见的题目:
- LeetCode 875 爱吃香蕉的CoCo,
- LeetCode 1011 在D天送达包裹的能力
- LeetCode 410 分割数组最大值
当然啦,二分查找可以和其他算法结合,例如高楼丢鸡蛋问题也是可以使用二分查找优化的,写完本文,我对自己的要求是掌握分析边界的方法以及学会寻找大于等于目标值的二分查找
参考
本文大量参考博主labuladong
的文章,平时仓看他的文章,写得很好,大家有兴趣可以去看看
- 我作了首诗,保你闭着眼睛也能写对二分查找,labuladong https://mp.weixin.qq.com/s/M1...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。