【算法详解】二分查找

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] 内查找 target
        while (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;
            }
        }
        // 判断区间剩余元素是否为目标元素,不是则返回 -1
        return 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] 内查找 target
        while (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;
            }
        }
        // 判断区间剩余元素是否为目标元素,不是则返回 -1
        return 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不同

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

界面组件DevExpress WinForms v23.2 - 功能区、富文本编辑器功能升级

DevExpress WinForms拥有180组件和UI库&#xff0c;能为Windows Forms平台创建具有影响力的业务解决方案。DevExpress WinForms能完美构建流畅、美观且易于使用的应用程序&#xff0c;无论是Office风格的界面&#xff0c;还是分析处理大批量的业务数据&#xff0c;它都能轻松胜…

软考 系统架构设计师系列知识点之云原生架构设计理论与实践(16)

接前一篇文章&#xff1a;软考 系统架构设计师系列知识点之云原生架构设计理论与实践&#xff08;15&#xff09; 所属章节&#xff1a; 第14章. 云原生架构设计理论与实践 第3节 云原生架构相关技术 14.3.3 无服务器技术 1. 技术特点 2. 技术关注点 &#xff08;1&#xff…

四川一体化污水处理设备厂家如何挑选

如果您想寻找一家可靠的四川地区的污水处理设备厂家&#xff0c;以下是一些挑选的关键要素可以考虑&#xff1a; 1. 信誉和口碑&#xff1a;了解该厂家在业界的声誉和客户的评价&#xff0c;可以通过查阅相关的评论和建议&#xff0c;或者咨询其他业内人士来了解。 2. 技术实力…

数据生成 | Matlab实现基于SNN浅层神经网络的数据生成

数据生成 | Matlab实现基于SNN浅层神经网络的数据生成 目录 数据生成 | Matlab实现基于SNN浅层神经网络的数据生成生成效果基本描述模型描述程序设计参考资料 生成效果 基本描述 1.Matlab实现基于SNN浅层神经网络的数据生成&#xff0c;运行环境Matlab2021b及以上&#xff1b; …

【接口自动化】参数化替换

在做接口测试时&#xff0c;除了测单个接口&#xff0c;还需要进行业务链路间的接口测试 比如[注册-登陆]需要token鉴权的业务流 当我们用使用postman/jmeter等工具时&#xff0c;将注册接口的一些响应信息提取出来&#xff0c;放到登陆接口的请求中&#xff0c;来完成某个业务…

紫叶写作能用吗 #微信#知识分享

紫叶写作是一款非常好用、靠谱的论文写作工具&#xff0c;它旨在帮助用户快速高效地完成论文写作任务&#xff0c;并提供查重降重的功能。它不仅操作简单方便&#xff0c;而且功能强大&#xff0c;能够有效提高论文写作的效率和质量。 首先&#xff0c;紫叶写作提供了丰富的模板…

CKA 基础操作教程(五)

Kubernetes Ingress 理论学习 Ingress 提供从集群外部到集群内服务的 HTTP 和 HTTPS 路由。 流量路由由 Ingress 资源所定义的规则来控制。 Ingress 资源示例&#xff1a; apiVersion: networking.k8s.io/v1 # 指定 Kubernetes 中使用的 API 版本 kind: Ingress # 指定对象…

matlab使用教程(38)—傅里叶变换

1基本概念 傅里叶变换是一个数学公式&#xff0c;用于将按时间或空间采样的信号变换为按时序或空间频率采样的相同信号。 在信号处理中&#xff0c;傅里叶变换可以揭示信号的重要特征&#xff08;即其频率分量&#xff09;。 对于包含 n 个均匀采样点的向量 x &#xff0c;其…

为说阿拉伯语的国家进行游戏本地化

阿拉伯语是由超过4亿人使用的语言&#xff0c;并且是二十多个国家的官方语言。进入这些国家的市场并非易事——虽然他们共享一种通用语言&#xff0c;但每个国家都有自己独特的文化&#xff0c;有自己的禁忌和对审查的处理方式。这就是为什么视频游戏公司长期以来都远离阿拉伯语…

mac-m1pro芯片编译java项目慢且发热严重问题

目录 一、背景二、排查三、解决四、效果以及结果展示五、总结 一、背景 使用idea编译项目等操作&#xff0c;经常性发热严重&#xff0c;并且时间慢。直到昨天编译一个项目用时30分钟&#xff0c;电脑温度很高&#xff0c;并且有烧灼的味道&#xff0c;于是有了此篇文章。 二、…

Linux应用开发(3):Linux时间操作(time、mktime、localtime等)

1. 简述 在Linux系统中&#xff0c;时间操作函数是编程中经常使用的一部分&#xff0c;它们允许程序获取和设置系统时间&#xff0c;以及对时间进行各种处理。以下是一些常用的时间操作函数的详细介绍。 2. 时间操作 &#xff08;1&#xff09;time(): 获取1970年1月1日以来的…

TalkingData——Unity应用开发中集成统计分析工具

第一步:帐号注册 官方网站:TalkingData-移动.数据.价值 第二步:创建应用查看 appid 可以进入网站注册,注册好以后就可以创建应用 创建好应用后,点击 应用管理-》基本信息就可以查看自己的 AppID 第三步:申请对应平台的sdk 接下来就是申请sdk 这里是申请sdk的网站…

Unity MySql安装部署与Unity连接 上篇

1.前言 最近项目用到MySql&#xff0c;记录一下安装部署过程。 数据量过大或者需要管理用户数据的时候用mysql的话数据结构比较清晰明了&#xff0c;便于管理。 2.安装MySql Unity版本&#xff1a;2019.4.16 MySql版本&#xff1a;8.2.0 下载地址&#xff1a;MySql 下载…

使用Nodejs + express连接数据库mongodb

文章目录 先创建一个js文档安装 MongoDB 驱动程序&#xff1a;引入 MongoDB 模块&#xff1a;设置数据库连接&#xff1a;新建一个表试试执行数据库操作&#xff1a;关闭数据库连接&#xff1a; 前面需要准备的内容可看前面的文章&#xff1a; Express框架搭建项目 node.js 简单…

数字化智慧养老:引领老年人融入科技时代新生活

hello宝子们...我们是艾斯视觉擅长ui设计和前端开发10年经验&#xff01;希望我的分享能帮助到您&#xff01;如需帮助可以评论关注私信我们一起探讨&#xff01;致敬感谢感恩&#xff01; 人类社会已经步入了一个全新的数字时代。在这个时代&#xff0c;互联网、大数据、人工智…

Docker 引擎一键安装

文章目录 一、场景说明二、脚本职责三、参数说明四、操作示例五、注意事项 一、场景说明 本自动化脚本旨在为提高研发、测试、运维快速部署应用环境而编写。 脚本遵循拿来即用的原则快速完成 CentOS 系统各应用环境部署工作。 统一研发、测试、生产环境的部署模式、部署结构、…

Spring Cloud 集成 RabbitMQ

目录 前言步骤引入相关maven依赖添加相关配置 使用方法配置消息序列化创建第一个消息队列和交换机使用方法 总结 前言 在当今的微服务架构盛行的时代&#xff0c;消息队列作为一种重要的通信机制&#xff0c;在分布式系统中扮演着不可或缺的角色。RabbitMQ&#xff0c;作为一款…

数据静态脱敏技术广泛的应用价值

数据静态脱敏是一种重要的数据保护技术&#xff0c;其核心目标是在保证数据库系统正常运行的前提下&#xff0c;对敏感数据进行脱敏处理&#xff0c;从而降低数据泄露的风险。在数字化时代&#xff0c;数据成为企业最宝贵的资产之一&#xff0c;然而&#xff0c;数据的敏感性和…

每日一题(leetcode2009):使数组连续的最小操作数--滑动窗口

从相反面考虑&#xff0c;一条已知长度的线段最多能覆盖多少数值&#xff0c;最先用长度减一下就行。线段覆盖问题用滑动窗口就行。代码如下&#xff1a; class Solution { public:int minOperations(vector<int>& nums) {int lennums.size();sort(nums.begin(),num…

安全威胁情报的漏洞挖掘

前段时间edu上出现了两个网安总队收取安全情报&#xff0c;不收漏洞&#xff0c;下面简单分析一下如何挖掘安全情报。 在发现在edu中新增了两个网安总队收安全情报等漏洞&#xff0c;那威胁情报又会包含哪些内容呢&#xff1f;以前或许会看到各种ss网站、bc网站、yx网站满天飞&…