文章介绍了二分搜索最常见的几个场景的使用:寻找一个数寻找左侧边界以及寻找右侧边界。阅读本文只需读者了解二分搜索的使用限制和基本原理即可。

我相信,友好的讨论交流会让彼此快速进步!文章难免有疏漏之处,十分欢迎大家在评论区中批评指正。

发现更多好文,请前往柿子先生的博客。


寻找一个数

搜索一个数,如果存在,返回其索引,否则返回 -1。

int binarySearch(int[] nums, int target) {// 搜索区间 [left, right]int left = 0, right = nums.length - 1;while (left <= right) { // !!!int mid = left + ((right - left) >> 1);if (nums[mid] == target) {return mid; // !!!} else if (nums[mid] < target) { // 搜索区间变为 [mid+1, right]left = mid + 1; // !!!} else { // 搜索区间变为 [left, mid-1]right = mid - 1; // !!!}}return -1; // !!!
}

计算 mid 时需要防止溢出,left + ((right - left) >> 1)(left + right) / 2 的结果相同,但是有效防止了 leftright 太大,直接相加导致溢出的情况。

  • 搜索区间: [left, right],因为 right 初始化时是 nums.length - 1,即最后一个元素的索引。

  • 停止搜索: nums[mid] == target,找到目标值即停止。如果没有找到,while 循环终止,并返回 -1。

  • 循环终止: 搜索区间为空的时候,循环终止。while (left <= right) 的循环终止条件是当 left 的值为 right + 1 时,写成区间形式就是 [right + 1, right],此时搜索区间为空。此时,while 循环终止是正确的,直接返回 -1 即可。

  • 区间移动: 在搜索区间为 [left, right] 时,若索引 mid 上的元素不是要找的 target 时,要去 [left, mid - 1][mid + 1, right] 区间上搜索,因为 mid 已经被搜索过了,应当从搜索区间中删除

  • 代码缺陷: 无法找到升序数组中存在多个目标值的左右边界索引情况。假设有升序数组 nums = [1, 2, 3, 3, 3, 3, 3],target 为 3,该算法返回的索引是 3。如果此时,我想得到 target 的左侧边界索引,即 2,或者想得到 target 的右侧边界,即 6,上述代码是无法处理的。

下面,我们来学习如何使用二分搜索找到目标值的左右边界。


寻找左侧边界的二分搜索

int searchLeftBound(int[] nums, int target) {int left = 0, right = nums.length - 1;while (left <= right) {int mid = left + ((right - left) >> 1);if (nums[mid] == target) {right = mid - 1; // 收缩右边界} else if (nums[mid] < target) {left = mid + 1;} else {right = mid - 1;}}// 越界检查,不存在检查if (left >= nums.length || nums[left] != target) {return -1;}return left;
}

1. 为什么能够搜索左侧边界?

关键点在于 nums[mid] == target 时的处理。

if (nums[mid] == target) {right = mid - 1; // 收缩右边界
}

当找到 target 时,不立即返回,而是缩小搜索区间的右边界 right,在区间 [left, mid - 1] 中继续搜索,也就是不断向左靠拢,达到锁定左侧边界的目的。

2. 为什么最终返回的是 left 而不是 right

while (left <= right) 的循环终止条件是 left == right + 1 ,因此左边界的索引值一定是在 left == right 时出现的。然而此时,循环无法停止,right 还要继续收缩,因此只能返回左边界的索引值 left

3. 为什么越界检查只检查左边界 left

我们最终返回的是左边界索引 left,因此只需校验左边界 left 最终是否合法即可,另一方面,由于 nums[mid] == target 时,right = mid - 1; 这样 right 在很多情况下都会越界(比如,左边界的索引为 0 时),校验其是否合法没有意义,还会导致返回错误。


寻找右侧边界的二分查找

int searchRightBound(int[] nums, int target) {int left = 0, right = nums.length - 1;while (left <= right) {int mid = left + ((right - left) >> 1);if (nums[mid] == target) {left = mid + 1; // 收缩左边界} else if (nums[mid] < target) {left = mid + 1;} else {right = mid - 1;}}if (right < 0 || nums[right] != target) {return -1;}return right;
}

1. 为什么能够搜索右侧边界?

关键点在于 nums[mid] == target 时的处理。

if (nums[mid] == target) {left = mid + 1; // 收缩左边界
}

当找到 target 时,不立即返回,而是增大搜索区间的左边界 left,在区间 [mid + 1, right] 中继续搜索,也就是不断向右靠拢,达到锁定右侧边界的目的。

2. 为什么最终返回的是 right 而不是 left

while (left <= right) 的循环终止条件是 left == right + 1 ,而右边界的索引值一定是在 left == right 时出现的。然而此时,循环无法停止,left 还要继续增大,因此只能返回右边界的索引值 right

3. 为什么越界检查只检查右边界 right

我们最终返回的是右边界索引 right,因此只需校验右边界 right 最终是否合法即可,另一方面,由于 nums[mid] == target 时,left = mid + 1; 这样 left 在很多情况下都会越界(比如,右边界的索引为 0 时),校验其是否合法没有意义,还会导致返回错误。


逻辑统一

首先,我们先来梳理一下,需要统一哪些内容?

1. 搜索区间:[left, right],即搜索区间左右皆闭合。

2. 循环条件:left <= right,因此,终止条件为 left == right + 1

3. 收缩区间: 寻找左边界,找到目标值后,收缩 right,即 right = mid - 1;寻找右边界,找到目标值后,扩大 left,即 left = mid + 1

4. 越界校验: 寻找左边界,跳出循环后,校验 left;寻找右边界,跳出循环后,校验 right

5. 返回值: 寻找左边界,返回 left;寻找右边界,返回 right

再次回顾之前的代码:

寻找一个数

int binarySearch(int[] nums, int target) {int left = 0, right = nums.length - 1; // !!!while (left <= right) { // !!!int mid = left + ((right - left) >> 1);if (nums[mid] == target) {return mid; // !!!} else if (nums[mid] < target) {left = mid + 1;} else {right = mid - 1;}}return -1; // !!!
}

寻找左边界

int searchLeftBound(int[] nums, int target) {int left = 0, right = nums.length - 1; // !!!while (left <= right) { // !!!int mid = left + ((right - left) >> 1); if (nums[mid] == target) {right = mid - 1; // !!!} else if (nums[mid] < target) {left = mid + 1;} else {right = mid - 1;}}if (left >= nums.length || nums[left] != target) { // !!!return -1;}return left; // !!!
}

寻找右边界

int searchRightBound(int[] nums, int target) {int left = 0, right = nums.length - 1; // !!!while (left <= right) { // !!!int mid = left + ((right - left) >> 1); if (nums[mid] == target) { left = mid + 1; // !!!} else if (nums[mid] < target) {left = mid + 1;} else {right = mid - 1;}}if (right < 0 || nums[right] != target) { // !!!return -1;}return right; // !!!
}

写在最后,二分搜索最有价值的思想在于,通过已知信息尽可能多地收缩(折半)搜索空间,从而提高穷举效率,快速找到目标。


实战一下

力扣-704-二分查找

题目描述:

参考代码:

class Solution {public int search(int[] nums, int target) {int left = 0, right = nums.length - 1;while (left <= right) {int mid = left + ((right - left) >> 1);if (nums[mid] == target)return mid;else if (nums[mid] < target)left = mid + 1;elseright = mid - 1;}return -1;}
}

力扣-34-在排序数组中查找元素的第一个和最后一个位置

题目描述:

参考代码:

class Solution {public int[] searchRange(int[] nums, int target) {return new int[] {searchLeftBound(nums, target), searchRightBound(nums, target)};}    int searchLeftBound(int[] nums, int target) {int left = 0, right = nums.length - 1;while (left <= right) {int mid = left + ((right - left) >> 1);if (nums[mid] == target) {right = mid - 1;} else if (nums[mid] < target) {left = mid + 1;} else {right = mid - 1;}}if (left >= nums.length || nums[left] != target) {return -1;}return left;} int searchRightBound(int[] nums, int target) {int left = 0, right = nums.length - 1;while (left <= right) {int mid = left + ((right - left) >> 1);if (nums[mid] == target) {left = mid + 1;} else if (nums[mid] < target) {left = mid + 1;} else {right = mid - 1;}}if (right < 0 || nums[right] != target) {return -1;}return right;}
}

剑指 Offer 53 - I. 在排序数组中查找数字 I

题目描述:

参考代码:

class Solution {public int search(int[] nums, int target) {int L = leftBound(nums, target);int R = rightBound(nums, target);if ( L == -1 || R == -1) {return 0;} else {return R - L + 1;}}int leftBound(int[] nums, int target) {int left = 0, right = nums.length - 1;while (left <= right) {int mid = left + ((right - left) >> 1);if (nums[mid] == target) {right = mid - 1;} else if (nums[mid] < target) {left = mid + 1;} else {right = mid - 1;}}if (left >= nums.length || nums[left] != target)return -1;return left;}int rightBound(int[] nums, int target) {int left = 0, right = nums.length - 1;while (left <= right) {int mid = left + ((right - left) >> 1);if (nums[mid] == target) {left = mid + 1;} else if (nums[mid] < target) {left = mid + 1;} else {right = mid - 1;}}if (right < 0 || nums[right] != target)return -1;return right;}
}

写在最后

文章主体内容在《labuladong 的算法小抄》的基础上进行了个性化增删,文章围绕 「通解框架」 这一核心,删除了许多个人认为不必要的内容,大幅缩减了文章篇幅。同时,在使用二分搜索寻找左右侧边界问题上提出并回答了几个比较关键的问题,最后在逻辑统一部分提炼了几个关键点,更加有助于通解框架的理解与记忆。


参考资料

  1. labuladong 的算法小抄
  2. 力扣网

二分搜索算法通解框架相关推荐

  1. Java1.使用二分搜索算法查找任意N个有序数列中的指定元素。 2.通过上机实验进行算法实现。 3.保存和打印出程序的运行结果,并结合程序进行分析,上交实验报告。 4.至少使用两种方法进行编程,直接查

    1.使用二分搜索算法查找任意N个有序数列中的指定元素. 2.通过上机实验进行算法实现. 3.保存和打印出程序的运行结果,并结合程序进行分析,上交实验报告. 4.至少使用两种方法进行编程,直接查找/递归 ...

  2. C++——《算法分析与设计》实验报告——二分搜索算法

    实验名称: 二分搜索算法 实验地点: 实验目的: 理解分治算法的概念和基本要素: 理解递归的概念: 掌握设计有效算法的分治策略: 通过二分搜索技术学习分治策略设计技巧: 实验原理: 二分搜索算法也称为 ...

  3. C++——《算法分析》实验壹——二分搜索算法

    实验目的: 1.理解分治算法的概念和基本要素: 2.理解递归的概念: 3.掌握设计有效算法的分治策略: 4.通过二分搜索技术学习分治策略设计技巧: 实验原理: 二分搜索算法也称为折半查找法,它充分利用 ...

  4. this.$set 更新整个数组_学点算法(二)——有序数组二分搜索算法

    成功与失败 今天来学习一下二分搜索算法.二分搜索算法针对有序数组,如果数组乱序,则无法使用二分搜索法. 先来看一下二分搜索算法的运行原理: 判断区间是否有效,无效区间则退出循环. 取待查找区间的中间位 ...

  5. 改写二分搜索算法C++

    改写二分搜索算法,使得当搜索元素x不在数组中时,返回小于x的最大元素位置i和大于x的元素位置j.当搜索元素在数组中时,i和j相同,均为x在数组中的位置. //二分查找 #include <bit ...

  6. 7-2 改写二分搜索算法 (20 分)

    题目来源:<计算机算法设计与分析>,王晓东 设a[0:n-1]是已排好序的数组,请改写二分搜索算法,使得当x不在数组中时,返回小于x的最大元素位置i和大于x的最小元素位置j.当搜索元素在数 ...

  7. PTA:改写二分搜索算法

    改写二分搜索算法 题目: 代码如下: #include<iostream> using namespace std; int binarySearch(int arr[], int x, ...

  8. Python改写二分搜索算法

    题目来源:<计算机算法设计与分析>,王晓东 设a[0:n-1]是已排好序的数组,请改写二分搜索算法,使得当x不在数组中时,返回小于x的最大元素位置i和大于x的最小元素位置j.当搜索元素在数 ...

  9. 二分搜索算法(以力扣周赛“每个小孩最多分到多少糖果”为例)

    上午刚刚打完力扣周赛,感觉心有余而力不足,好多次都有这么一种差一点就AC的感觉,所以归根结底还是自己基础不牢的缘故,借着有感记录下二分搜索算法的解法,也祭奠下自己第287场周赛的再次失败! 二分搜索算 ...

最新文章

  1. 安卓手机如何朗读屏幕_安卓手机的屏幕怎么投屏在电视大屏幕上
  2. mysql5.6更改datadir数据存储目录
  3. main函数第3个参数envp装的是什么(envp:环境变量)
  4. [Xcode 实际操作]七、文件与数据-(2)创建文件夹
  5. MySQL中常见的单行函数(下)
  6. 患者如何区分股癣和银屑病+药膏根治
  7. java务必让常量的值在运行期保持不变
  8. 调用exe文件(一般处理登陆安全窗口)+睡眠等待(--------------------)
  9. 红橙Darren视频笔记 自定义RatingBar touch事件学习 dp转px listener监听
  10. Promise基本概念和基本示例使用
  11. bin文件如何编辑_如何将PS中图片模糊文字(位图)转换为AI的高清矢量图和CDR文件相关编辑?...
  12. MRI T1、T2mapping
  13. 618什么数码产品值得入手、好评超高的数码产品排行榜
  14. 红帽linux如何装软件,redhat 下软件的安装
  15. qq第三方登录所需appid
  16. java poi pdf 导出
  17. 新概念二册 Lesson 46 Expensive and uncomfortable既昂贵又受罪(同位语从句+doing/being done+介词后的宾语从句)
  18. iOS截屏后仿今日头条实现一键分享
  19. 加索引如何避免锁表?
  20. 探秘Kotlin协程机制

热门文章

  1. hotmail域名_英国短信服务商以131万元收购域名it.co.uk
  2. OpenTX学习笔记
  3. 新手安装WIN7与ubuntu双系统 安装记录
  4. table 斜线表头
  5. 看这里,全网最详细的Sonar代码扫描平台搭建教程
  6. 怎么样查杀主页劫持木马,劫持浏览器首页的驱动木马解决方法
  7. 软考高级 真题 2017年下半年 信息系统项目管理师 案例分析
  8. 项目二:绘制数学函数图像
  9. psd文件一键瘦身脚本使用详细步骤
  10. 【Windows】手工删除启动项