二分查找
二分查找是将待查找的区间折半,并只取一部分继续进行后续的查找,这样可以大大降低查找的时间复杂度,比如对于一个长度为n的数组,使用二分查找的时间复杂度为O(logn)。
二分查找在操作上很简单,只需要记录区间的开始和结束,然后每次计算区间的中点,难点在于折半的选择,即应该按照什么标准来判断中间位置的值,然后更新下一次查找的区间。
左闭右开和左开右闭
二分查找的区间记录可以使用左闭右开和左闭右闭两种方式(左闭右开更符合C++语言的习惯,左闭右闭更方便处理边界条件),对于大多数的二分查找,这两种方式都是可行的。
不同场景下可能一种方式比另一种方式更简洁,可以通过考虑折半的判断条件和区间只剩下1或2个值时,当前的方式是否会导致无法跳出等情况来具体选择使用哪种方式。如果一种方式确实会导致死循环或者判断条件十分别扭,那么不妨考虑使用另一种方式。
求开方
参考LeetCode第69题。
class Solution {
public:
int mySqrt(int x) {
if(x < 2) {
return x;
}
int l = 1;
int r = x / 2 + 1;
while(l < r) {
int mid = l + (r - l) / 2;
if(mid < x / mid) {
l = mid + 1;
}
else if(mid > x / mid) {
r = mid;
}
else {
return mid;
}
}
return r - 1;
}
};
思路:
实际上就是在[1, x/2]这段连续递增区间内查找能满足开方条件的数。
每次找到区间的中点,判断中点值的平方和x的关系,决定继续查找哪一半的区间。
当中点值大于x时,肯定查找前半段,当中点值小于x时,就继续查找后半段。
这里使用了左闭右开的方式,所以判断条件是l < r,当x没有完美匹配的值时。退出状态应该是[target + 1, target + 1),所以返回r - 1。如果使用左闭右闭的方式可能更清晰一点。
查找区间
参考LeetCode第34题,查找增序数组中给定值第一次出现和最后一次出现的位置。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int l = 0;
int r = nums.size();
if(nums.empty() || nums[l] > target || nums[r - 1] < target) {
return {-1, -1};
}
// 先找左侧
while(l < r) {
int mid = l + (r - l) / 2;
if(nums[mid] < target) {
l = mid + 1;
}
else {
r = mid;
}
}
int left = l;
// 再找右侧
r = nums.size();
while(l < r) {
int mid = l + (r - l) / 2;
if(nums[mid] > target) {
r = mid;
}
else {
l = mid + 1;
}
}
int right = l - 1;
return nums[left] > target || nums[right] < target ? std::vector<int>(2, -1) :
std::vector<int>{left, right};
}
};
思路:
困难的点在于,当折半查找到这个值时,无法判断当前位置是第一次出现还是最后一次出现。
那么不妨换个思路,一次二分不行,就两次二分,第一次负责找到大于等于target的左边界,第二次负责找到小于等于target的右边界,瞬间就豁然开朗了。
查找峰值
参考LeetCode第162题,在给定数组中找到比两边值都大的数。
class Solution {
public:
int findPeakElement(vector<int>& nums) {
if(nums.size() == 1) {
return 0;
}
int r = nums.size() - 1;
if(nums[0] > nums[1]) {
return 0;
}
else if(nums[r] > nums[r - 1]) {
return r;
}
int l = 1;
r -= 1;
int res = 0;
while(l <= r) {
int mid = l + (r - l) / 2;
if(nums[mid] > nums[mid - 1] && nums[mid] > nums[mid + 1]) {
res = mid;
break;
}
else if(nums[mid] < nums[mid - 1]) {
r = mid - 1;
}
else if(nums[mid] < nums[mid + 1]) {
l = mid + 1;
}
}
return res;
}
};
思路:
在我固有的认知中,以为只有有序的区间才可以使用二分查找,这是不对的。能否进行二分的关键应该是通过中间值的处理是否能够锁定或者排除掉一半的区间。
在这个题中,数组并非单调的,但分析可以知道,当中点不是峰值时,那么左右两侧一定会有峰值,所以仍然可以进行二分。
旋转数组查找
参考LeetCode第81题,查找给定的目标值是否在旋转过的数组中。
class Solution {
public:
bool search(vector<int>& nums, int target) {
int l = 0;
int r = nums.size() - 1;
while(l <= r) {
int mid = l + (r - l) / 2;
if(nums[mid] == target) {
return true;
}
// 无法判断是否是增区间,但是l值一定不是目标值
if(nums[mid] == nums[l]) {
l++;
}
// 无法判断是否是增区间,但是r值一定不是目标值
else if(nums[mid] == nums[r]) {
r--;
}
// 右侧是增区间
else if(nums[mid] < nums[r]) {
// 在增区间中
if(nums[mid] < target && target <= nums[r]) {
l = mid + 1;
}
// 在另一侧
else {
r = mid - 1;
}
}
// 左侧是增区间
else if(nums[mid] > nums[l]) {
// 在区间中
if(nums[mid] > target && target >= nums[l]) {
r = mid - 1;
}
// 不在区间中
else {
l = mid + 1;
}
}
}
return false;
}
};
思路:
无法判断有时也是一种判断,比如该题中,中间值和左边界值相等或和右边界值相等,虽然不能折半,但至少可以缩小边界。
中间值和边界值不相等时,可以判断目标值是否在递增区间中,不在则说明在另一侧。

浙公网安备 33010602011771号