如果看到在某个查找问题时间要求logn,那基本是考二分查找。平衡二叉树的本质也是二分查找,写树还费时。
二分查找的前提是数组有序,如果无序需要先排序。
二分查找框架:
int binarySearch(int[] nums, int target) {
int left = 0, right = ...;
while(...) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = ...
} else if (nums[mid] > target) {
right = ...
}
}
return ...;
}
要注意的点:
1. mid=left+(right-left)/2 相比 mid=(left+right)/2,结果相同,前者可以防止溢出。
2. while()的判断条件。
3. else if 要写全,不要只写else,容易出错。
4. else if内的right、left的更新。
5. 退出条件。
基础版二分查找:
int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 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;
}
1. while() 的判断条件。left<=right。因为right初始化为nums.length-1,相当于[left, right];如果初始化为nums.length,那就相当于[left, right),只能取<,否则越界。
2. 退出条件, 查找成功:if(nums[mid] == target) return mid; 查找失败:while结束后return -1.
3. 左右区间的更新, left=mid+1,right=mid-1。
这个算法有个缺陷,如果被查找数在数组中出现多次,只能返回其中一个的下标。比如[1,2,2,2,3],返回索引2.
如果想查找左侧区间:
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
// 搜索区间为 [left, right]
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
// 搜索区间变为 [mid+1, right]
left = mid + 1;
} else if (nums[mid] > target) {
// 搜索区间变为 [left, mid-1]
right = mid - 1;
} else if (nums[mid] == target) {
// 收缩右侧边界
right = mid - 1;
}
}
// 检查出界情况
if (left >= nums.length || nums[left] != target)
return -1;
return left;
}
如果想查找右侧边界:
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 这里改成收缩左侧边界即可
left = mid + 1;
}
}
// 这里改为检查 right 越界的情况,见下图
if (right < 0 || nums[right] != target)
return -1;
return right;
}
这里会出现越界的情况,当target比所有元素都小时,会出现越界:
这题需要同时查找左右边界:在排序数组中查找元素的第一个和最后一个位置
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
// if(nums.size()==0)return [-1,-1];
vector<int> res;
res.push_back(-1);
res.push_back(-1);
if(nums.size()==0)
return res;
int left=0;
int right=nums.size()-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 if(nums[mid]> target) right=mid-1;
}
if(left<nums.size()&&nums[left]==target) res[0]=left;
left=0;
right=nums.size()-1;
while(left<=right){
int mid=left+(right-left)/2;
if(nums[mid]==target) left=mid+1;
else if(nums[mid]<target) left=mid+1;
else if(nums[mid]>target) right=mid-1;
}
if(right>=0&&nums[right]==target) res[1]=right;
return res;
}
};
这题要求在一个旋转过的数组中查找target。
思路是:由于二分查找的使用条件一定是有序,那么旋转数组一定有一半是有序的,另一半存在一个旋转点。如果中间的数小于最右边的数,则右半段是有序的,若中间数大于最右边数,则左半段是有序的,我们只要在有序的半段里用首尾两个数组来判断目标值是否在这一区域内,这样就可以确定保留哪半边了。
class Solution {
public:
int search(vector<int>& nums, int target) {
int left=0, right=nums.size()-1;
while(left<=right){
int mid=left+(right-left)/2;
if(nums[mid]==target){
return mid;
}
else if(nums[mid]<nums[right]){
if(nums[mid]<target&&target<=nums[right])
left=mid+1;
else
right=mid-1;
}
else{
if(nums[left]<=target&&target<=nums[mid])
right=mid-1;
else
left=mid+1;
}
}
return -1;
}
};