优选算法——二分查找

1. 二分查找简介

关于二分查找,想必有很多同学都是有所耳闻的,我们在C语言阶段也接触过,当时我们说的是数组有序的情况下才可以使用二分查找,但是在这里,这句话是有局限性的,在一些特定情况下,无序的也可以用二分来解决。二分查找算法存在3个模板,分别是最简单版本、查找左边界、查找右边界,下面将通过例题为大家一一展现。

2. 二分查找

题目链接704. 二分查找 - 力扣(LeetCode)

题目展示

题目分析

本题属于最简单的二分算法,要用到上面提到的第一种模板。

大家可以看上图,要注意的首先是循环结束的条件,一定是left<=right,因为相等的情况下,就剩一个数,这个数仍然需要判断;其次,二分算法的时间复杂度是O(logN),这意味着其效率是非常高的。

代码实现:

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

朴素模板展示

大家能够发现,这个模板其实非常简单,省略号中的内容需要根据具体题目分析出"二段性"才可以得到,这里的"二段性"是解决问题的关键。

3. 在排序数组中查找元素的第一个位置和最后一个位置

题目链接34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

题目展示

题目分析: 这道题用上一道题的方法就无法解决了,因为我们无法确定找到的mid是否为边界点,而本题要求的是找到左端点和右端点,所以本题要运用前面说过的后两种模板来解决,具体大家来看下图;

大家注意与朴素二分进行区分,以查找左端点为例,我们将数组分为了两个区间,这也就是前面所说的二段性,在左端点左边,一定是小于目标值的,这时指针的移动和前面是一样的;而左端点右边的部分,是大于或者等于目标值的,这时我们指针的移动就需要与前面进行区别了,为什么会改变呢?

大家可以来思考这样一种情况,当我们的mid指向左端点时,那么此时,如果按照之前朴素二分的方法去写,即right=mid-1,这时我们就会错过左端点,导致找不到左端点了,所以指针的移动方式是right=mid。同理,查找右端点时也是一样的分析方法。

还有两点细节需要大家注意,循环条件和求中点的方式,循环条件一定是left<right,这与朴素解法是有明显区别的,具体原因大家可以参照上图中的解释;求中点的方式对于两种不同的查找方式也有所区别,在朴素二分中,这两种求中点的方式都是OK的,但是在这里是需要匹配使用,否则会导致死循环。

代码实现

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) 
    {
        //处理边界情况
        if(nums.size()==0)
        {
            return{-1,-1};
        }
        int left=0;
        int right=nums.size()-1;
        int begin=0;
        //二分左端点
        while(left<right)
        {
            int mid=left+(right-left)/2;
            if(nums[mid]<target)
            {
                left=mid+1;
            }
            else
            {
                right=mid;
            }
        }
        //判断是否有结果
        if(nums[left]!=target)
        {
            return{-1,-1};
        }
        else
        {
            begin=left;//标记左端点
        }
        left=0;
        right=nums.size()-1;
        //二分右端点
        while(left<right)
        {
            int mid=left+(right-left+1)/2;
            if(nums[mid]<=target)
            {
                left=mid;
            }
            else
            {
                right=mid-1;
            }
        }
        return {begin,right};
    }
};

模板总结

上面就是两种二分方法的模板,大家可以根据对原理的理解去记忆。

4. x的算术平方根

题目链接69. x 的平方根 - 力扣(LeetCode)

题目展示

题目分析:本题要求我们求一个数的算数平方根,不能用内置函数,这道题我们怎么去寻找二段性呢?大家来看下图:

大家可以发现,我们最终的结果的平方一定在上图所示的两个区间的其中之一里,所以二段性就出来了,这时我们仅需要对不同的情况进行分类讨论就可以解决问题了。

代码实现

​
class Solution {
public:
    int mySqrt(int x) 
    {
        if(x<1)
        {
            return 0;
        }
        int left=1;
        int right=x;
        while(left<right)
        {
            long long mid=left+(right-left+1)/2;//防溢出
            if(mid*mid<=x)
            {
                left=mid;
            }
            else
            {
                right=mid-1;
            }
        }
        return left;    
    }
};

​

5. 搜索插入位置

题目链接35. 搜索插入位置 - 力扣(LeetCode)

题目展示

题目分析

代码实现

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

6. 山脉数组的封顶索引

题目链接852. 山脉数组的峰顶索引 - 力扣(LeetCode)

题目展示

题目分析:本题需要我们找到顶峰元素的下标,我们需要找到题目中存在的二段性,具体大家来看下图:

大家可以看到,以顶峰元素为界,我们将数组分成了两部分,两部分各自有特殊的规律,就得到了二段性。

代码实现

class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) 
    {
        int left=0;
        int right=arr.size()-1;
        while(left<right)
        {
            int mid=left+(right-left+1)/2;
            if(arr[mid]>arr[mid-1])
            {
                left=mid;
            }
            if(arr[mid]<arr[mid-1])
            {
                right=mid-1;
            }
        }
        return left;
    }
};

7. 寻找峰值

题目链接162. 寻找峰值 - 力扣(LeetCode)

题目展示

题目分析:本题与上一题有一些类似,也是让我们寻找峰值并返回其下标,分析方法类似;

这里大家要注意本题二段性的由来,以mid为界,当mid右边是递减序列时,意味着左边一定会出现峰值,因为最左边是负无穷;同理,当mid左边是递增序列时,意味着右边一定会出现峰值,因为最右边也是负无穷。由此,二段性就出来了。

代码实现

class Solution {
public:
    int findPeakElement(vector<int>& nums) {
        int left=0;
        int right=nums.size()-1;
        while(left<right)
        {
            int mid=left+(right-left)/2;
            if(nums[mid]<nums[mid+1])
            {
                left=mid+1;
            }
            if(nums[mid]>nums[mid+1])
            {
                right=mid;
            }
        }
        return left;
    }
};

8. 寻找旋转排序数组中的最小值

题目链接153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)

题目展示

题目分析: 本题要求我们设计算法复杂度为O(logN),其实就已经暗示我们使用二分来解决;

这里大家可以看到,我们根据数组的特性,可以将其分成两部分,在图中可以发现,AB段的值一定大于D点的值,而CD段的值一定小于或者等于D点的值;当mid落在AB段时,意味着结果肯定不会在mid左边;同理当mid落在CD段时,意味着结果肯定不会在mid右边。

代码实现

class Solution {
public:
    int findMin(vector<int>& nums) {
        int left=0;
        int right=nums.size()-1;
        int x=nums[right];
        while(left<right)
        {
            int mid=left+(right-left)/2;
            if(nums[mid]<=x)
            {
                right=mid;
            }
            if(nums[mid]>x)
            {
                left=mid+1;
            }
        }
        return nums[left];
    }
};

9. 点名问题

题目链接LCR 173. 点名 - 力扣(LeetCode)

题目展示

题目分析:本题有很多种解法,这里我们只介绍二分解法,而二分解法也是本题的最优解法;

大家可以看到本题的二段性来源于数组中的值与其下标的对应关系,以消失的那个数字为界,我们可以将数组分为两部分,左边是数组值与下标相等的情况,右边是数组值与下标不等的情况;由此我们就可以得到二段性,当mid位于左边区间时,那么结果就一定不在mid左边;同理,当mid位于右边区间时,那么结果就一定不在mid右边。

代码实现

class Solution {
public:
    int takeAttendance(vector<int>& records)
    {
        int left=0;
        int right=records.size()-1;
        while(left<right)
        {
            int mid=left+(right-left)/2;
            if(records[mid]==mid)
            {
                left=mid+1;
            }
            else
            {
                right=mid;
            }
        }
        if(records[left]==left)
        {
            return left+1;
        }
        return left;
    }
};

10. 总结

本篇博客为大家介绍了二分算法,其核心在于寻找二段性,二段性确定了,代码可以直接套模板进行编写。二分算法是一种效率很高的算法,可以帮助我们解决一些比较复杂的问题,希望大家可以掌握上面所提到的例题,最后希望本篇博客可以为大家带来帮助,感谢阅读!

相关推荐
芒果爱编程1 小时前
MCU、ARM体系结构,单片机基础,单片机操作
开发语言·网络·c++·tcp/ip·算法
再不会python就不礼貌了1 小时前
震撼!最强开源模型通义千问2.5 72B竟在4GB老显卡上成功运行!
人工智能·算法·机器学习·chatgpt·产品经理
工业甲酰苯胺4 小时前
C语言之输入输出
c语言·c++·算法
努力d小白5 小时前
leetcode98.验证二叉搜索树
算法
YueTann5 小时前
Leetcode SQL 刷题与答案-基础篇
sql·算法·leetcode
归寻太乙5 小时前
算法基础Day7(动态规划)
算法·动态规划
hn小菜鸡6 小时前
LeetCode 2320.统计放置房子的方式数
算法·leetcode·职场和发展
高一学习c++会秃头吗6 小时前
leetcode_547 省份数量
算法·leetcode·职场和发展
天赐细莲6 小时前
(仓颉) Cangjie 刷力扣基础语法小结
数据结构·算法·leetcode·职场和发展
dundunmm7 小时前
论文阅读:Statistical Comparisons of Classifiers over Multiple Data Sets
论文阅读·人工智能·算法·机器学习·评估方法