当前位置:   article > 正文

数据结构与算法——二分查找_二分查找 开闭区间

二分查找 开闭区间

前言:

作为查找板块中最重要的算法和思想,二分查找是典型的一看就会,一做就废。要不要加=?要不要+1?要不要-1?这是二分查找最让人头痛的地方,作为一个思想不难,细节制胜的算法,拒绝死记硬背,本文将详细解析它的算法思路和原理。

目录

一、查找确切值

 1、左闭右闭区间

2、左闭右开区间

二、寻找左侧边界(最大值最小化or在单调序列中找x或其前驱)

三、寻找右侧边界(最小值最大化or在单调序列中找x或其后继)

四、二分查找的经典情景应用

五、二分的好处和局限性

1、好处

2、局限性

六、总结


一、查找确切值

 1、左闭右闭区间

顾名思义,该方法是在一段区间内寻找target目标值,并且左边界和右边界都可以取到。

左边界和右边界都可以取到,看似非常简单,但是是本方法控制边界和细节的关键所在。

  1. int binarySearch(vector<int> &nums, int target) {
  2. //初始化区间
  3. int left = 0, right = nums.size() - 1;
  4. while (left <= right)
  5. {
  6. //小细节:为什么不直接用(left+right)/2
  7. //这就涉及到程序执行先后顺序的问题了
  8. //直接先i+j的话有可能发生整数溢出的问题
  9. //而下面这种方法便可巧妙地一定程度上规避这种问题
  10. int mid = left + (right - left) / 2;
  11. if (nums[mid] < target)
  12. left = mid + 1;
  13. else if (nums[mid] > target)
  14. right = mid - 1;
  15. else
  16. return mid;
  17. }
  18. // 未找到目标元素,返回 -1
  19. return -1;
  20. }
  • 因为是双闭区间,左右边界的值都可以取到,所以我们在取左右边界值时分别取它可以取到的最小和最大值,即left = 0, right = nums.size() - 1;
  • 因为是双闭区间,左右边界的值都可以取到,所以我们在缩小边界时左右边界都要进行移动

1.当target>nums[mid]时,说明target在[mid+1,right]上,因此left=mid+1;

2.当target<nums[mid]时,说明target在[left,mid-1]上,因此right=mid-1;

3.当target=nums[mid]时,说明已找到,返回target;

  • 为什么时left<=right?

同样,还是因为它是双闭区间,left=right时也可以存在区间[left,right]

看到这里可能还是理解的不太深刻,看接下来这个对比例子就明白了

2、左闭右开区间

  1. int binarySearch(vector<int> &nums, int target) {
  2. //初始化区间
  3. int left = 0, right = nums.size();
  4. while (left < right)
  5. {
  6. //小细节:为什么不直接用(left+right)/2
  7. //这就涉及到程序执行先后顺序的问题了
  8. //直接先i+j的话有可能发生整数溢出的问题
  9. //而下面这种方法便可巧妙地一定程度上规避这种问题
  10. int mid = left + (right - left) / 2;
  11. if (nums[mid] < target)
  12. left = mid + 1;
  13. else if (nums[mid] > target)
  14. right = mid ;
  15. else
  16. return mid;
  17. }
  18. // 未找到目标元素,返回 -1
  19. return -1;
  20. }
  • 因为左闭右开,右边界的值不能确切取到,所以在设置right时要+1
  • 同样,还是因为左闭右开,右边界的值不能确切取到,所以在缩小区间时,right不用-1,反正它也取不到,直接等于mid即可,其实仔细想想效果是一样的
  • 同样,还还是因为左闭右开,右边界的值不能确切取到,所以left=right时不可能存在区间[left,right),自然也就无法继续了,故循环条件为left<right而没有=

根据上面的对比分析,我们可以发现,它的细节其实原理非常简单,一切细节的设计都是围绕区间的开闭展开,不过要特别说明的是,我们通常更习惯的是采用左闭右闭的双闭区间形式,这样左右都是对称操作的,更不容易出错。

本文下面提到的算法均采用双闭区间的形式。

二、寻找左侧边界(最大值最小化or在单调序列中找x或其前驱)

  1. int binarySearchInsertion(vector<int> &nums, int target) {
  2. int l = 0, r = nums.size() - 1;
  3. while (l <= r) {
  4. int m = l + (r - l) / 2;
  5. if (nums[m] < target) {
  6. l = m + 1;
  7. } else if (nums[m] > target) {
  8. r = m - 1;
  9. } else {
  10. l = m - 1;
  11. }
  12. }
  13. return l;
  14. }
  • 当target>nums[m]或target<nums[m]时,说明还没有找到target,因此仍采用普通二分区间的缩小区间操作,使指针l和r向target靠近
  • 当nums[m]==target时,使r向小于target的元素靠近,因此用r=m-1来缩小区间(因为此处是寻找左侧边界,所以缩小边界时向左缩小)
  • 循环结束后,l指向最左边的target,r指向首个小于target的元素,所以l就是我们的左侧边界

不难发现,二分查找无非就是给指针i和j分别设定搜索目标,在不断的循环二分中,让l和r都逐渐逼近预先设定的目标,总的来说就是一个不断向目标奔赴的过程。

三、寻找右侧边界(最小值最大化or在单调序列中找x或其后继)

  1. int binarySearchInsertion(vector<int> &nums, int target) {
  2. int l = 0, r = nums.size() - 1;
  3. while (l <= r) {
  4. int m = l + (r - l) / 2;
  5. if (nums[m] < target) {
  6. l = m + 1;
  7. } else if (nums[m] > target) {
  8. r = m - 1;
  9. } else {
  10. l = m +1;
  11. }
  12. }
  13. return r;
  14. }
  • 与找左侧边界的思想相同,在没有找到时都是进行普通的二分区间操作缩小区间,唯一变化的两个地方在于:
  • ①当nums[m]==target时,使l向大于target的元素靠近,因此用l=m+1来缩小区间(因为此处是寻找右侧边界,所以缩小边界时向右缩小)
  • ②循环结束后,r指向最右边的target,l指向首个大于target的元素,所以r就是我们的左侧边界

四、二分查找的经典情景应用

通过上面三种算法的介绍,我们已经初步掌握了二分的思想,并且这三种方法可以用来解决绝大部分二分的题目,不过要注意巧妙灵活的变形和运用

下面介绍二分的一种经典应用思想:
当我们容易得出结果具有单调性时,可以先从小到大假设结果,然后设置一个check函数看该答案是否能使题目成立,主要用到的二分思想是寻找左侧边界和右侧边界,因为题目通常是问最小值或最大值,下面是该思想的几道经典题目:

1.扫地机器人 - 蓝桥云课 (lanqiao.cn)

1.跳石头 - 蓝桥云课 (lanqiao.cn)

[蓝桥杯 2017 省 AB] 分巧克力 - 洛谷

1631. 最小体力消耗路径 - 力扣(LeetCode)

五、二分的好处和局限性

1、好处

  • 二分的查找效率非常高,每次都砍掉一半不可能的值,定量分析也可以得出
  • 无需额外空间

2、局限性

  • 仅限于有序数据
  • 仅适用于顺序表,不能用于链表
  • 当数据量较小时,线性查找反而比二分查找更快

六、总结

       本文详细介绍了二分的细节实现,这是一种很重要的思想,一定要多加练习,想不清楚时多画图进行实践验证。

参考资料

10.1   二分查找 - Hello 算法 (hello-algo.com)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/正经夜光杯/article/detail/1001768
推荐阅读
相关标签
  

闽ICP备14008679号