二分查找算法总结

目录

1. 算法解释

2.经典例题——求开方

 69.x的平方根

3.查找区间

34. Find First and Last Position of Element in Sorted Array (Medium)

4.旋转数组查找数字

81. Search in Rotated Sorted Array II (Medium)

​ 154. 寻找旋转排序数组中的最小值 II  ​

5.奇偶和二分

6.对两个数组同时二分


1. 算法解释

二分查找也常被称为二分法或者折半查找,每次查找时通过将待查找区间分成两部分并只取
一部分继续查找,将查找的复杂度大大减少。对于一个长度为 O ( n ) 的数组,二分查找的时间复
杂度为 O ( log n )
举例来说,给定一个排好序的数组 {3,4,5,6,7} ,我们希望查找 4 在不在这个数组内。第一次
折半时考虑中位数 5 ,因为 5 大于 4, 所以如果 4 存在于这个数组,那么其必定存在于 5 左边这一
半。于是我们的查找区间变成了 {3,4,5} 。(注意,根据具体情况和您的刷题习惯,这里的 5 可以
保留也可以不保留,并不影响时间复杂度的级别。)第二次折半时考虑新的中位数 4 ,正好是我们
需要查找的数字。于是我们发现,对于一个长度为 5 的数组,我们只进行了 2 次查找。如果是遍
历数组,最坏的情况则需要查找 5 次。
我们也可以用更加数学的方式定义二分查找。给定一个在 [ a , b ] 区间内的单调函数 f ( x ) ,若
f ( a ) f ( b ) 正负性相反,那么必定存在一个解 c ,使得 f ( c ) = 0 。在上个例子中,
f ( x ) 是离散函数 f ( x ) = x + 2 ,查找 4 是否存在等价于求 f ( x ) − 4 = 0 是否有离散解。因为 f ( 1 ) − 4 = 3 4 = 1 < 0 f ( 5 ) − 4 = 7 4 = 3 > 0 ,且函数在区间内单调递增,因此我们可以利用二分查找求解。如果最后 二分到了不能再分的情况,如只剩一个数字,且剩余区间里不存在满足条件的解,则说明不存在 离散解,即 4 不在这个数组内。
具体到代码上,二分查找时区间的左右端取开区间还是闭区间在绝大多数时候都可以,因此
有些初学者会容易搞不清楚如何定义区间开闭性。这里我提供两个小诀窍,第一是尝试熟练使用一种写法,比如左闭右开(满足 C++、Python 等语言的习惯)或左闭右闭(便于处理边界条件),尽量只保持这一种写法;第二是在刷题时思考如果最后区间只剩下一个数或者两个数,自己的写法是否会陷入死循环,如果某种写法无法跳出死循环,则考虑尝试另一种写法。
实际上我们在实际操作中l<r l<=r l=mid l=mid+1 r=mid r=mid-1,都会带来很不一样的结果,很难把握,一般初学可以通过具体的样例来调整。同时下标的问题也很关键,4. 寻找两个正序数组的中位数,这道题就是因为下标我没弄好,直接gg

二分查找也可以看作双指针的一种特殊情况,但我们一般会将二者区分。双指针类型的题,
指针通常是一步一步移动的,而在二分查找里,指针每次移动半个区间长度

2.经典例题——求开方

 69.x的平方根

题解
我们可以把这道题想象成,给定一个非负整数 a ,求 f ( x ) = x 2 a = 0 的解。因为我们只考
x 0 ,所以 f ( x ) 在定义域上是单调递增的。考虑到 f ( 0 ) = a 0 f ( a ) = a 2 a 0 ,我们可以对 [ 0 , a ] 区间使用二分法找到 f ( x ) = 0 的解。
注意,在以下的代码里,为了防止除以 0 ,我们把 a = 0 的情况单独考虑,然后对区间 [ 1 , a ]
进行二分查找。我们使用了左闭右闭的写法。
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;
}

3.查找区间

34. Find First and Last Position of Element in Sorted Array (Medium)

题解
这道题可以看作是自己实现 C++ 里的 lower_bound upper_bound 函数。这里我们尝试
使用左闭右开的写法,当然左闭右闭也可以。
这里就有问题了,其实我写的总体都还是对的,但是为什么会出错或者全错呢?原因就在于二分时变换区间的步骤,还有一些特殊情况,这些细节也很难,万万不要忽视:
先看看我改了很多遍后的:

class Solution {
public:
    //我这个还是根据自己理解来的,很多细节一开始没注意到后来都可以了,也可以去看看pdf的标答
    
    int LowerBounder(vector<int>& nums, int target) {
        int low = 0;
        int high = nums.size() - 1;
        int mid = -1;
        while (low<high) {
            mid = (low + high) / 2;
            if (nums[mid] < target) {
                low = mid + 1;
            }
            else if (nums[mid] >= target) {
                high = mid ;
            }
        }
        if (low == nums.size() || nums[low] != target) {
            return -1;
        }
        return low;

    }


    int UpperBounder(vector<int>& nums, int target, int l) {
        int low = l;
        if (low == -1) { return -1; }
        while (nums[low] == target) {
            ++low;
            if (low == nums.size()) { break; }
        }
        return --low;

    }

    vector<int> searchRange(vector<int>& nums, int target) {
        if (nums.empty()) { return vector<int>{-1, -1}; }
        int l = LowerBounder(nums, target);
        int r = UpperBounder(nums, target,l);
        if (l ==-1|| r==-1) {
            return vector<int>{-1, -1};
        }
        return vector<int>{l, r};

    }
    
};

标答:

 热门题解:

 

// 二分查找,寻找target的右边界(不包括target)
// 如果rightBorder为没有被赋值(即target在数组范围的左边,例如数组[3,3],target为2),为了处理情况一
int getRightBorder(vector<int>& nums, int target) {
    int left = 0;
    int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
    int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况
    while (left <= right) { // 当left==right,区间[left, right]依然有效
        int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
        if (nums[middle] > target) {
            right = middle - 1; // target 在左区间,所以[left, middle - 1]
        } else { // 当nums[middle] == target的时候,更新left,这样才能得到target的右边界
            left = middle + 1;
            rightBorder = left;
        }
    }
    return rightBorder;
}

 左边界:

// 二分查找,寻找target的左边界leftBorder(不包括target)
// 如果leftBorder没有被赋值(即target在数组范围的右边,例如数组[3,3],target为4),为了处理情况一
int getLeftBorder(vector<int>& nums, int target) {
    int left = 0;
    int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
    int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况
    while (left <= right) {
        int middle = left + ((right - left) / 2);
        if (nums[middle] >= target) { // 寻找左边界,就要在nums[middle] == target的时候更新right
            right = middle - 1;
            leftBorder = right;
        } else {
            left = middle + 1;
        }
    }
    return leftBorder;
}

代码随想录704. 二分查找

代码随想录35.搜索插入位置

4.旋转数组查找数字

81. Search in Rotated Sorted Array II (Medium)

这是一道难题:

题解
即使数组被旋转过,我们仍然可以利用这个数组的递增性,使用二分查找。对于当前的中点, 如果它指向的值小于等于右端,那么说明右区间是排好序的;反之,那么说明左区间是排好序的。 如果目标值位于排好序的区间内,我们可以对这个区间继续二分查找;反之,我们对于另一半区间继续二分查找。

注意,因为数组存在重复数字,如果中点和左端的数字相同,我们并不能确定是左区间全部
相同,还是右区间完全相同。在这种情况下,我们可以简单地将左端点右移一位,然后继续进行二分查找.至于为什么移动左端点,因为移动左端点后Mid也会动,真的很妙,要体会一下
这个把我扣死了,一个没想到,二来提醒后也没有处理好

先看正确的:

  //下面是正确答案:[3,1] 1
    bool search2(vector<int>& nums, int target) {
        int start = 0, end = nums.size() - 1;
        while (start <= end) {
            //这里又用小于等于
            //它把while放在最外面是很关键的,我上面那个为什么最后没法改了就是因为我最外层是两个if判断,十分不自然
            int mid = (start + end) / 2;
            if (nums[mid] == target) {
                return true;
            }
            if (nums[start] == nums[mid]) {
                // 无法判断哪个区间是增序的 举例:1111111112221111111111111111111111
                //这个很难思考,更难的是要和其他的二分连接起来
                ++start;
            }
            else if (nums[mid] <= nums[end]) {
                // 右区间是增序的
                if (target > nums[mid] && target <= nums[end]) {
                    start = mid + 1;
                }
                else {
                    //说明target不在右区间
                    //而下一次循环你可以看做继续在剩下的里面找有序的区间,然后二分,有点递归的感觉,这个也是很难的我没想到
                    //我是用了while循环,也幸好他时间要求不高,否则一定要这样做
                    end = mid - 1;
                }
            }
            else {// 左区间是增序的
                if (target >= nums[start] && target < nums[mid]) {
                    end = mid - 1;
                }
                else {
                    //说明Target不在左区间
                    start = mid + 1;
                }
            }
        }
        return false;
    }

再看一下更易理解的:

//法三:
    //每次二分,左半部分和右半部分至少有一边是有序的,以此为条件可以分成两种情况:
//1、左半边是有序的
//(1) target落在左半边
//(2) otherwise
//2、右半边是有序的
//(1) target落在右半边
//(2) otherwise
//综上所述,一共两种可能性,这两种情况各自又有两种可能性,代码如下:
    bool search3(vector<int>& nums, int target) {
        int l = 0, r = nums.size() - 1;
        while (l <= r) {
            
            //处理重复数字
            while (l < r && nums[l] == nums[l + 1]) ++l;
            while (l < r && nums[r] == nums[r - 1]) --r;
            int mid = l + (r - l) / 2;
            if (nums[mid] == target) return true;
            //左半部分有序
            if (nums[mid] >= nums[l]) {
                if (target < nums[mid] && target >= nums[l]) r = mid - 1;//target落在左半边
                else l = mid + 1;
            }
            else {//右半部分有序
                if (target > nums[mid] && target <= nums[r]) l = mid + 1;//target落在右半边
                else r = mid - 1;
            }
        }
        return false;
    }

最后看看我错误的代码:(最大的问题是我最外围是if这样就很难搞了)

//这里关键有一个问题,你一开始等于的时候,没法知道左右的情况所以我是279/280
    bool search(vector<int>& nums, int target) {
        if (nums.empty()) { return false; }
        int l = 0;
        int r = nums.size() - 1;
        int mid = (l + r + 1) / 2;
        int flag = -1;
        if (nums[mid] <= nums[r]) {
            //说明右端是排好序的:
            flag = 1;
            l = mid;
            while (l < r) {
                mid = (l + r) / 2;
                if (nums[mid] < target) {
                    l = mid + 1;
                }
                else if (nums[mid] > target) {
                    r = mid - 1;
                }
                else {
                    return true;
                }
            }
            if (nums[l] == target) return true;
        }
        else {
            //说明左端是排好序的:
            flag = 0;
            r = mid;
            while (l < r) {
                mid = (l + r) / 2;
                if (nums[mid] < target) {
                    l = mid + 1;
                }
                else if (nums[mid] > target) {
                    r = mid - 1;
                }
                else {
                    return true;
                }
            }
            if (nums[l] == target) return true;
        }

        if (flag == 0) {
            //说明在左没找到哈哈
            for (int i = nums.size() / 2; i < nums.size(); ++i) {
                if (nums[i] == target) {
                    return true;
                }
            }
        }
        else if (flag == 1) {
            for (int i = 0; i <= nums.size() / 2; ++i) {
                if (nums[i] == target) {
                    return true;
                }
            }
        }

        return false;

    }

​ 154. 寻找旋转排序数组中的最小值 II  ​

class Solution {
public:
    int findMin(vector<int>& nums) {
        //一开始是升序,我想利用81的方法来做
        //条件说明数组不空
        int low = 1;
        int high = nums.size() - 1;
        int mid;
        int mini = nums[0];
        //注意[3, 1] 1
        while (low <= high) {
            mid = (low + high) / 2;
            if (nums[mid] < mini) {
                //这个是用来处理[3, 1] 1
                mini = nums[mid];
            }
            if (nums[low] == nums[mid]) {
                ++low;
                continue;
            }
            if (nums[low] < nums[mid]) {
                //说明左边升序
                if (mini > nums[low]) {
                    mini = nums[low];
                }
                else {
                    //说明左边都比Mini大,所以去看右边
                    low = mid;
                }
                
            }
            else {
                //说明右边升序
                if (mini > nums[mid]) {
                    mini = nums[mid];

                }
                else {
                    high = mid;
                }
            }
        }

        return mini;

    }
};

5.奇偶和二分

class Solution {
public:

    //第一个想法分奇偶数组
    int singleNonDuplicate(vector<int>& nums) {
        int l = 0;
        int r = nums.size() - 1;
        int mid;
        while (l < r) {
            mid = l + (r - l) / 2;
            if (mid == nums.size()) {
                //说明是最后一个
                break;
            }
            if (mid % 2 == 0) {
                if (nums[mid] == nums[mid + 1]) {
                    //说明x应该在Mid右边
                    l = mid + 2;
                }
            }
            else if(mid%2==1){
                if (nums[mid] == nums[mid - 1]) {
                    //说明x在mid右边
                    l = mid + 1;
                }
            }
            else {
                //说明x在左边
                r = mid;
            }
        

        }
        return nums[l];
    }

 法二

        // 有序全数组二分, x为单一元素下标
 // nums [1, 1, 2, 2, 3, 4, 4, 5, 5]
 // index[0, 1, 2, 3, 4, 5, 6, 7, 8]
 // 数组总长度是奇数,单一元素x左右两边数据长度都是偶数,观察单一元素左边得:
 // 如果上述比较相邻元素的结果是相等,则 mid < x,调整左边界,否则 mid ≥ x,调整右边界
 // 偶数时比较nums[mid] 和 nums[mid + 1],一样说明x在右边 调整左边界 left = mid + 1;
 // 奇数时比较nums[mid - 1] 和 nums[mid],一样说明x在右边 调整左边界 left = mid + 1;
 // 当 mid 是偶数时,mid + 1 = mid ^ 1。
 // 当 mid 是奇数时,mid - 1 = mid ^ 1。

int singleNonDuplicate2(vector<int>& nums) {


        int left = 0, right = nums.size() - 1;
        while (left < right) {
            int mid = left + ((right - left) >> 1);
            if (nums[mid] ==  nums[mid ^ 1]) {
                left = mid + 1;
            }
            else {
                right = mid;
            }
        }
        return nums[left];
    }

    int singleNonDuplicate3(vector<int>& nums) {
        int l = 0, r = nums.size() - 1, m;
        while (l < r) {
            m = l + (r - l) / 2;
            if (m % 2 == 1) {
                m--;
            }
            if (nums[m] == nums[m + 1]) {
                l = m + 2;
            }
            else {
                r = m;
            }
        }
        return nums[l];
    }

6.对两个数组同时二分

 //要求是log(m+n)
    /*这道题让我们求两个有序数组的中位数,而且限制了时间复杂度为O(log (m+n)),
    看到这个时间复杂度,自然而然的想到了应该使用二分查找法来求解。那么回顾一下中位数的定义,
    如果某个有序数组长度是奇数,那么其中位数就是最中间那个,如果是偶数,那么就是最中间两个数字的平均值。
    这里对于两个有序数组也是一样的,假设两个有序数组的长度分别为m和n,由于两个数组长度之和 m+n 的奇偶不确定,
    因此需要分情况来讨论,对于奇数的情况,直接找到最中间的数即可,偶数的话需要求最中间两个数的平均值。
    为了简化代码,不分情况讨论,我们使用一个小trick,我们分别找第 (m+n+1) / 2 个,和 (m+n+2) / 2 个,
    然后求其平均值即可,这对奇偶数均适用。加入 m+n 为奇数的话,那么其实 (m+n+1) / 2 和 (m+n+2) / 2 的值相等,
    相当于两个相同的数字相加再除以2,还是其本身。*/
    //然后要写函数找到m+n+1和m+n+2
    //实际就是一个写一个函数找到合并后的数组中第k个元素
    //不过我们肯定不能去合并,不然就要超过Log(m+n)
    //所以二分查找在这个函数用到
    //来吧:

    /*
    思路可以总结如下:取两个数组中的第k/2个元素进行比较,如果数组1的元素小于数组2的元素,
    则说明数组1中的前k/2个元素不可能成为第k个元素的候选,所以将数组1中的前k/2个元素去掉,
    组成新数组和数组2求第k-k/2小的元素,因为我们把前k/2个元素去掉了,所以相应的k值也应该减小。
    另外就是注意处理一些边界条件问题,比如某一个数组可能为空或者k为1的情况。*/

int findTheK(vector<int>& nums1, int i,vector<int>& nums2,int j,int k) {
        if (i >= nums1.size()) return nums2[j + k - 1];//nums1中的搜索标i已经超出了,说明第k个一定在nums2了
        /*
        当某一个数组的起始位置大于等于其数组长度时,说明其所有数字均已经被淘汰了,
        相当于一个空数组了,那么实际上就变成了在另一个数组中找数字,直接就可以找出来了
        */
        if (j >= nums2.size()) return nums1[i + k - 1];

        if (k == 1) {
            return min(nums1[i], nums2[j]);
            //这里也是,不是0,是i,j,ij代表的就是在递归中数组的左边界
        }

        //一直错的细节点i+k/2-1这个-1不加就错光光,你奶奶滴,注意k是指第几个第一个实际上下标是0,所以要减一才行。。。
        int mid1 = i + k / 2 - 1 < nums1.size() ? nums1[i + k / 2 - 1] : 200000000;
        int mid2 = j + k / 2 - 1 < nums2.size() ? nums2[j + k / 2 - 1] : 200000000;

        if (mid1<mid2) {
            //然后这里的二分不是令i=k/2,而是i=i+k/2,k不是终点而是路程
            return findTheK(nums1, i+k / 2, nums2, j, k - k / 2);
        }
        else {
            return findTheK(nums1, i, nums2, j+k / 2, k - k / 2);
        }


    }



    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size();
        int n = nums2.size();
        double r=findTheK(nums1, 0, nums2, 0, (m + n + 1)/2) ;
        double r2 = findTheK(nums1, 0, nums2, 0, (m + n + 2)/2);
        return (r+r2)/2;
    }

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值