当前位置: 首页 > article >正文

专题三_二分查找算法_算法详细总结

目录

二分查找

1.⼆分查找(easy)

1)朴素二分查找,就是设mid=(left+right)/2,x=nums[mid],t就是我们要找的值

2)二分查找就是要求保证数组有序的前提下才能进行。

3)细节问题:

总结:

细节:

2.二分查找的进阶:

查找区间左端点:

查找区间右端点:

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

解析:

总结:

总结二分模板:

记忆:

4.搜索插⼊位置(easy)

解析:

5.x的平方根(easy)

解析:

1)暴力:

2)优化:

6.⼭峰数组的峰顶(easy)

解析:

总结:

7.寻找峰值(medium)

解析:

8.搜索旋转排序数组中的最⼩值(medium)

解析:

1)优化:

以D为target:

以A为target:

总结:

9.LCR 点名

解法一:

解法二:

总结一下:


二分查找

二分查找算法,当我们发现一个规律,能在这个规律里面选取一个点之后,能把数组分成两部分,有选择性的选择一部分,进而能在另一部分继续选择,就说明数组有“二段性”。不一定非要数组完全有序,只要能找到一部分有规律,就可以采用二分查找。我们可以找这个数组的二分之一、三分之一、四分之一都行,因为只需要我们能将数组分成两部分就ok。

只要弄清除二分查找算法的进阶,查找"数组"的最左端点 或 查找"数组"的最右端点,对于二分查找的问题一般就迎刃而解了。

那么我们先来看一下朴素的二分查找,后面就来进阶:

1.⼆分查找(easy)

这题就是朴素的二分查找,简直入门级别。了解什么是二分,在什么情况下使用二分。

解析:

1)朴素二分查找,就是设mid=(left+right)/2,x=nums[mid],t就是我们要找的值

1.x<t ,lid-1   ->[left,right];
3.x==t   返回结果

2)二分查找就是要求保证数组有序的前提下才能进行。

3)细节问题:

1.循环结束的条件:left>right,那么while(left<=right)
2.为什么是正确的?
是从暴力解法优化过来的
3.时间复杂度,O(logN)

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

总结:

二分查找经典题目。首先最重要的就是判断循环的结束条件,left>right,因为在这之前,left==right的时候,仍然要判断一次nums[left]是否等于target,所以循环条件就是while(left<=right) 然后就是固定的套路

朴素二分查找,就是设mid=(left+right)/2,x=nums[mid],t就是我们要找的值
1.x<t ,left=mid+1   ->[left,right];
2.x>t, right=mid-1   ->[left,right];
3.x==t   返回结果

细节:

为了防止right 和 left 都太大,防止left+right 会存在溢出的风险,所以最好就不要采用这种直接相加的方式。 我们可以先求出这个数组的一半,然后再用这一半长度加上left起始位置,就同样可以得到(left+right)/2 的效果 即:mid=left+(left+right)/2

总结:

二分查找的模板:

     while(left<=right)
        {
            int mid=left+(right-left)/2;
            if(nums[mid]==target) return mid;
            else if(.....) left=mid+1;
            else right=mid-1;
        }

根据题目要求,往里面填入相关的条件,最主要的判断条件就是left<=right;二段性。

2.二分查找的进阶:

面对二分查找,那么数组是必须要有序才能进行吗?难道数组不有序,就不能进行二分查找了嘛?

面对这些问题,就有了二分的进阶,可以说学会二分进阶,简直无敌~

我们先来上一个简单示例:

eg:

要我们查早数组某一重复元素的开始和结尾的下标,任何利用O(logN) 的时间复杂度进行查找。

第一肯定会想到暴力,但时间复杂度是O(N) 太大,那么任何利用二分,就可以完美解决这个问题。

所以就想到利用二分的性质,将数组分两块!!!二段性~

一块 < t ; 一块 >= t 

那么由此就可以看出,我们要利用二分的性质来查找这一块区间的最左端点和最右端点,就能满足条件,因为这个数组满足二段性,那么如何实现呢?请看下面例子:

查找区间左端点:

1.循环区间:
x<t   ->   left=mid+1   [left,right]
x>=t   ->   right=mid   [left,right]

2.循环条件 left<right  
1).如果left==right  就是最终的结果,无需判断
2).如果判断,那么就会进入死循环,因为mid=(left+right)/2,right=mid,就会一直循环

3.求中点操作
1).left+(right-left)/2, 不能用left+(right-left+1)/2,因为这样的话,如果数组元素个数是偶数个,那么mid就等之间右边的一个元素,这时,right就跳不到left的位置,一直在mid这个位置进行死循环

4.为什么要用right=mid,而不是mid-1;因为我们判断的区间是,在大于等于t这个区间,那么就可能存在等于t的情况,如果冒然写right=mid-1,很可能会跳过nums[right]==t,这个值的区间,所以只能让right=mid

5.为什么判断while(left<=right) 就会死循环?
比如left==right==mid,这三个指针,都指向同一个下标,还要进行判断,那么right就一直等于mid 一直仍然进入循环出不来

查找区间右端点:

1.循环区间:

1)x<=t  那么此时x=nums[mid] 落在区间  [小于等于t] 的位置 那么肯定是更新left=mid,,因为我的left不能越过mid,如果left=mid+1,就会越过mid,如果此时的mid就是指向这个区间的最后一个元素,那么left越过后就到了[mid,right] 这个区间,那么这个区间里面就没有最终的结果

2)x>t 同理就是更新right=mid-1,因为这个区间是确定的,是大于target的值,不会存在我们要找的区间的值,就可以越过mid,成为mid-1

2.循环条件:
left<right,因为跟求左端点一样,如果判断等于的话,left,right,mid三个指针在同一个位置,会陷入死循环

3.求中点的方式

1)这次是采用left+(right-left+1)/2,不能采用left+(right-left)/2,因为正好跟求左端点相反,这里求右端点,left==mid,为了防止left一直等于mid陷入死循环,要让mid指向right这一边的中点,所以要让right-left+1

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

经过上面学习到的二分查找数组区间的左右端点,可以直接试试这题,会有很好的效果,是真的简单。

解析:

1)先将数组分块后,对其求mid,然后判断是求左端点还是求右端点

2)确定好后,对于nums[left] < target  ->   left=mid+1 的判断,说明left是可以越过mid的。

对于下面的记忆过程可以确定,上面的mid = left+ (right-left)/2 ,是没有+1的。

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        //查找区间左端点
        int n=nums.size();
        if(n==0) return {-1,-1};
        int left=0,right=n-1;
        vector<int> ret;
        while(left<right)
        {
            int mid=left+(right-left)/2;
            if(nums[mid]<target) left=mid+1;
            else right=mid;
        }
        if(nums[left]==target) ret.push_back(left); else ret.push_back(-1);

        //查找区间右端点
        left=0,right=n-1;
        while(left<right)
        {
            int mid=left+(right-left+1)/2;
            if(nums[mid]>target) right=mid-1;
            else left=mid;
        }
        if(nums[left]==target) ret.push_back(left); else ret.push_back(-1);
        return ret;
    }
};

总结:

1)暴力:
从头开始遍历,那时间复杂度肯定O(N^2) 绝对超时

2)优化,二分查找左右两端点
1.查找左端点
2.查找右端点
模板

总结二分模板:

查找区间左端点:
while(left<right)
        {
            int mid=left+(right-left)/2;
            if(...) left=mid+1;
            else right=mid;
        }

//查找区间右端点
        while(left<right)
        {
            int mid=left+(right-left+1)/2;
            if(...) right=mid-1;
            else left=mid;
        }
 

记忆:

当上面出现+1的时候,下面就是right=mid-1
当上面没有+1 的时候,下面就是left=mid+1

4.搜索插⼊位置(easy)

这题和前面一样,这个数据具有二段性,如果这时的你能够看出数组具有二段性,你就真的已经成功了一大步。将数组分为一块小于target;一块大于等于target;

解析:

还是一样的,简单的二分查找,对于返回target元素的下标,或者插入的下标,只要在>= target这个区间内,找到最左边的元素就ok,然后<target 区间的元素就已经确定,那么left=mid+1

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

学会了之后真的是很简单这种题目,利用二分效率还很高。

5.x的平方根(easy)

这一题,我第一次做的时候,确实没能看出来二段性,用的暴力,后来才发现原来这样是一样具有二段性,就比如从1~x ,那么一块,i*i<x ; 另一块是 i*i>=x;数组分两块,这样就可以照样用暴力。 

解析:

1)暴力:

求x的平方根,那么就是找i*i<=x 然后返回i,如果暴力,肯定超时,

2)优化:

可以考虑i~x的值,然后对于i*i 是否小于等于x,求这个区间的最右端点,即利用二分查找求最右端点
考虑到[mid,right] 区间是确定大于x的值,那么right=mid-1,那么上面判断mid就是mid=left+(right-left+1)/2,那么left=mid

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

对于这种二段性数组,掌握了套路简直信手拈来。

6.⼭峰数组的峰顶(easy)

题目意思就是说,要求先增后减的数组的峰值,那么这个二段性也太明显了!!!

解析:

1)那么还是简单的二分查找模板,只需要简单判断left 或 right会不会越过峰值即可。

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

总结:

面对求峰值在图中可以看出 left 是可以越过mid的,成为mid+1,所以这下right=mid,

mid=left+(right-left)/2,就全都一气呵成了~

7.寻找峰值(medium)

这题简直根上一题大差不差,只是多了几个峰值而已,但实质上,left和right缩小后其实也就只是在一个峰值范围内。

解析:

1)寻找数组中的任意一个峰值即可,在取mid的过程中,left和right就又被缩小到一个峰值,那么根上一题一样只需要判断left和right会不会越过峰值,带模板就能解决

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

跟上一题一样,就当个练手的,简直easy~

8.搜索旋转排序数组中的最⼩值(medium)

这一题其实二段性超级明显对吧,那么数组分两块,就是根峰值问题不一样的就是数组分两块后全是递增序列,那么我们可以找到里面的规律进行二分。

解析:

1)优化:

这题二分查找有点难度,跟之前的不一样,这个在一个区间内都是单调递增的,所以要单独拿出一个数据当作target,就比如D点,A~B段全是大于D的,C~D全是小于或等于D的。那么求这个数组的最小值,就是求C~D的左端点,那么找最左端点就是二分模板,if(nums[mid]>t) left=mid+1;
            else right=mid;
if(nums[mid]>t) left=mid+1;
            else right=mid;  //nums[mid]<=t,这里是C~D段,为了right不越过最小值点,所以right=mid,left在A~B段进行,所以当mid属于最大值点的时候,left也可以越过变成最小值点 所以left=mid+1

以D为target:

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

以A为target:

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

总结:

这证明了,二分其实是很随意的,但是只要把内在本质掌握好了,不管以哪个点为标准,都可以很快的找到正确答案。

9.LCR 点名

虽然只是一道简单题,但是要是能想到用二分,那么性能又能提升不少~

解法一:

O(N):

1.哈希表

2.直接遍历找结果

3.位运算

4.数学

解法二:

二分查找 

仍然是寻找二段性,然后判断left是否可以越过mid

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

总结一下:

我觉得二分最主要就是刚开始学习的查找左右两端点,如果了解这些了,那么对于二分算法就是了解的透透的了。再就是对于有序数组,第一就应该要想到双指针和二分查找,如果数组分块,具有二段性,那不用说了,妥妥的二分!

我觉得对我的进步挺大的,希望对你也有帮助~


http://www.kler.cn/a/302799.html

相关文章:

  • SpringBoot实战(三十一)集成iText5,实现RSA签署PDF
  • 亲测有效:Maven3.8.1使用Tomcat8插件启动项目
  • Redo与Undo的区别:数据库事务的恢复与撤销机制
  • MFC工控项目实例二十九主对话框调用子对话框设定参数值
  • 【C++】一种针对代码的连续条件检查方案,累计布尔结果
  • Vue 项目打包后环境变量丢失问题(清除缓存),区分.env和.env.*文件
  • Jmeter之beanshell使用
  • 适合博客的组件库
  • RHEL 7 安装配置( Linux 网络操作系统 02)
  • 【智能流体力学】数值模拟中的稳态和瞬态
  • OpenHarmony(鸿蒙南向开发)——轻量系统芯片移植指南(二)
  • C#多线程进阶
  • Java面试题·解释题·单例模式、工厂模式、代理模式部分
  • 基于Qt的串口包装器
  • 【SqlServer】SQL Server Management Studio (SSMS) 下载、安装、配置使用及卸载——保姆级教程
  • 数学建模笔记—— 最大最小化规划模型
  • mysql——关于表的增删改查(CRUD)
  • macOS镜像下载(ISO、DMG)
  • xss-labs-master通关教程
  • 起重机检测系统源码分享
  • 【C++11 —— 包装器】
  • 【Sceneform-EQR】通过sceneform-eqr实现一个视频播放器(使用安卓MediaPlayer实现视频播放)
  • 从0开始深入理解并发、线程与等待通知机制
  • 基于微信小程序点餐、外卖系统的设计与实现 (源码+lw+参考文档+核心代码讲解等)
  • 多模态大模型中的图片文本对齐
  • visual studio code下载教程(手把手)