二分查找的一般模板如下所示,首先边界取的是0和n-1,左边界和右边界都可以取值,所以while的条件语句是left小于等于right。这个一般模板适用的是无重复的有序数组。
int left = 0;
int n = nums.size();
int right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2; //找中间值
if (nums[mid] > target)
right = mid - 1;
else if (nums[mid] < target)
left = mid + 1;
else
return mid;
}
return -1;
像第34题这种,找到target的开始位置和结束位置,需要对二分查找做出特殊的修改。
首先我们要定义两个函数,一个查找开始位置,一个查找结束位置。
查找开始位置函数如下所示:
int findFirst(vector<int>& nums, int target) {
int left = 0;
int n = nums.size();
int right = n - 1;
while (left < right) {
int mid = left + (right - left) / 2; //找中间值
if (nums[mid] > target)
right = mid - 1;
else if (nums[mid] < target)
left = mid + 1;
else
right = mid;
}
if (nums[left] == target)
return left;
else
return -1;
}
查找开始位置的二分查找和普通的模板区别在于:
- while判断条件变了,符合条件
left < right
的继续进行下一轮循环,从while挑出来的时候一定是不符合条件,left一定是等于right。 - 当找到nums[mid]等于target的时候,可以肯定的是在mid右边的编号不是我们想要的开始位置,但是mid也不一定是开始位置,可能左边还有等于target的数,所以需要继续往左边找。如果mid左边并不存在其他等于taget的值,那mid就是开始位置,所以改变边界的时候,不能把mid排除在外,
right = mid
。下面我们看看这一点小小的改变是如何帮助我们找到开始位置的。
考虑下面的情况,target = 8,此时nums[mid] = target
,下一步就应该在left和mid之间寻找开始位置。
计算得到新的mid,nums[1] < target
,left指针一直往右走,直到指向nums[2]的位置,此时nums[mid] == target
, 即符合第三个if语句,又所以right等于mid, 向左移动,此时left == right
跳出while循环,就找到了taget第一次出现的位置。
考虑另外一种情况,mid已经是target的开始位置,前面已经没有其他等于target的数,计算新的mid,并一直把left指针往右移动。当处于如下图的情况时时,发现nums[mid] < target
,left指针继续右移,此时left == right
且都指向taget,跳出while循环。
当我们跳出while循环之后,要对left或right所指向的数进行判断(此时left和right相等),因为有可能这个数组根本不存在target。如果最终left指针指向的数等于target,不管是返回left,还是返回right,都是可以的。如果这个数不等于target,那说明数组内根本不存在等于target的数,返回-1;
if (nums[left] == target)
return left;
else
return -1;
以下是寻找结束位置的函数:
int findEnd(vector<int>& nums, int target) {
int left = 0;
int n = nums.size();
int right = n - 1;
while (left < right) {
int mid = left + (right - left + 1) / 2; //找中间值
if (nums[mid] > target)
right = mid - 1;
else if (nums[mid] < target)
left = mid + 1;
else
left = mid;
}
return left;
}
寻找结束位置和寻找开始位置最大的不同,在于mid的取值不一样。这是很重要的一点,如果这个没有修改过来,就会陷入死循环。
这个mid取值加不加1,其实对于数量为奇数的数组,是没有影响的。比如对于n = 3的数组,如果没有加1,left = 0, right = 2, mid = 0 + (2 - 0) / 2 = 1
,此时mid计算出来是1,刚好是中间那个编号。 接下来看看加1的做法,left = 0, right = 2, mid = 0 + (2 - 0 + 1) / 2 = 1
,会发现后面的3 / 2 = 1.5被隐式转换成整数1,最终计算出来的结果还是1。
接下来看看数量为偶数的数组,对于n = 4的数组,中间其实是有两个编号1和2的。如果没有加1,left = 0, right = 3, mid = 0 + (3- 0) / 2 = 1
,此时mid计算出来是1,是中间两个取左值。 接下来看看加1的做法,left = 0, right = 3, mid = 0 + (3 - 0 + 1) / 2 = 2
,此时mid计算出来是2,是中间两个取右值。
所以int mid = left + (right - left + 1) / 2
这里加1是为了偶数数量的数组在取mid的时候,取到中间数组偏右的那个,那为什么要这么取呢?前面寻找开始位置的时候,明明取的是左值。
考虑下面这种情况,此时nums[mid] == target
,我们是想继续看看右边的数字的,我们令left = mid
,然后继续计算[left, right]的中间值,然后算出来mid取左值,即等于left(又是原来那个数),然后对nums[mid]进行判断,发现相等,令left = mid
,继续计算mid,此时就会陷入无限循环。
所以我们要让int mid = left + (right - left + 1) / 2
取到右值,即下图的情况。此时nums[mid] = target
,所以令left = mid
,此时left和right相等,退出while循环。
总结:只要二分查找类型的算法出现left = mid
的时候,记得取mid取中间的右值。
完整代码:
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int n = nums.size();
if (n == 0) return {-1, -1};
int start = findFirst(nums, target);
//如果返回-1,证明没找到target,那也不用继续找结束位置了
if (start == -1) return {-1, -1};
int end = findEnd(nums, target);
return {start, end};
}
int findFirst(vector<int>& nums, int target) {
int left = 0;
int n = nums.size();
int right = n - 1;
while (left < right) {
int mid = left + (right - left) / 2; //找中间值
if (nums[mid] > target)
right = mid - 1;
else if (nums[mid] < target)
left = mid + 1;
else
right = mid;
}
if (nums[left] == target)
return left;
else
return -1;
}
int findEnd(vector<int>& nums, int target) {
int left = 0;
int n = nums.size();
int right = n - 1;
while (left < right) {
int mid = left + (right - left + 1) / 2; //找中间值
if (nums[mid] > target)
right = mid - 1;
else if (nums[mid] < target)
left = mid + 1;
else
left = mid;
}
return left;
}
};