【算法详解】二分查找

1. 二分查找算法介绍

「二分查找算法(Binary Search Algorithm)」:也叫做 「折半查找算法」「对数查找算法」。是一种在有序数组中查找某一特定元素的搜索算法。

基本算法思想:先确定待查找元素所在的区间范围,在逐步缩小范围,直到找到元素或找不到该元素为止。

二分查找算法的过程如下所示:

  1. 每次查找时从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;
  2. 如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。
  3. 如果在某一步骤数组为空,则代表找不到。

举个例子来说,给定一个有序数组 [0, 1, 2, 3, 4, 5, 6, 7, 8]。如果我们希望查找 5 是否在这个数组中。

  1. 第一次区间为整个数组 [0, 1, 2, 3, 4, 5, 6, 7, 8],中位数是 4,因为 4 小于 5,所以如果 5 存在在这个数组中,那么 5 一定在 4 右边的这一半区间中。于是我们的查找范围变成了 [4, 5, 6, 7, 8]
  2. 第二次区间为 [4, 5, 6, 7, 8],中位数是 6,因为 5 小于 6,所以如果 5 存在在这个数组中,那么 5 一定在 6 左边的这一半区间中。于是我们的查找范围变成了 [4, 5, 6]
  3. 第三次区间为 [4, 5, 6],中位数是 5,正好是我们需要查找的数字。

于是我们发现,对于一个长度为 9 的有序数组,我们只进行了 3 次查找就找到了我们需要查找的数字。而如果是按顺序依次遍历数组,则最坏情况下,我们需要查找 9 次。

二分查找过程的示意图如下所示:

2. 二分查找算法思想

二分查找算法是经典的 「减而治之」 的思想。

这里的 「减」 是减少问题规模的意思,「治」 是解决问题的意思。「减」「治」 结合起来的意思就是 「排除法解决问题」。即:每一次查找,排除掉一定不存在目标元素的区间,在剩下可能存在目标元素的区间中继续查找。

每一次通过一些条件判断,将待搜索的区间逐渐缩小,以达到「减少问题规模」的目的。而于问题的规模是有限的,经过有限次的查找,最终会查找到目标元素或者查找失败。

3. 二分查找细节

从上面的例子中我们了解了二分查找的思路和具体代码。但是真正在解决二分查找题目的时候还是需要考虑很多细节的。比如说以下几个问题:

  1. 区间的开闭问题:区间应该是左闭右闭,还是左闭右开?
  2. mid 的取值问题mid = (left + right) // 2,还是 mid = (left + right + 1) // 2
  3. 出界条件的判断left <= right,还是 left < right
  4. 搜索区间范围的选择left = mid + 1right = mid - 1left = mid right = mid 应该怎么写?

下面一一进行讲解。

3.1 区间的开闭问题

区间的左闭右闭、左闭右开指的是初始待查找区间的范围。

  • 左闭右闭:初始化赋值时,left = 0right = len(nums) - 1left 为数组第一个元素位置,right 为数组最后一个元素位置,从而区间 [left, right] 左右边界上的点都能取到。
  • 左闭右开:初始化赋值时,left = 0right = len(nums)left 为数组第一个元素位置,right 为数组最后一个元素的下一个位置,从而区间 [left, right) 左边界点能取到,而右边界上的点不能取到。

关于区间的左闭右闭、左闭右开,其实在网上都有对应的代码和解法。但是相对来说,左闭右开这种写法在解决问题的过程中,需要考虑的情况更加复杂,所以建议 全部使用「左闭右闭」区间

3.2 mid 的取值问题

在二分查找的实际问题中,最常见的 mid 取值就是 mid = (left + right) // 2 或者 mid = left + (right - left) // 2 。前者是最常见写法,后者是为了防止整型溢出。式子中 // 2 就代表的含义是中间数「向下取整」。当待查找区间中有偶数个元素个数时,则位于最中间的数为 2 个,这时候使用上面式子只能取到中间靠左边那个数,而取不到中间靠右边的那个数。那么,右边的那个数到底能取吗?

其实,右边的数也是可以取的,令 mid = (left + right + 1) // 2,或者 mid = left + (right - left + 1) // 2。这样如果待查找区间的元素为偶数个,就能取到中间靠右边的那个数了,把这个式子代入到 704. 二分查找 中试一试,发现也是能通过题目评测的。

这是因为二分查找的思路是根据每次选择中间位置上的数值来决定下一次在哪个区间查找元素。每一次选择的元素位置可以是中间位置,但并不是一定非得是区间中间位置元素,靠左一些、靠右一些、甚至区间三分之一、五分之一处等等,都是可以的。比如说 mid = left + (right - left + 1) * 1 // 5 也是可以的。

但一般来说,取中间位置元素在平均意义下所达到的效果最好。同时这样写最简单。而对于 mid 值是向下取整还是向上取整,大多数时候是选择不加 1。但有些写法中,是需要考虑加 1 的,后面会讲解这种写法。

3.3 出界条件的判断

我们经常看到二分查找算法的写法中,while 语句出界判断的语句有left <= rightleft < right 两种写法。那我们究竟应该在什么情况用什么写法呢?

这就需要判断一下导致 while 语句出界的条件是什么。

  • 如果判断语句为 left <= right,且查找的元素不存在,则 while 判断语句出界条件是 left == right + 1,写成区间形式就是 [right + 1, right],此时待查找区间为空,待查找区间中没有元素存在,所以此时终止循环可以直接返回 -1 是正确的。
    • 比如说区间 [3, 2],不可能存在一个元素既大于等于 3 又小于等于 2,此时直接终止循环,返回 -1 即可。
  • 如果判断语句为left < right,且查找的元素不存在,则 while 判断语句出界条件是 left == right,写成区间形式就是 [right, right]。此时区间不为空,待查找区间还有一个元素存在,并不能确定查找的元素不在这个区间中,此时终止循环返回 -1 是错误的。
    • 比如说区间 [2, 2],元素 2 就属于这个区间,此时终止循环,返回 -1 就漏掉了这个元素。

但是如果我们还是想要使用 left < right 的话,怎么办?

可以在返回的时候需要增加一层判断,判断 left 所指向位置是否等于目标元素,如果是的话就返回 left,如果不是的话返回 -1。即:

// ...
while (left < right) {// ...
}
return nums[left] == target ? left : -1;

此外,循环语句用 left < right 还有一个好处,就是在退出循环的时候,一定有 left == right,我们就不用判断应该返回 left 还是 right 了。

3.4 搜索区间范围的选择

在进行区间范围选择的时候,有时候是 left = mid + 1right = mid - 1,还有的时候是 left = mid + 1 right = mid,还有的时候是 left = midright = mid - 1。那么我们到底应该如何确定搜索区间范围呢?

这是二分查找的一个难点,写错了很容易造成死循环,或者得不到正确结果。

这其实跟二分查找算法的两种不同思路有关。

  • 思路 1:「直接找」—— 在循环体中找到元素后直接返回结果。
  • 思路 2:「排除法」—— 在循环体中排除目标元素一定不存在区间。

4. 查找的三种常见模板

4.1 基础二分

思路 1:「直接找」

第 1 种思路:一旦我们在循环体中找到元素就直接返回结果。

这种思路比较简单,其实我们在上边 「3. 简单二分查找 - 704. 二分查找」 中就已经用过了。这里再看一下思路和代码:

思路:

  • 取两个节点中心位置 mid,先看中心位置值 nums[mid]

    • 如果中心位置值 nums[mid] 与目标值 target 相等,则 直接返回 这个中心位置元素的下标。
    • 如果中心位置值 nums[mid] 小于目标值 target,则将左节点设置为 mid + 1,然后继续在右区间 [mid + 1, right] 搜索。
    • 如果中心位置值 nums[mid] 大于目标值 target,则将右节点设置为 mid - 1,然后继续在左区间 [left, mid - 1] 搜索。
      二分查找的基础模板,适用于可以通过访问数组中单个索引来确定元素或条件的情况。
int binarySearch(vector<int>& nums, int target) {if (nums.size() == 0) return -1;int left = 0, right = nums.size() - 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
  • 如果一旦退出循环,则说明这个区间内一定不存在目标元素。

4.2 排除法

思路 2:「排除法」

第 2 种思路:在循环体中排除目标元素一定不存在区间。

思路:

  • 取两个节点中心位置 mid,根据判断条件先将目标元素一定不存在的区间排除。
  • 然后在剩余区间继续查找元素,继续根据条件排除不存在的区间。
  • 直到区间中只剩下最后一个元素,然后再判断这个元素是否是目标元素。

根据第二种排除法的思路,我们可以写出来两种代码。

  1. 寻找左端点
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
//寻找左边界
//找到 ≥target的最小值
int search(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1;// 在区间 [left, right] 内查找 targetwhile (left < right) {// 取区间中间节点int mid = left + (right - left) / 2;// nums[mid] 小于目标值,排除掉不可能区间 [left, mid],在 [mid + 1, right] 中继续搜索if (nums[mid] < target) {left = mid + 1;// nums[mid] 大于等于目标值,目标元素可能在 [left, mid] 中,在 [left, mid] 中继续搜索} else {right = mid;}}// 判断区间剩余元素是否为目标元素,不是则返回 -1return nums[left] == target ? left : -1;}
  1. 寻找右端点
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
//寻找右边界
//找到 ≤target 的最大值int search(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1;// 在区间 [left, right] 内查找 targetwhile (left < right) {// 取区间中间节点int mid = left + (right - left + 1) / 2;// nums[mid] 大于目标值,排除掉不可能区间 [mid, right],在 [left, mid - 1] 中继续搜索if (nums[mid] > target) {right = mid - 1;// nums[mid] 小于等于目标值,目标元素可能在 [mid, right] 中,在 [mid, right] 中继续搜索} else {left = mid;}}// 判断区间剩余元素是否为目标元素,不是则返回 -1return nums[left] == target ? left : -1;}

细节:

  • 判断语句是 left < right。这样在退出循环时,一定有left == right 成立,就不用判断应该返回 left 还是 right 了。同时方便定位查找元素的下标。但是一定要注意最后要对区间剩余的元素进行一次判断。
  • 在循环体中,优先考虑 nums[mid] 在什么情况下一定不是目标元素,排除掉不可能区间,然后再从剩余区间中确定下一次查找区间的范围。
  • 在考虑 nums[mid] 在什么情况下一定不是目标元素之后,它的对立面(即 else 部分)一般就不需要再考虑区间范围了,直接取上一个区间的反面区间。如果上一个区间是 [mid + 1, right],那么相反面就是 [left, mid]。如果上一个区间是 [left, mid - 1],那么相反面就是 [mid, right]
  • 当区分被分为 [left, mid - 1][mid, right] 两部分时,mid 取值要向上取整。即 mid = left + (right - left + 1) // 2。因为如果当区间中只剩下两个元素时(此时 right = left + 1),一旦进入 left = mid 分支,区间就不会再缩小了,下一次循环的查找区间还是 [left, right],就陷入了死循环。
  • 关于边界设置可以记忆为:只要看到 left = mid 就向上取整。或者记为:
    • left = mid + 1right = midmid = left + (right - left) /2 一定是配对出现的。
    • right = mid - 1left = midmid = left + (right - left + 1) / 2 一定是配对出现的。

4.3 两种思路适用范围

  • 二分查找的思路 1:因为判断语句是 left <= right,有时候要考虑返回是 left 还是 right。循环体内有 3 个分支,并且一定有一个分支用于退出循环或者直接返回。这种思路适合解决简单题目。即要查找的元素性质简单,数组中都是非重复元素,且 ==>< 的情况非常好写的时候。
  • 二分查找的思路 2:更加符合二分查找算法的减治思想。每次排除目标元素一定不存在的区间,达到减少问题规模的效果。然后在可能存在的区间内继续查找目标元素。这种思路适合解决复杂题目。比如查找一个数组里可能不存在的元素,找边界问题,可以使用这种思路。

5. 题目描述

给定一个按照升序排列的长度为n的整数数组,以及 q 个查询。对于每个查询,返回一个元素k的起始位置和终止位置(位置从0开始计数)。如果数组中不存在该元素,则返回“-1 -1”。

输入格式

  • 第一行包含整数n和q,表示数组长度和询问个数。
  • 第二行包含n个整数(均在1~10000范围内),表示完整数组。
  • 接下来q行,每行包含一个整数k,表示一个询问元素。

输出格式

  • 共q行,每行包含两个整数,表示所求元素的起始位置和终止位置。如果数组中不存在该元素,则返回“-1 -1”。

数据范围

  • 1 ≤ n ≤ 100000
  • 1 ≤ q ≤ 10000
  • 1 ≤ k ≤ 10000

输入样例

6 3
1 2 2 3 3 4
3
4
5

输出样例

3 4
5 5
-1 -1

题解思路

这道题可以使用二分查找来解决。我们首先实现两个二分查找函数,一个用于找到元素k的起始位置,另一个用于找到元素k的终止位置。然后,对于每个查询,我们使用这两个函数分别找到起始位置和终止位置,并输出结果。

参考代码

#include<iostream>
using namespace std;int n, q;
const int N = 100010;
int a[N];int binary_search(int k) {int l = 0, r = n - 1;while (l < r) {int mid = l + r >> 1;if (a[mid] < k) l = mid + 1;else r = mid;}return l;
}int binary_search2(int k) {int l = 0, r = n - 1;while (l < r) {int mid = l + r + 1 >> 1;if (a[mid] > k) r = mid - 1;else l = mid;}return l;
}int main() {scanf("%d%d", &n, &q);for (int i = 0; i < n; i++)scanf("%d", &a[i]);while (q--) {int temp;scanf("%d", &temp);int p = binary_search(temp);int q = binary_search2(temp);if (a[p] == temp)cout << p << " " << q << endl;else cout << "-1 -1" << endl;}return 0;
}

这样,我们就完成了对这道题目的解答。通过这个例子,我们可以看到二分查找在处理有序数组时的应用,以及如何利用二分查找来解决一些问题。

5.二分查找总结

需要注意的是,不存在 target 的时候,直接返回 -1。在二分查找值时,返回条件是 nums[mid] == target 时直接 return,而查找左右侧边界时,返回条件则需要等 while() 循环完毕后,才能返回。观察下表可知,区间右侧开闭主要影响 right 的更新和 while 判断。

场景左闭右开 [left, right)左闭右闭 [left, right]备注
初始赋值left = 0, right = numsSizeleft = 0, right = numsSize - 1部分不同
while条件left < rightleft <= right不同
nums[mid] < targetleft = mid + 1left = mid + 1相同
nums[mid] > targetright = midright = mid - 1不同
nums[mid] == target返回 mid返回 mid相同

下面左右侧边界查找采用的是左闭右开区间,读者有兴趣可自行分析左闭右闭区间对应的情况。注意,如果有左边界不存在的场景,在 while 循环后,要判断下标对应值是否与 target 相等。

观察下表可知,在区间开闭情况相同时,左右侧边界的查找的主要区别在于 nums[mid] == target 时边界更新和返回值。

场景左侧边界右侧边界备注
初始赋值left = 0, right = numsSizeleft = 0, right = numsSize相同
while条件left < rightleft < right相同
nums[mid] < targetleft = mid + 1left = mid + 1相同
nums[mid] > targetright = midright = mid相同
nums[mid] == targetright = midleft = mid + 1不同
返回值leftleft - 1不同

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/302276.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

k8s_入门_命令详解

命令详解 kubectl是官方的CLI命令行工具&#xff0c;用于与 apiserver进行通信&#xff0c;将用户在命令行输入的命令&#xff0c;组织并转化为 apiserver能识别的信息&#xff0c;进而实现管理k8s各种资源的一种有效途径 1. 帮助 2. 查看版本信息 3. 查看资源对象等 查看No…

C语言——文件管理

文件&#xff1a;即磁盘上的文件&#xff0c;使用文件可以将数据直接存放在电脑的硬盘上&#xff0c;做到数据持久化。 在程序设计中&#xff0c;按文件的功能划分&#xff0c;将文件分为程序文件与数据文件 程序文件 程序文件包括源文件&#xff08;.c&#xff09;&#xff0…

MySQL - 基础三

11、事务管理 CURD不加控制&#xff0c;会有什么问题&#xff1f; 当客户端A检查还有一张票时&#xff0c;将票卖掉&#xff0c;还没有执行更新数据库时&#xff0c;客户端B检查了票数&#xff0c;发现大于0&#xff0c;于是又卖了一次票。然后A将票数更新回数据库。这是就出现…

C语言从入门到实战————文件操作

目录 前言 1. 为什么使用文件&#xff1f; 2. 什么是文件&#xff1f; 2.1 程序文件 2.2 数据文件 2.3 文件名 3. ⼆进制文件和文本文件&#xff1f; 4. 文件的打开和关闭 4.1 流和标准流 4.1.1 流 4.1.2 标准流 4.2 文件指针 4.3 文件的打开和关闭 5. 文…

javaWeb车辆管理系统设计与实现

摘 要 随着经济的日益增长,车辆作为最重要的交通工具,在企事业单位中得以普及,单位的车辆数目已经远远不止简单的几辆,与此同时就产生了车辆资源的合理分配使用问题。 企业车辆管理系统运用现代化的计算机管理手段&#xff0c;不但可以对车辆的使用进行合理的管理&#xff0c;…

基于ssm的充电桩综合管理系统(java项目+文档+源码)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于ssm的充电桩综合管理系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 充电桩综合管理系统的主要使…

护眼台灯品牌哪个品牌好用?护眼台灯品牌排行推荐

在光照不足的环境中&#xff0c;护眼台灯还能提升阅读和学习的视觉舒适度&#xff0c;减轻眼疲劳和视觉疲劳的可能性。鉴于当今儿童和青少年的学习用眼时间较长&#xff0c;而且他们处于视力发展的关键阶段&#xff0c;眼瞳更为敏感&#xff0c;容易发生近视&#xff0c;因此&a…

【产品】ADW300 无线计量仪表 用于计量低压网络的三相有功电能

1 概述 ADW300 无线计量仪表主要用于计量低压网络的三相有功电能&#xff0c;具有体积小、精度高、功能丰富等优点&#xff0c;并且可选通讯方式多&#xff0c;可支持 RS485 通讯和 Lora、2G、NB、4G 等无线通讯方式&#xff0c;增加了外置互感器的电流采样模式&#xff0c;从…

App加固:不同类型和费用对比

文章目录 [TOC]引言应用程序加固是什么不同类型[App加固](https://www.ipaguard.com/)的费用对比基础加固高级加固云加固 白嫖的混淆加密工具](https://www.ipaguard.com/)-[ipaguard总结参考资料 引言 在当前移动应用市场中&#xff0c;安全性已经成为一个非常重要的话题。为…

心灵鸡汤之励志正能量文案,积极向上热爱生活短句

1、在一切变好之前&#xff0c;我们总要经历一些不开心的日子&#xff0c;这段日子也许很长&#xff0c;也许只是一觉醒来。有时候&#xff0c;选择快乐&#xff0c;更需要勇气。 2、靠自己&#xff0c;才能无惧艰难&#xff0c;靠他人&#xff0c;永远害怕风霜&#xff0c;别…

跨云迁移实操:AWS RDS for mysql 迁移至腾讯云mysql --DTS方式

实操场景&#xff1a;从AWS RDS for mysql 迁移至腾讯云云数据库Mysql&#xff0c;通过腾讯云数据传输服务DTS,进行实时全量增量迁移. 下面九河云给大家带来具体实践介绍 购买迁移数据库--目的端机器&#xff08;腾讯云MYSQL&#xff09; 可以源端为5.7所以新建一个参数模版 其…

nginx 配置访问地址和解决跨域问题(反向代理)

1、配置访问地址&#xff08;通过ip访问&#xff09; //配置ip访问地址 location ^~/auditApp{alias /usr/local/front-apps/cbd/auditApp;index index.html;if (!-e $request_filename) {rewrite ^/(.*) /auditApp/index.html last;break;}} 2、解决跨域问题&…

Ceph学习 -4.Ceph组件介绍

文章目录 1.Ceph组件介绍1.1 组件介绍1.2 流程解读1.2.1 综合效果图1.2.2 数据存储逻辑 1.3 小结 1.Ceph组件介绍 学习目标&#xff1a;这一节&#xff0c;我们从组件介绍、流程解读、小结三个方面来学习。 1.1 组件介绍 无论是想向云平台提供 Ceph 对象存储和 Ceph 块设备服务…

Qt快速入门到熟练(3.程序运行发布与设置图标)

程序运行发布 当我们执行过qt过后&#xff0c;将会在项目目录里面生成出一个debug构建目录&#xff0c;点击进去选择debug文件夹&#xff0c;就可以看到我们生成出来的可执行文件。 很显然我们的项目就叫做MyFirstWidget&#xff0c;所以生成的可执行文件在没有人为设置的情…

什么是国密SSL证书,和其他SSL证书的区别?

我们要了解什么是SSL证书。SSL&#xff08;Secure Sockets Layer&#xff0c;安全套接层&#xff09;是一种安全协议&#xff0c;主要用于在互联网上对通信双方进行身份验证以及保障数据的安全传输。而SSL证书则是由权威的数字证书认证机构签发的&#xff0c;用于证明网站身份的…

Spring事务简介,事务角色,事务属性

1.Spring事务简介 事务作用&#xff1a;在数据层保障一系列的数据库操作同成功同失败Spring事务作用&#xff1a;在数据层或业务层保障一系列的数据操作同成功同失败 public interface PlatformTransactionManager{void commit(TransactionStatus status) throws TransactionE…

武汉星起航:跨境电商新航标,打造卓越卖家孵化平台!

在全球经济一体化的浪潮下&#xff0c;跨境电商行业蓬勃发展&#xff0c;成为推动国际贸易增长的新引擎。在这个充满挑战与机遇的舞台上&#xff0c;武汉星起航电子商务有限公司以其深厚的自营经验和专业的卖家孵化服务&#xff0c;成为跨境电商领域的璀璨明星。 武汉星起航电…

ESP32cam 摄像头 AIcam 全球远程视频监控的实现方法

AIcam远程视频监控 ​ 在学习应用各种物联网创客场景时我们时常会用到远程视频监控&#xff0c;当然我们可以通过发送图片的方式的来远程查看&#xff0c;但如果能视频查看将会更加的生动&#xff0c;比如在公司查看家里宠物的动态&#xff0c;鱼儿的活动情况。。。。。。 这个…

查看MySQL版本的方式

文章目录 一、使用cmd输入命令行查看二、在mysql客户端服务器里查询 一、使用cmd输入命令行查看 1、打开 cmd &#xff0c;输入命令行&#xff1a; mysql --version 2、还是打开cmd&#xff0c;输入命令行&#xff1a;mysql -V (注意了&#xff0c;此时的V是个大写的V) 二、…

C++set和map详细介绍

文章目录 前言一、关联式容器和序列式容器二、set1.set文档介绍2.set成员函数1.构造函数2.迭代器3.容量4.修改5.其他 三.multiset四.map1.map文档介绍2.map成员函数1.构造2.insert插入3.count4.迭代器5.【】和at 五.multimap总结 前言 在本篇文章中&#xff0c;我们将会学到关…