一位大佬曾经说过:二分查找思路很简单,细节是魔鬼
做了一些题后感慨真的是这样…
二分查找法:实质就是将一个有序的数据集不断地对半分割,直至找到目标值;其中两个最关键的也是在面对不同题目会稍有不同的就是怎么退出循环,怎么缩小查找空间。怎么突破这两点就是关键
在一个有序数据集里查找一个一个目标数可以有一下两种写法
int Search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid - 1;
}
return -1;
}
int Search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while(left < right) {
int mid = left + (right - left) / 2;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid - 1;
}
return nums[left] == target ? left : -1;
}
先来看第一个,它退出循环的条件是left <= right
,表示当left == right
成立的时候,还有一个元素,即 left(right)位置的元素还没有看到,需要继续查看这个元素的值,看看是不是我们想要的。第二个是退出循环的时候left == right
成立。此时left (right) 这个位置的值可能程序还没有读取到,因此“有可能”需要再对 left(right) 这个位置的值是否是目标元素的值做一次判断。这是考虑循环退出条件的思路,接下来看具体怎么缩小比较空间
对于思考怎么缩小比较空间我们就想什么时候不是解,当中间这个元素是大于target是那中间这个元素肯定也不是结果值,所以就left = mid + 1
;
上面代码是针对一旦找到目标值就返回的情况,那如果是要找目标值的左侧分界呢(有对个目标值),来屡一下分析思路;当数组里的元素都大于目标值是应该返回0,当有序数组的元素都小于目标值的时候左边界应该为n(数组元素个数),可以看出返回值的可能区间是[0, nums.size()]
,所以要设right的起始值是nums,size(),但因为不能访问nums[n],所以搜索空间为[left, right)
左闭右开,再接着想退出条件,就是[left, left)
,循环退出条件可写成while(left < right)
,因为找的是左边界所以在得到nums[mid] == target
这种情况时就要继续往左找
if (nums[mid] == target)
right = mid;
在区间[left, mid)
中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。所以完整代码就如下
int left_bound(vector<int>& nums, int target) {
if (nums.size() == 0) return -1;
int left = 0;
int right = nums.size();
while (left < right) {
int mid = (left + right) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else{
right = mid;
}
}
return left;
}
来总结下怎么推敲具体的细节
- 确定搜索区间初始化时候的左右边界,有时需要关注一下边界值。在初始化时,有时把搜索区间设置大一点没有关系,但是如果恰好把边界值排除在外,再怎么搜索都得不到结果
- 确定while里的条件,如果right其实是size的话当left==right的时候就应该立马退出,所以循环条件是while (left < right)
- 思考怎么缩小比较空间就想什么时候不是解
下面来列出几道题根据上面的思路来练练脑
1.计算并返回 x 的平方根,其中 x 是非负整数
class Solution {
public:
int mySqrt(int x) {
if(x == 0) return 0;
long long start = 1;
long long end = x/2;
while(start < end){
long long mid = (end+start+1)>>1;
long long res = mid*mid;
if(res > x)
end = mid-1;
else{
start = mid;
}
}
return start;
}
};
这个情况就相当于查找右边界
2.假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
注意数组中可能存在重复的元素
class Solution {
public:
int findMin(vector<int>& nums) {
int size = nums.size();
if(size == 0) return 0;
int left = 0;
int right = size-1;
while(left < right){
int mid = (left + right)/2;
if(nums[mid] == nums[right]) right--;
else if(nums[mid] > nums[right])
left = mid + 1;
else
right = mid;
}
return nums[right];
}
};
**
3.峰值元素是指其值大于左右相邻值的元素。给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。
**
class Solution {
public:
int findPeakElement(vector<int>& nums) {
int l = 0, r = nums.size() - 1;
while (l < r) {
int mid = (l + r) / 2;
if (nums[mid] <= nums[mid + 1])
l = mid + 1;
else
r = mid;
}
return l;
}
};
4.给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> ans;
ans.push_back(left_bound(nums, target));
ans.push_back(right_bound(nums, target));
return ans;
}
int left_bound(vector<int> &nums, int target) {
if (nums.size() == 0) return -1;
int i = 0, j = nums.size(), mid = -1;
while (i < j) {
//左半区间[i, mid) 右半区间[mid+1, j)
mid = i + (j - i) / 2; //中间位置
if (nums[mid] == target) {
j = mid;
}
else if (nums[mid] > target) { //左半区间 [i, mid)
j = mid;
}
else if (nums[mid] < target) { //右半区间 [mid+1, j)
i = mid + 1;
}
}
if (i == nums.size()) return -1;
else return (nums[i] == target) ? i : -1;
}
int right_bound(vector<int> &nums, int target) {
if (nums.size() == 0) return -1;
int i = 0, j = nums.size(), mid = -1; //搜索区间是左闭右开的
while (i < j) {
mid = i + (j - i) / 2;
//左半区间 [i, mid) 右半区间 [mid+1, j)
if (nums[mid] == target) { //收缩下界
i = mid + 1;
}
else if (nums[mid] > target) { //左半区间
j = mid;
}
else if (nums[mid] < target) { //右半区间
i = mid + 1;
}
}
if (j == 0) return -1;
return nums[j - 1] == target ? (j - 1) : -1;
}
};