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

算法练习——二分算法

学前须知

👉:二分算法的核心在于:能够正确找到数据的二段性(其中一段有目标值,另一段没有)

👉:二分算法的另一个核心在于边界条件的取舍,这关系到整个代码是否会陷入死循环

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

题目要求:

解题思路:

二分算法的关键在于:如何找到那个能够将数据分成两段,并逐步缩小范围,最终得到想要的答案。

本题思路:用两次二分算法分别确定左下标和右下标,完成题解。

有关二分算法的知识补充——当只有两个数据时(偶数)

👉:mid = left + (right -left)/2 得到是左边数据

👉:mid = left + (right -left+1)/2得到是右边数据

小贴士(规律)

凡是 right = mid - 1   则 mid = left + (right -left+1)/2

否则 mid = left + (right -left)/2

二分查找(以找到左下标为例):

问:怎么做才能得到左边的下标呢?

答:前面我们提到,二分算法的目的是能够将数据分成两段,其中一段有我们想要的数据,而另一端没有,因此可以将另一端完全排除。定义left 和 right 分别指向数组左端点和右端点。mid根据查找的下标为左还是右决定。此处mid = left + (right -left)/2

:那该如何排除我们不想要的数据呢?

答:我们来看看示例一,题目有两个数据满足我们的要求,如果要按上述将数据分成有用段和无用段那么只有两种方法(数据是连续的),要么排除所有比8大的,要么排除所有比8小的,因为我们要的是左下标,因此排除比8小的

问:为什么排除所有比8小的,能够得到左下标,而排除所有比8大的,能得到右下标?

        当 nums[mid] < 8 时,此时 mid 落在了左端部分,因为左端部分所有数都比 8 小,所以left = mid + 1,此时 left 下标处的值满足两种情况 1、小于 target; 2、等于target 

        当 nums[mid] ≥ 8 时,此时mid 落在了右端部分,则 right = mid,此时right也有两种情况 1、大于 target; 2、等于target

☆☆☆ :右端部分的target可能是多个 target 中的任意一个,不满足我们所要的最左端,因此从这里就能看出,为什么排除所有比8小的,才能的到最左端下标。 同理找右下标

细节:外循环的结束条件(left < right)

问:取不取等号?

答:不取,如图所示(找左下标时)

分析:按照上面的步骤最终我们可以得到上图,此时nums[mid] = 8 ,因为找的是左下标,所以此时 left 不同,right = mid,得到下图:

若取等号再次进入循环,则 mid == 8 ,right == left 进入死循环,因此不能取等号。 

总结:

👉:每次二分的点只有一个,但是一组数据可能用到多次二分算法来确定最终结果

👉:二分算法的难点在于处理边界问题,稍有不慎就有可能陷入死循环

代码实现:

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

        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)
        {
            return {tmp,left};
        }
        else
        {
            return {-1,-1};
        }
    }

二:x的平方根

题目要求:

解题思路:

以示例2为例

思路

按照上述思路执行时,最终会来到如图所示的结果,此时 left = right 跳出循环, left 所在位置即为最终答案

代码实现:

 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;
    }

三:寻找山峰值

题目要求:

解题思路:

以下述图片为例:
寻找数据二段性是解题关键。

该数据段的特点是:某一段的数据是递增,有一段是递减,

即某一段 nums[n] < nums[n+1],另一段 nums[n] > nums[n+1]

于是我们便可以通过比较 nums[n]与nums[n+1] 的大小关系来判断当前处于哪一段上

👉当 nums[n] < nums[n+1]时,n+1 左端的数据可以全部舍去,因此 left = mid + 1

👉:当 nums[n] ≥ nums[n+1]时,n 对应位置处可能刚好是目标值,因此 right = mid

代码实现:

   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;
    }

四:寻找旋转排序数组中的最小值

题目要求:

解题思路:

个人见解:

这道题不像前面三道二段性那么好找,做了这道题之后,对于博主而言,二分算法还有一个容易忽视的地方,那就是:如何找到正确的参考点!

第一、二中的参考点是题目给的,第三问的参考点是mid位置处的下一个或者是上一个位置处的数据,但是这题的参考点并非一、二、三问那样。

之前我们都是在数组中去观察数据,这道题当中,我们不妨换一个角度去看问题,首先明确这段数据的变化规律是:一段有序的数组,将他的尾部数据依次从后向前插入到头部,整个数据被分成两段递增的数据段,且没有重复的数据。

以示例2为例:

我们可以得到如下图片。

上面我们说了,直接观察数组中的数据很难发现二段性,我们不妨如下绘制图片

以数组末尾数据2为参考点,可以发现:

👉:当nums[mid] > 2 时,说明此时 mid 落在左上角处,这里没有我们想要的答案

👉:当nums[mid] ≤ 2 时,说明此时 mid 落在右下角处,这里有我们要的答案

当我们弄清楚数据的二段性时,再回到数组,想想 left 和 right 的更新方式。

👉:当 mid 位置处的数据 > 2 ,那么此时 mid+1 左侧的数据都大于 2,因此 left=mid+1

:别忘了这原本就是从一个有序数组变过来的,首端数据是大于末端数据的!

👉:当 mid 位置处的数据 ≤ 2 ,那么此时 mid 左侧包含 mid 可能存在答案,因此 right=mid

代码实现:

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

五:0~n-1中缺失的数字

题目要求:

解题思路:

以示例2为例,我们可以得到如下图片:

仔细观察不难发现:假设缺失的数字为 x,那么x前面的数字都和下标相等,而x后的数字都大于下标,因此左右指针有这样的更新条件:

👉:当 num[i] == i 时,缺失的数字不可能在 i 的左侧,包括i,那么 left = mid + 1;

👉:当 num[i]  > i 时, 缺失的数字可能不在i的右侧,也可能刚好是 i,那么right = mid;

这道题还有一个边界问题需要考虑,我们来看以下这个情况(缺失数字为8,不在数组中):

当我们按着上述的思路,最后一步会执行到如图所示处,此时跳出循环,但是left并非目标值,因此还需多做一步判断:

👉:若arr[left] = left,说明此时缺失值不在数组中,而在下一个位置。

👉:若arr[left] != left,说明当前位置即为缺失值。

代码实现:

    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(records[mid] == mid)
            {
                left = mid + 1;
            }else
            {
                right = mid;
            }
        }
        if(records[left] == left)
        {
            return left+1;
        }
        else
        {
            return left;
        }
            
    }


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

相关文章:

  • 硬件电路基础
  • LabVIEW涡轮诊断系统
  • 深入浅出:频谱掩码 Spectral Masking —— 噪音消除利器
  • 如何创建折叠式Title
  • 第28节课:前端项目实战—从需求分析到开发流程的全方位指南
  • VSCode源码分析参考资料
  • Linux的SSH远程管理及安全配置
  • [OpenHarmony5.0][Docker][环境]OpenHarmony5.0 Docker pull线上镜像方式构建编译环境
  • ESP32-S3模组上跑通ES8388(8)
  • android studio Terminal控制台命令打包 apk
  • 0.shell 脚本执行方式
  • 零基础Python学习
  • centos7.6升级cmake+编译pcm工具
  • Mybatis:CRUD数据操作之单个条件(动态SQL)
  • FreeRTOS 软件定时器
  • Selenium 自动化测试demo
  • 【K230 CanMV】图像识别-摄像头获取图像 Sensor 函数全解析
  • 开源法律、政策和实践
  • ArcGIS栅格影像裁剪工具
  • R安装rgdal报错 解决办法
  • Android 引入 proto 项目及使用方法
  • 网络安全相关证书资料
  • linux环境下,导出conda和pip的安装包和对应版本
  • solana java 转账交易示例
  • 前端用原生js下载File对象文件,多用于上传附件时,提交之前进行点击预览,或打开本地已经选择待上传的附件列表
  • DDR3保姆级使用教程:ZYNQ 7010