先看看leetcode两道相关的题目,都是二分思想的应用。
leetcode 35 https://leetcode-cn.com/problems/search-insert-position/
leetcode 34 https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/
二分思想用一句话概况就是“一看就懂,一写就废”,花了很长时间都没有弄得懂,每个人都是不同的写法,新手很难从中发现规律。
其实二分查找最重要的一句话就是“查找范围”,如何确定查找范围?只要确定了查找范围,二分查找其实都是一个套路。而二分查找大致分为以下三种:
①普通二分,也就是给定一个排序数组nums和目标值target,在nums中查找target,如果找到就返回下标。写法也非常简单:
int left = 0;
int right = nums.size() - 1;
while(left <= right){ //查找范围[left, right]
int mid = left + (right - left) / 2;
if(nums[mid] < target){
left = mid + 1; //查找范围缩小为[mid+1, right]
} else if(nums[mid] > target){
right = mid - 1; //查找范围缩小为[left, mid-1];
} else if(nums[mid] == target){
return mid;
}
}
return -1;
初学建议不要写else,一步一步else if分析会更容易理解其中的“查找范围”如何思考。
下面两种是二分查找的变种,比基本二分要难一点。由于需要确定边界,所以不能一找到nums[mid] == target就返回,而应该继续缩小查找范围,不断逼近边界。
②确定左侧边界的二分查找
需求:给定一个排序数组nums和一个目标值target,在nums中找到target出现的最左侧位置,返回该下标。先给出代码再作解释:
int left_bound(vector<int> nums, int target){
if(nums.size() <= 0){
return -1;
}
int left = 0;
int right = nums.size();
while(left < right){ //查找范围为[left, right)
int mid = left + (right - left) / 2;
if(nums[mid] > target){
right = mid; //查找范围缩小为[left, mid)
} else if(nums[mid] < target){
left = mid + 1; //查找范围缩小为[mid + 1, right)
} else if(nums[mid] == target){
right = mid; //注意这里!right往左靠, 查找范围为[left, mid)
}
}
if(left == nums.size() || nums[left] != target){
return -1; //target比nums所有数都大,那么就找不到了
}
return left; //否则此时left就是左边界
}
注意到上面的代码,首先确定查找范围!这篇文章我全部采用左闭右开区间的查找范围,当然也可以左闭右闭。
而right一开始为什么是nums.size()而不是nums.size() - 1呢?这是因为查找范围为[left, right)左闭右开,为了确保所有的数都能被用到,所以right = nums.size(); 这样就可以取到nums[nums.size() - 1],也即最后一个数了。
好了,上面说完查找范围的确定,那确定左边界就是不断往左逼近了。简单地说,就是区间尽量往左靠,不断往左边缩小区间,具体的体现就是当 nums[mid] == target的时候,并不是直接返回,而是把right = mid; 把查找区间往左边缩小为[left, mid),因为mid已经被我们用过了(也就是已经和target比较过了),而右区间又是开区间,所以right = mid而非mid - 1!
那为什么left = mid + 1呢?这是因为我们左区间是闭区间,当mid被用过之后,下一个区间就是mid + 1开始了。
到了这里,相信大家对思路已经明白了,最后就是循环退出的时候,进行异常情况的判断,循环退出的时候,left == right。
此时,有两种情况:1. 找到了左边界。 2.找不到左边界。
找不到左边界是什么情况呢?要么left = nums.size()超出数组下标(target比所有nums的数都大),要么nums[left] != target,至此结束。
③确定右侧边界的二分查找
需求:给定一个排序数组nums和一个目标值target,在nums中找到target出现的最右侧位置,返回该下标。
还是先给出代码:
int right_bound(vector<int> nums, int target){
if(nums.size() <= 0){
return -1;
}
int left = 0;
int right = nums.size();
while(left < right){ //查找范围为[left, right)
int mid = left + (right - left) / 2;
if(nums[mid] > target){
right = mid; //查找范围缩小为[left, mid)
} else if(nums[mid] < target){
left = mid + 1; //查找范围缩小为[mid + 1, right)
} else if(nums[mid] == target){
left = mid + 1; //注意这里!left往右靠, 查找范围为[mid + 1, right)
}
}
return right - 1; //注意这里的返回值
}
分析思路同上:既然要找最右边界,那么left就要不断往右靠,查找范围同样是[left, right),相信大家已经可以自己分析出来了。
那么最终的返回值是什么呢?我们知道,循环退出的时候left ==rigiht,由于有可能出现在最后一个数,所以我们返回right - 1(left - 1也可以)。这里是一个小细节,因为上面找到target并缩小区间时,是这样写的
else if(nums[mid] == target){
left = mid + 1; //注意这里!left往右靠, 查找范围为[mid + 1, right)
}
所以真正的mid = left - 1(mid就是我们找到的目标值下标),这也是上面返回left的原因。
到此,大家可以去试一试上面两道题,采用这思路解决,会在O(logn)复杂度解决。下面是我写的代码:
34. 在排序数组中查找元素的第一个和最后一个位置
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.size() <= 0){
return vector<int>{-1, -1};
}
vector<int> res(2, -1);
int left = 0;
int right = nums.size();
while(left < right){ //搜索区间[left, right)
//寻找左边界, right左靠
int mid = left + (right - left) / 2;
if(nums[mid] > target){
right = mid;
} else if(nums[mid] < target){
left = mid + 1; //mid已经被搜索过了, 下一个[mid + 1, right)
} else if(nums[mid] == target){
right = mid; //right左靠
}
}
//异常情况判断: 比数组所有数都大 / 不存在target
if(left == nums.size() || nums[left] != target){
return res;
}
res[0] = left; //确定左边界
left = 0;
right = nums.size();
while(left < right){ //搜索区间[left, right)
//寻找右边界, left左靠
int mid = left + (right - left) / 2;
if(nums[mid] > target){
right = mid; //mid已经被搜索过了, 右开区间, 所以 [left, mid)
} else if(nums[mid] < target){
left = mid + 1; //mid已经被搜索过了, 下一个[mid + 1, right)
} else{
left = mid + 1; //left右靠
}
}
if(nums[left - 1] != target){
return res;
}
res[1] = left - 1; //注意, 因为搜索到mid的时候, left = mid + 1
return res;
}
};
35. 搜索插入位置
class Solution {
public:
int searchInsert(vector<int>& nums, int target)
{
if(nums.size() <= 0){
return 0;
}
int left = 0;
int right = nums.size();
while(left < right){ //搜索区间[left, right)
int mid = left + (right - left) / 2;
if(nums[mid] < target){
left = mid + 1;
} else if(nums[mid] > target){
right = mid;
} else{
return mid;
}
}
return left;
}
};