前言
二分查找算法是一个基本但用处十分广泛的算法,但要写出一个没有bug的二分查找算法也不容易,《编程珠玑》一书中提到仅有百分之十的人可以第一次就写出没有bug的二分查找算法,主要原因在于寻找中间区间时数据有可能溢出,以及区间的选择不正确导致死循环,数组越界等等。
具体到代码上,我认为需要考虑两个最主要的问题:
- 左闭右开,还是左闭右闭(熟练使用一种)
- 如果区间只剩下一个数或者两个数,自己的写法是否会陷入死循环
leetcode 69
class Solution {
public:
int mySqrt(int x) {
if(x==0) return 0;
int l=1,r=x,mid,sqrt;
while(l<=r){
mid=l+(r-l)/2;
sqrt=x/mid;
if(sqrt==mid) return mid;
else if(mid>sqrt) r=mid-1;
else l=mid+1;
}
return r;
}
};
while 循环的条件中为什么是 <=而不是<,因为采用了左闭右闭的写法。初始状态为[left, right],每一次维护完的状态为[mid+1,right]或者是[left,mid-1]。
考虑循环执行到只剩下一个数时,即是否有必要进行检查left==right的情况。
while 循环什么时候应该终止?搜索区间为空的时候应该终止,意味着你没得找了,就等于没找到嘛。while(left <= right) 的终止条件是 left == right + 1,写成区间的形式就是
[
r
i
g
h
t
+
1
,
r
i
g
h
t
]
\ [right + 1, right]
[right+1,right],或者带个具体的数字进去 [3, 2],可见这时候区间为空,因为没有数字既大于等于 3 又小于等于 2 的吧。所以这时候 while 循环终止是正确的,直接返回 -1 即可。
下面是左闭右开的写法,循环结束返回l=r,返回l-1是因为题目要求去除小数部分。
class Solution {
public:
int mySqrt(int x) {
if(x<=1) return x;
int l=1,r=x,mid,sqrt;
while(l<r){
mid=l+(r-l)/2;
sqrt=x/mid;
if(mid>sqrt) r=mid;
else l=mid+1;
}
return l-1;
}
};
leetcode 34
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.empty()) return vector<int> {-1,-1};
int l=low_bound(nums,target);
int r=high_bound(nums,target)-1;
if(l==nums.size()||nums[l]!=target){
return vector<int> {-1,-1};
}
return vector<int> {l,r};
}
int low_bound(vector<int> &nums,int target){
int l=0,r=nums.size(),mid;
while(l<r){
mid=l+(r-l)/2;
if(nums[mid]>=target){
r=mid;
}
else{
l=mid+1;
}
}
return l;
}
int high_bound(vector<int> &nums,int target){
int l=0,r=nums.size(),mid;
while(l<r){
mid=l+(r-l)/2;
if(nums[mid]>target){
r=mid;
}
else{
l=mid+1;
}
}
return l;
}
};
这道题可以看作是自己实现C++ 里的
l
o
w
e
r
_
b
o
u
n
d
lower\_bound
lower_bound 和
u
p
p
e
r
_
b
o
u
n
d
upper\_bound
upper_bound 函数。这里我们尝试使用左闭右开的写法 [left,right)。
在使用左闭右闭时,一般免不了多写一两个+1,-1,return,而且在最后处理返回时left,right只有一个是正确答案,极易出错。
而在左闭右开中,终止后返回left 和right是相同的。
if(l==nums.size()||nums[l]!=target){
return vector<int> {-1,-1};
}
这里考虑短路机制,需要先判断l的位置,防止其数组越界。
leetcode 81
class Solution {
public:
bool search(vector<int>& nums, int target) {
int l=0,r=nums.size()-1,mid;
while(l<=r){
mid=l+(r-l)/2;
if(nums[mid]==target){
return true;
}
if(nums[l]==nums[mid]){
++l;
}else if(nums[mid]<=nums[r]){
if(target>nums[mid]&&target<=nums[r]){
l=mid+1;
}else{
r=mid-1;
}
}else{
if(target>=nums[l]&&target<nums[mid]){
r=mid-1;
}else{
l=mid+1;
}
}
}
return false;
}
};
即使数组被旋转过,我们仍然可以利用这个数组的递增性,使用二分查找。对于当前的中点,如果它指向的值小于等于右端,那么说明右区间是排好序的;反之,那么说明左区间是排好序的。如果目标值位于排好序的区间内,我们可以对这个区间继续二分查找;反之,我们对于另一半区
间继续二分查找。
注意,因为数组存在重复数字,如果中点和左端的数字相同,我们并不能确定是左区间全部相同,还是右区间完全相同。在这种情况下,我们可以简单地将左端点右移一位,然后继续进行二分查找。
leetcode 154
class Solution {
public:
int findMin(vector<int>& nums) {
int l=0,r=nums.size()-1;
int mid;
while(l<r){
mid=l+(r-l)/2;
if(nums[mid]<nums[r]){
r=mid;
}else if(nums[mid]>nums[r]){
l=mid+1;
}else{
--r;
}
}
return nums[l];
}
};
上一题判断的是
l
e
f
t
=
=
m
i
d
left==mid
left==mid,所以是右移一位left;
这里判断的是
m
i
d
=
=
r
i
g
h
t
mid==right
mid==right,所以是左移一位right;
leetcode 540
class Solution {
public:
int singleNonDuplicate(vector<int>& nums) {
if(nums.size()==1) return nums[0];
int l=0,r=nums.size()-1,mid;
while(l<=r){
mid=l+(r-l)/2;
if(nums[mid] == nums[mid ^ 1]){
l=mid+1;
}else{
r=mid-1;
}
}
return nums[l];
}
};
nums[mid] == nums[mid ^ 1]
通过和1亦或表示下面的条件
{
X
为偶数
X
xor
1
=
X
+
1
X
为奇数
X
xor
1
=
X
−
1
\left\{\begin{array}{c}X \text { 为偶数 } \mathrm{X} \text { xor } 1=\mathrm{X}+1 \\X \text { 为奇数 } \mathrm{X} \text { xor } 1=\mathrm{X}-1\end{array}\right.
{X 为偶数 X xor 1=X+1X 为奇数 X xor 1=X−1
!(mid%2)&&nums[mid]==nums[mid+1])||((mid%2)&&nums[mid]==nums[mid-1])
leetcode 4
第一种方法,归并两个数组,返回中位数。
排序算法的时间复杂度为O((m+n)log(m+n))
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
nums1.insert(nums1.end(),nums2.begin(),nums2.end());
sort(nums1.begin(),nums1.end());
int n=nums1.size();
if(n%2==0){
return (double)(nums1[n/2]+nums1[n/2-1])*0.5;
}
else{
return (double)nums1[n/2];
}
}