zl程序教程

您现在的位置是:首页 >  后端

当前栏目

要写出优雅的二分查找实属不易(C++,LeetCode34)

C++ 查找 优雅 二分 写出
2023-09-11 14:20:51 时间

先说句废话:我个人比较喜欢左闭右开区间,所以这里仅展示左闭右开区间的代码......

废话不多说先看题:(来源于leetcode)在排序数组中查找元素的第一个和最后一个位置


想都不用想,有序数组,显然二分。

于是我就开始敲代码

敲代码

敲代码

敲完过一下示例

啪!解错。

改完再过

啪!超时。

改完再过

啪!越界。

······

(事实证明stl的lower_bound和upper_bound虽然很好用,但是没事练练写二分还是很重要的。。。)

虽然最后,代码还是过了。但是···真的不堪入目:

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if(nums.size()<1)
            return {-1,-1};
        int left = 0,right = nums.size();
        while(left<right){
            int mid = (left+right)/2;
            if(nums[mid]>=target)
            {
                right = mid;
            }
            else if(nums[mid]<target)
            {
                left = mid+1;
            }
        }
        int res1 = right;
        if(res1>=nums.size()||nums[res1]!=target)
            res1 = -1;
         left = 0,right = nums.size();
        while(left<right){
            int mid = (left+right)/2;
            if(nums[mid]>target)
            {
                right = mid;
            }
            else
            {
                left = mid+1;
            }
        }
        int res2 = left+1<nums.size()&&nums[left]==target?left:left-1;
        if(res2<0||nums[res2]!=target)
            res2 = -1;
        return {res1,res2};
    }
};

 (啊啊啊啊啊啊啊·····································)


那么到底怎么才能写出优雅的二分查找呢?

(这里就拿左闭右开区间的写法作为展示,其实这个搞明白了左闭右闭区间也能很好的写出来。)

记住一点:先明确思路,再动手去做,并且时刻明白自己在做什么。

首先,根据二分法的思想,每一次取mid = left +(right-left)>>1 ,由于我们确定的是[left,right) 区间,

所以,如果数组元素数量是奇数,所得的mid对应的元素便是中间元素;

如果数组元素数量是偶数,所得的mid对应的元素其实是中间偏  的元素。

其次要注意,每一次更新左右端点,都要保证区间能变小能够使得循环跳出,否则会导致死循环。

 好的,然后我们要做的就是在二分查找的大框架中一步一步填补空缺:

(以下代码的注释便是思考的过程)

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if(nums.size()==0)
            return {-1,-1};     //这个特判是必要的,因为后面免不了需要判断是否存在(否则24,40行处会出问题),
        //*******************
        int left = 0,right = nums.size(); //区间的左右
        int res1,res2;                    //用来分别记录两次查找的答案
        //***********************************
        while(left<right){                //先找最靠前的target
            int mid = left+((right-left)>>1); //每一次循环都要取mid,即此区间中间的数,
                                            //若区间内元素为奇数,则mid代表最中间的数,偶数的话就是最中间偏右的数
            if(nums[mid]<target){           //如果nums[mid]小于target  则说明[0,mid]区间内都没有target 则使left = mid+1
                left = mid+1;  
            }else if(nums[mid]>target){     //如果nums[mid]大于target  则说明[mid,right)区间内都没有target 则使right = mid
                right = mid;
            }else{          //若nums[mid]等于target 说明最靠【前】的target在 [left,mid]内   本应该使right = mid+1
                                        //但是此时考虑到如果right = mid+1  可能会导致死循环 比如搜索区间:[1,2,3) target = 2时 right就不变了
                 right = mid;           //所以暂且先令right = mid;
                                        //因为这样的话,如果mid前面没有target的话,答案就是这里的right(并且此后right不会再移动)  如果有的话 也会在【更新后】的[left,right)区间内
            }                           //最后思考一下,如果循环跳出时是什么情况————是left == right(因为我们的更新区间的方式,所以不会出现left>right)
        }                               //而此时left ==right就是我们要的答案吗? 不一定!因为可能target不在nums内
                                        //但是如果存在的话,由上面咱们缜密地缩小区间可知,target肯定跑不掉,一定是现在的right,不要想些其他的有的没的了。
        res1=right<nums.size()&&nums[right]==target?right:-1;  //所以最后判断下是否存在即可  但是此时要注意 因为我们是左闭右开区间 如果right没动的话 right 就越界了
                                                                //所以要加一个判断条件right<nums.size()
        //************************************
        left = 0,right = nums.size();
        while(left<right) //再找最靠后的target
        {                 //思考过程同上
            int mid = left +((right -left)>>1);
            if(nums[mid]<target){
                left = mid+1;
            }else if(nums[mid]>target){
                right = mid;
            }else{                 //主要是这里不同,若nums[mid]等于target  说明最靠【后】的target在[mid,right)内 所以移动left短点
                                   //但是此时考虑到如果left = mid  可能会对导致死循环 比如搜索区间[1,2) target = 2 时 left就不变了
                left = mid +1;     //所以暂且先令left = mid +1;
                                   //因为这样的话,如果mid后面没有target的话,答案就是这里的 left-1 如果有的话 也会在【更新后】的[left,right)区间内
            }
        }                          //由缩小方式可知结束时一定是left == right
        res2 = left-1>=0&&nums[left-1]==target?left-1:-1;//判断是否存在  注意!!由于是判断下标left-1    所以如果left==0的话会爆掉 
                                                         //所以要加一个判断条件:left-1>=0  (注意 因为这里是left-1也是right-1所以不用担心会大于等于nums.size())
        //********************************************************
        return {res1,res2};        //输出答案 结束
    }
};

条理清楚,一般就会减少混乱及其带来的麻烦。

然后,考虑到如果查找不到target的话 可以直接输出{-1,-1},于是还可以做点优化

最后合并一下nums[mid]==target的情况

得到改进版代码:

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if(nums.size()<=0) return {-1,-1};
        int left = 0,right = nums.size();
        int res1,res2;
        while(left<right)
        {
            int mid = left+((right-left)>>1);
            (nums[mid]<target)?left=mid+1:right = mid;
        }
        if(right==nums.size()||nums[right]!=target)
            return {-1,-1};
        else 
            res1 = right;
        left = 0,right = nums.size();
         while(left<right)
        {
            int mid = left+((right-left)>>1);
            (nums[mid]<=target)?left=mid+1:right = mid;
        }
        if(right==0||nums[right-1]!=target)
            return {-1,-1};
        else 
            res2 = right-1;
        return {res1,res2}; 
    }
};

结束