虽然对于我们每个人来说二分查找都是耳熟能详的存在,但在实际刷题和开发过程中还是经常性地会写错二分查找(多为出现死循环)。故在此整理一下有关二分查找的常用模板。
标准二分查找
标准二分查找会找到非降序数组中的一个与目标值target
相等的元素的位置下标,若未找到则返回-1
。
为了确保二分查找算法的正确性,我们必须在每一次算法迭代缩小查找区间的过程中,保证每一次缩小原区间而得到的新的待查找区间都符合相同的特征。常见的区间特征有左闭右闭区间([i, j]
)和左闭右开区间([i, j)
)两种。
[i, j]区间式
在采用左闭右闭区间的二分查找实现中,我们认为区间最右侧j
指针所指的元素也是被算在查找范围之内的,因此循环条件为i <= j
,且区间向左缩小时的表达式为j = mid - 1
(mid
所指的元素被排除,但mid-1
所指的元素仍然保留在待查找区间内)。
int search(vector<int>& ar, int target) {
int i = 0;
int j = ar.size() - 1;
while (i <= j) {
int mid = i + ((j - i) >> 1);
if (ar[mid] == target) {
return mid;
}
else if (ar[mid] < target) {
i = mid + 1;
}
else {
j = mid - 1;
}
}
return -1;
}
在有些资料中,会将计算中点下标的表达式写作
mid = i + ((j - i) >> 1)
,而非mid = (i + j) >> 1
。这是为了防止在计算i + j
出现溢出。很容易证明,这两个表达式在数学上是等价的。
[i, j)区间式
与前者类似,在左闭右开区间的二分查找实现中,待查找区间并不包括指针j
所指元素,因此循环条件和待查找区间向左收缩的表达式也要作相应的修改。
int search(vector<int>& ar, int target) {
int i = 0;
int j = ar.size();
while (i < j) {
int mid = i + ((j - i) >> 1);
if (ar[mid] == target) {
return mid;
}
else if (ar[mid] < target) {
i = mid + 1;
}
else {
j = mid;
}
}
return -1;
}
虽然在这种实现中
j
所指元素并不包括在待查找区间内,但我们却不用修改计算mid
的表达式。这是因为只有某次进入循环时恰有i
、j
两者相等,才有mid=j
这个不安全的结果,但根据这个实现的循环条件i < j
可知,这种情况是不可能发生的。
变式
以一个具体的数组nums=[1, 3, 5, 7, 7, 7, 8, 9]
和目标值target=7
为例,我们可能碰到针对二分查找综合运用的如下几种变式:
变式1:寻找最后一个小于target的元素的下标
在本例中正确答案应为2.
int search(vector<int>& ar, int target) {
int i = 0;
int j = ar.size() - 1;
while (i <= j) {
int mid = i + ((j - i) >> 1);
if (ar[mid] < target) {
i = mid + 1;
}
// ar[mid] ≥ target
// 如果ar[mid]=target,则继续将区间向左缩小
else {
j = mid - 1;
}
}
// 循环退出时的情形:
// [1, 3, 5, 7, 7, 7, 8, 9]
// ↑ ↑
// j i
return j;
}
试一试:如果将target值由7修改为1,会得到什么结果?这个结果说明了什么?
变式2:寻找第一个大于target的元素的下标
在本例中正确答案应为6.
int search(vector<int>& ar, int target) {
int i = 0;
int j = ar.size() - 1;
while (i <= j) {
int mid = i + ((j - i) >> 1);
// 如果ar[mid]=target,则继续将区间向右缩小
if (ar[mid] <= target) {
i = mid + 1;
}
// ar[mid] < target
else {
j = mid - 1;
}
}
// 循环退出时的情形:
// [1, 3, 5, 7, 7, 7, 8, 9]
// ↑ ↑
// j i
return i;
}
变式3:寻找第一个≥target的元素的下标
这个变式等价于寻找数组中第一个值为target的元素的下标。
在本例中正确答案应为3.
int search(vector<int>& ar, int target) {
int i = 0;
int j = ar.size() - 1;
while (i <= j) {
int mid = i + ((j - i) >> 1);
if (ar[mid] < target) {
i = mid + 1;
}
// ar[mid] ≥ target
// 如果ar[mid]=target,则继续将区间向左缩小
else {
j = mid - 1;
}
}
// 循环退出时的情形:
// [1, 3, 5, 7, 7, 7, 8, 9]
// ↑ ↑
// j i
return i;
}
变式4:寻找最后一个≤target的元素的下标
这个变式等价于寻找数组中最后一个值为target的元素的下标。
在本例中正确答案应为5.
int search(vector<int>& ar, int target) {
int i = 0;
int j = ar.size() - 1;
while (i <= j) {
int mid = i + ((j - i) >> 1);
// 如果ar[mid]=target,则继续将区间向右缩小
if (ar[mid] <= target) {
i = mid + 1;
}
// ar[mid] < target
else {
j = mid - 1;
}
}
// 循环退出时的情形:
// [1, 3, 5, 7, 7, 7, 8, 9]
// ↑ ↑
// j i
return j;
}