二分查找也常被称为二分法或者折半查找,每次查找时通过将待查找区间分成两部分并只取一部分继续查找,将查找的复杂度大大减少。对于一个长度为 O(n) 的数组,二分查找的时间复杂度为 O(log n)。
(1)69. Sqrt(x) (Easy)
题目描述
给定一个非负整数,求它的开方,向下取整。
个人思路
由于只需要取整数,所以用二分查找挺好解决的。但是如果是浮点数的话,可以想到以前所学的牛顿迭代法。因此,此题可以有两种方法解答。
二分查找
int mySqrt(int a) {
if (a == 0) {
return a;
}
int l = 1, r = a, mid, sqrt;
while (l <= r) {
mid = l + (r - l) / 2;
sqrt = a / mid;
if (sqrt == mid) {
return mid;
} else if (mid > sqrt) {
r = mid - 1;
} else {
l = mid + 1;
}
}
return r;
}
牛顿迭代法
int mySqrt(int a) {
long x = a;
while (x * x > a) {
x = (x + a / x) / 2;
}
return x;
}
(2)34. Find First and Last Position of Element in Sorted Array (Medium)
题目描述
给定一个增序的整数数组和一个值,查找该值第一次和最后一次出现的位置。
个人思路
最容易想到的思路就是先二分找左边界,然后二分找右边界。与常规二分不同的是,这里的找到target后应该继续持续更新,并改变当前查找的区间范围,以保证找到真正的边界值。在对区间范围进行更新时,需要仔细考虑区间变化的条件和变化值,否则容易陷入死循环。
代码展示
vector<int> searchRange(vector<int>& nums, int target) {
int min=-1,max=-1;
int left=0,right=nums.size()-1;
int mid;
while(left<=right){
mid=(left+right)/2;
if(nums[mid]==target){
min=mid;
right=mid-1;
}
else if(nums[mid]<target){
left=mid+1;
}
else if(nums[mid]>target){
right=mid-1;
}
}
left=0,right=nums.size()-1;
while(left<=right){
mid=(left+right)/2;
if(nums[mid]==target){
max=mid;
left=mid+1;
if(max==nums.size()-1){
break;
}
}
else if(nums[mid]<target){
left=mid+1;
}
else if(nums[mid]>target){
right=mid-1;
}
}
return {min,max};
}
题解思路
// 辅函数:返回大于等于目标值的第一个位置
int lower_bound(vector<int> &nums, int target) {
int l = 0, r = nums.size(), mid;
while (l < r) {
mid = (l + r) / 2;
if (nums[mid] >= target) {
r = mid;
} else {
l = mid + 1;
}
}
return l;
}
// 辅函数:返回大于目标值的第一个位置
int upper_bound(vector<int> &nums, int target) {
int l = 0, r = nums.size(), mid;
while (l < r) {
mid = (l + r) / 2;
if (nums[mid] > target) {
r = mid;
} else {
l = mid + 1;
}
}
return l;
}
// 主函数
vector<int> searchRange(vector<int>& nums, int target) {
if (nums.empty()) {
return vector<int>{-1, -1};
}
int lower = lower_bound(nums, target);
int upper = upper_bound(nums, target) - 1; // 这里需要减1位
if (lower == nums.size() || nums[lower] != target) {
return vector<int>{-1, -1};
}
return vector<int>{lower, upper};
}
(3)81. Search in Rotated Sorted Array II (Medium)
题目描述
一个原本增序的数组被首尾相连后按某个位置断开(如 [1,2,2,3,4,5] → [2,3,4,5,1,2],在第一位和第二位断开),我们称其为旋转数组。给定一个值,判断这个值是否存在于这个为旋转数组中。
个人思路
最开始想的是先找到k值然后再接成数组查找。本来想能不能接成有序数组,但没想出来应该怎么查找,还是看题解吧。
看完题解再做一遍的时候对于一些边界条件的把握还是不太行,做题的时候还是得多思考,先好好思考后下笔,今天有点急了。
题解思路
即使数组被旋转过,我们仍然可以利用这个数组的递增性,使用二分查找。对于当前的中点,如果它指向的值小于等于右端,那么说明右区间是排好序的;反之,那么说明左区间是排好序的。如果目标值位于排好序的区间内,我们可以对这个区间继续二分查找;反之,我们对于另一半区 间继续二分查找。
注意,因为数组存在重复数字,如果中点和左端的数字相同,我们并不能确定是左区间全部相同,还是右区间完全相同。在这种情况下,我们可以简单地将左端点右移一位,然后继续进行二分查找。
代码展示
bool search(vector<int>& nums, int target) {
int left=0,right=nums.size()-1;
int mid;
while(left<=right){
mid=(left+right)/2;
if(nums[mid]==target)
return true;
if(nums[left]==nums[mid])
left++;
else if(nums[mid]<=nums[right]){
if(target>nums[mid]&&target<=nums[right])
left=mid+1;
else
right=mid-1;
}
else {
if(target>=nums[left]&&target<nums[mid])
right=mid-1;
else
left=mid+1;
}
}
return false;
}
总结
本来以为二分查找会比较简单,但是在真正上手做的时候发现对于边界条件和特殊情况的处理还是要谨慎思考,不然很容易就出现死循环、段错误和答案错误等一系列错误情况。