引言
二分查找是时间复杂度为O(logn)
的简单算法,适用于在有序的数组中查找想要的数据,这里的数组也可以换成具有单调性的函数,把函数的传入值类比成数组的下标,函数的返回值类比成数组内这个下标存储的元素,那么函数就是不用开辟额外空间存储元素的压缩数组。
普通二分查找
假设数组中数据的值是单调递增的,定义左边界left
和右边界right
,从中间下标mid =(left + right)/ 2
开始搜索目标值target
:
- 如果
arr[mid] == target
,返回其在数组中的下标mid
; - 如果
arr[mid] > target
,说明现在的数据大了,要减小数据,则要让right
变小,所以right = mid - 1
; - 如果
arr[mid] < target
,说明现在的数据小了,要增大数据,则要让left
变大,所以left = mid + 1
;
每次搜索可以丢弃当前边界内一半不符合的数据,进行下一次搜索前,先更新mid
,直到左右边界内没有数据可搜索了,或者找到目标数据,返回其在数组中的下标。
图解过程:
在数组arr = {1,2,3,4,5,6,7,8,9} 中,找到target = 6
代码实现如下:
int binary_search(int *arr, int left, int right, int target)
{
int mid = 0;
while (left <= right)
{
mid = (left + right) / 2;
if (arr[mid] == target)
{
return mid;
}
else if (arr[mid] > target)
{
right = mid - 1;
}
else if (arr[mid] < target)
{
left = mid + 1;
}
}
return -1;//没有找到数据,返回-1。
}
特殊二分查找
000111型
假设需要查找的目标target = 1
,一个数组内存的数据为arr = {0,0,0,1,1,1}
,要找到数组中第一个出现的target的下标,left = 0
,right = 5
,还是从mid = (left + right) / 2
开始搜索:
- 如果
arr[mid] = = target
,此时不知道找到的是第几个出现的1
,所以这个结果要保留,现在下一次搜索从mid
向前搜,right = mid
; - 如果
arr[mid] != target
,此时这个结果不需要保留,现在下一次搜索从mid + 1
向后搜,left = mid + 1
;
每次搜索可以丢弃当前边界内一半的数据,进行下一次搜索前,先更新mid
,直到左右边界重合,此时left
或right
即是要找的下标。
PS: 假设更极端的情况,数组为arr = {0,0,0,0,0,0}
,没有1
,即没有目标值。
二分查找搜索到最后,
left = right = 5
,但是这个下标并不是正确答案,所以此时二分查找结束后,应该做个判断,判断这个下标内的数据是不是target
;也可以在搜索刚开始时,让right = 6
,超过了数组上限1位,当作虚拟尾元素,二分查找结束后,判断返回的下标是否合法,即是不是6
,是6
则没有找到target
,不是6
则没找到target
。
图解过程:
在数组arr = {0,0,0,1,1,1,1,1,1} 中,找到第一个1的下标。
代码实现如下:
int binary_search_01(int *arr, int left, int right, int target)
{
int mid = 0;
while (left != right)
{
mid = (left + right) / 2;
if (arr[mid] == target)
{
right = mid;
}
else
{
left = mid + 1;
}
}
return left;
}
111000型
假设需要查找的目标target = 1
,一个数组内存的数据为arr = {1,1,1,0,0,0}
,要找到数组中最后一个出现的target的下标,left = 0
,right = 5
,从 mid = (left + right) / 2 开始搜索:
- 如果
arr[mid] = = target
,此时不知道找到的是第几个出现的1
,所以这个结果要保留,现在下一次搜索从mid
向后搜,left = mid
; - 如果
arr[mid] != target
,此时这个结果不需要保留,现在下一次搜索从mid + 1
向前搜,right = mid - 1
;
每次搜索可以丢弃当前边界内一半的数据,进行下一次搜索前,先更新mid
,直到左右边界重合,此时left
或right
即是要找的下标。
PS:mid = (left + right) / 2
真的合理吗?
假设数组为
arr = {1,0}
,left = 0
,right = 1
,mid = (0 + 1) / 2 = 0
,则
arr[mid] == 1
,left = mid = 0
,可以看到此时left并没有被更新,会导致死循环,正确的做法是mid = (left + right + 1) / 2
PS: 假设更极端的情况,数组为arr = {0,0,0,0,0,0}
,没有1
,即没有目标值。
用二分查找搜索到最后,
left = right = 0
,但是这个下标并不是正确答案,所以此时二分查找结束后,应该做个判断,判断这个下标内的数据是不是target
;也可以在搜索刚开始时,让left = -1
,超过了数组下限1位,当作虚拟头元素,二分查找结束后,判断返回的下标是否合法,即是不是-1
,是-1
则没有找到target
,不是-1
则没找到target
。
图解过程:
在数组arr = {1,1,1,1,1,1,0,0,0} 中,找到最后一个1的下标。
代码实现如下:
int binary_search_10(int *arr, int left, int right, int target)
{
int mid = 0;
while (left != right)
{
mid = (left + right + 1) / 2;
if (arr[mid] == target)
{
left = mid;
}
else
{
right = mid - 1;
}
}
return left;
}
特殊二分查找的应用——二分答案
二分答案,顾名思义就是在所有可能答案的区间内,用二分查找搜索出目标值。说的有些抽象,下面我以leetcode278题做例子讲解一下二分答案的思路。
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
示例:
给定 n = 5,并且 version = 4 是第一个错误的版本。 调用 isBadVersion(3) -> false 调用 isBadVersion(5) -> true 调用 isBadVersion(4) -> true 所以,4 是第一个错误的版本。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/first-bad-version
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
读题后得到的有,目前有n个版本产品,需要知道第一个出现错误的版本是哪一个。
上图是我根据示例中的数据所画出来的解题思路,将找第一个出现错误的版本,转换为找数组中第一个1的下标,在解题过程中isBadVersion()
函数就相当于一个数组来使用。将问题转换成特殊的二分查找后,就可以按照上面介绍的二分查找模板完成代码了,这题是属于000111型。
// The API isBadVersion is defined for you.
// bool isBadVersion(int version);
int firstBadVersion(int n) {
long left = 1, right = n;
long mid = 0;
while (left != right)
{
mid = (left + right) / 2;
if (isBadVersion(mid))
{
right = mid;
}
else
{
left = mid + 1;
}
}
return left;
/*
因为题目必定有答案,所以可以不判断能不能找到。
if (isBadVersion(left)) {
return left;
}
else
{
return -1;
}
*/
}