【优选算法】—— 二分查找

序言:

  • 本期,我们将要介绍的是有关 二分查找算法 通过题目帮组大家更好的理解! 

目录

(一)基本介绍

1、基本思想

2、解题流程

3、复杂度以及注意事项

(二)题目讲解

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

2、搜索旋转排序数组中的最⼩值

3、搜索二维矩阵

总结


(一)基本介绍

1、基本思想

二分查找算法(Binary Search Algorithm)是一种在有序数组中查找目标值的高效算法。它的基本思想是将数组分成两部分,然后通过比较目标值和数组的中间元素,确定目标值可能存在的位置,然后将搜索范围缩小一半,逐步逼近目标值的位置,直到找到目标值或确定目标值不存在。


2、解题流程

以下是二分查找算法的详细过程:

  1. 初始化左指针(left)为数组的起始位置,右指针(right)为数组的末尾位置。
  2. 计算中间位置的索引(mid),mid =  left + (right - left) /2
  3. 将目标值与中间位置的元素进行比较:
    • 若目标值等于中间位置的元素,找到目标值,返回中间位置的索引。
    • 若目标值小于中间位置的元素,说明目标值可能在数组的左半部分,将右指针(right)更新为 mid - 1,继续执行步骤 2。
    • 若目标值大于中间位置的元素,说明目标值可能在数组的右半部分,将左指针(left)更新为 mid + 1,继续执行步骤 2。
  4. 重复执行步骤 2 和步骤 3,直到找到目标值或确定目标值不存在。即当左指针大于右指针时,表示搜索范围为空,目标值不存在。

3、复杂度以及注意事项

二分查找算法的时间复杂度为 O(log n),其中 n 是数组的大小。由于每次都将搜索范围缩小一半,因此算法的效率非常高。

二分查找算法的前提是数组必须是有序的,如果数组无序,则需要先进行排序操作。此外,二分查找算法还可用于在旋转有序数组中查找目标值,只需要在比较大小时增加一些额外的判断条件。

需要注意的是,二分查找算法适用于静态数组或只读的情况。如果需要频繁地插入或删除元素,会导致数组的重新排序,影响二分查找的优势。

【小结】

  • 总结起来,二分查找算法是一种高效的查找算法,适用于有序数组中查找目标值。它的核心思想是通过不断缩小搜索范围,以有效地定位目标值。

(二)题目讲解

接下来,我们通过具体的题目带着大家去进行理解相关算法。

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

【算法思路】 

  1. ⽤的还是⼆分思想,就是根据数据的性质,在某种判断条件下将区间⼀分为⼆,然后舍去其中⼀个区间,然后再另⼀个区间内查找;
  2. ⽅便叙述,⽤ x 表⽰该元素, resLeft 表⽰左边界, resRight 表⽰右边界。

 

【寻找左边界思路】

寻找左边界:
我们注意到以左边界划分的两个区间的特点:

  1. 左边区间 [left, resLeft - 1] 都是⼩于 x 的;
  2. 右边区间(包括左边界) [resLeft, right] 都是⼤于等于 x 的;

因此,关于 mid 的落点,我们可以分为下⾯两种情况:

  1. 当我们的 mid 落在 [left, resLeft - 1] 区间的时候,也就是 arr[mid] <target 。说明 [left, mid] 都是可以舍去的,此时更新 left 到 mid + 1 的位置,继续在 [mid + 1, right] 上寻找左边界;
  2. 当 mid 落在 [resLeft, right] 的区间的时候,也就是 arr[mid] >= target 。说明 [mid + 1, right] (因为 mid 可能是最终结果,不能舍去)是可以舍去的,此时更新 right 到 mid 的位置,继续在 [left, mid] 上寻找左边界;

• 由此,就可以通过⼆分,来快速寻找左边界;
 

注意:这⾥找中间元素需要向下取整
因为后续移动左右指针的时候:

  • 左指针: left = mid + 1 ,是会向后移动的,因此区间是会缩⼩的;
  • 右指针: right = mid ,可能会原地踏步(⽐如:如果向上取整的话,如果剩下 1,2 两个元素, left == 1 , right == 2 , mid == 2 。更新区间之后, left,right,mid 的值没有改变,就会陷⼊死循环)


因此⼀定要注意,当 right = mid 的时候,要向下取整。

【寻找右边界思路】

寻右左边界:
◦ ⽤ resRight 表⽰右边界;
◦ 我们注意到右边界的特点:

  1. 左边区间(包括右边界) [left, resRight] 都是⼩于等于 x 的;
  2. 右边区间 [resRight+ 1, right] 都是⼤于 x 的;

• 因此,关于 mid 的落点,我们可以分为下⾯两种情况:

  1. 当我们的 mid 落在 [left, resRight] 区间的时候,说明 [left, mid - 1]( mid 不可以舍去,因为有可能是最终结果)都是可以舍去的,此时更新 left 到 mid 的位置;
  2.  当 mid 落在 [resRight+ 1, right] 的区间的时候,说明 [mid, right] 内的元素是可以舍去的,此时更新 right 到 mid - 1 的位置;

• 由此,就可以通过⼆分,来快速寻找右边界;
 

注意:这⾥找中间元素需要向上取整
因为后续移动左右指针的时候:

  • 左指针: left = mid ,可能会原地踏步(⽐如:如果向下取整的话,如果剩下 1,2 两个元素, left == 1, right == 2,mid == 1 。更新区间之后, left,right,mid 的值没有改变,就会陷⼊死循环)。
  • 右指针: right = mid - 1 ,是会向前移动的,因此区间是会缩⼩的;


因此⼀定要注意,当 right = mid 的时候,要向下取整。

【代码展示】

class Solution {
public:vector<int> searchRange(vector<int>& nums, int target) {// 处理边界情况if(nums.size() == 0) return {-1, -1};  //创建一个变量来记录结果的左端点int begin = 0;// 1. ⼆分左端点int left = 0;int right = nums.size() - 1;while(left < right){int mid = left + (right - left) / 2;if(nums[mid] < target) left = mid + 1;else right = mid;}    // 判断是否有结果if(nums[left] != target) return {-1, -1};else begin = left; // 标记⼀下左端点   // 2. ⼆分右端点left = 0, right = nums.size() - 1;while(left < right){int mid = left + (right - left + 1) / 2;if(nums[mid] <= target) left = mid;else right = mid - 1;}return {begin, right};}
};

【解释说明】

  1. 在边界情况下,如果给定的数组为空,则直接返回结果为{-1, -1}。

  2. 创建一个变量来记录结果的左端点。begin

  3. 初始化左右指针和分别指向数组的起始位置和末尾位置。left right

  4. 第一个while循环是为了找到目标值的左端点,使用二分查找的思想。

    • 将中间位置的索引计算为,这是一个向下取整的操作。mid = (left + right) / 2
    • 如果中间位置的元素小于目标值,说明目标值可能在右半部分,将左指针更新
    • 否则,目标值可能在左半部分或者当前位置为目标值的左端点,将右指针更新为。right = mid
    • 重复上述操作,直到和指针相等时退出循环。left = right
  5. 判断是否等于目标值,如果不等于,则说明目标值不存在于数组中,返回结果为{-1, -1}。nums[left] = target

  6. 否则,标记左端点位置为。begin = left

  7. 第二个while循环是为了找到目标值的右端点,同样使用二分查找的思想。

    • 将中间位置的索引计算为,这个是为了取整到右边的元素。mid =(left + right + 1) / 2
    • 如果中间位置的元素小于等于目标值,说明目标值可能在右半部分或者当前位置为目标值的右端点,将左指针更新
    • 否则,目标值可能在左半部分,将右指针更新为。right = mid - 1
    • 重复上述操作,直到和指针相等时退出循环。left = right
  8. 返回结果为{, },即目标值的范围。

【性能分析】

  1. 这段代码可以在时间复杂度为O(log n)的时间内找到有序数组中目标值的范围,其中n是数组的大小;
  2. 两次二分查找分别找到目标值的左右端点,从而确定目标值在数组中的范围。 


2、搜索旋转排序数组中的最⼩值

【算法思路】

题⽬中的数组规则如下图所⽰:
 

其中 C 点就是我们要求的点。
⼆分的本质:找到⼀个判断标准,使得查找区间能够⼀分为⼆。
通过图像我们可以发现, [A,B] 区间内的点都是严格⼤于 D 点的值的, C 点的值是严格⼩
于 D 点的值的。但是当 [C,D] 区间只有⼀个元素的时候, C 点的值是可能等于 D 点的值。

因此,初始化左右两个指针 left , right :
然后根据 mid 的落点,我们可以这样划分下⼀次查询的区间:

  1. 当 mid 在 [A,B] 区间的时候,也就是 mid 位置的值严格⼤于 D 点的值,下⼀次查询区间在 [mid + 1,right] 上;
  2. 当 mid 在 [C,D] 区间的时候,也就是 mid 位置的值严格⼩于等于 D 点的值,下次查询区间在 [left,mid] 上。

当区间⻓度变成 1 的时候,就是我们要找的结果
 

【代码展示】

class Solution {
public:int findMin(vector<int>& nums) {int left = 0;int right = nums.size() - 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];}
};

【解释说明】

  1. 初始化左右指针和分别指向数组的起始位置和末尾位置。leftright

  2. 创建一个变量来存储数组最后一个位置上的值,即数组中的最大值。这是为了标记最后一个位置的值,以便在最后返回最小值。xnums[right]

  3. 进入while循环,判断条件是,即左指针小于右指针时执行循环。left < right

  4. 将中间位置的索引计算为,这是一个向下取整的操作。mid(left + right) / 2

  5. 比较中间位置的元素和最后一个位置的值。nums[mid]x

    • 如果,说明最小值在mid的右侧,将左指针更新
    • 否则,最小值可能在mid的左侧或者就是当前位置mid,将右指针更新为。
    • 重复上述操作,直到和指针相等时退出循环。
  6. 循环结束后,返回,即找到的最小值。nums[left]

【性能分析】

  1. 时间复杂度为O(log n)的时间内找到旋转排序数组的最小值,其中n是数组的大小;
  2. 通过二分查找的方式逐渐缩小查找范围,最终找到最小值的位置。

3、搜索二维矩阵

【算法思路】

1、初始化两个指针,一个指针指向二维矩阵的左上角,即第一行第一列的元素,另一个指针指向二维矩阵的右下角,即最后一行最后一列的元素。利用这两个指针来缩小搜索范围。

2、在每次循环中,首先比较左上角指针所指的元素与目标值的关系:

  • 如果左上角指针所指的元素等于目标值,说明找到目标值,返回true。
  • 如果左上角指针所指的元素大于目标值,说明目标值可能在左上角指针所在的列的左侧,将右下角指针的列数减1。
  • 如果左上角指针所指的元素小于目标值,说明目标值可能在左上角指针所在的行的下方,将左上角指针的行数加1。

3、重复步骤5,直到左上角指针的行数大于右下角指针的行数或者左上角指针的列数大于右下角指针的列数,表示搜索范围已经缩小到无法再继续缩小的情况

【代码实现】

class Solution {
public:bool searchMatrix(vector<vector<int>>& matrix, int target) {if (matrix.empty() || matrix[0].empty()) {return false;}int m = matrix.size();int n = matrix[0].size();int left = 0;int right = m * n - 1;while (left <= right) {int mid = left + (right - left) / 2;int row = mid / n;int col = mid % n;if (matrix[row][col] == target) {return true;} else if (matrix[row][col] < target) {left = mid + 1;} else {right = mid - 1;}}return false;}
};

【解释说明】

  1. 首先检查矩阵是否为空,如果是空矩阵,则直接返回。

  2. 获取矩阵的行数和列数。

  3. 初始化左指针为0,右指针为矩阵总元素数减1。

  4. 进入while循环,判断条件是,即左指针小于等于右指针时执行循环。left <= right

  5. 计算中间位置的索引,使用进行计算。mid=(left + right) / 2

  6. 根据中间索引计算出对应的行和列,通过和分别得到行和列的值。

  7. 将矩阵中对应的元素与目标值进行比较。

    • 如果等于,则找到目标值,返回。
    • 如果小于,说明目标值可能在当前位置的右侧,将左指针更新。
    • 如果大于,说明目标值可能在当前位置的左侧,将右指针更新。
    • 重复上述操作,直到找到目标值或者左指针大于右指针时退出循环。
  8. 循环结束后,如果没有找到目标值,返回。

【性能分析】

  • 该算法的时间复杂度为O(m+n),其中m为二维矩阵的行数,n为二维矩阵的列数。算法利用了二维矩阵的特点,在每次比较后可以减少一行或一列的搜索范围,从而快速找到目标值或确定不存在目标值。

总结

请⼤家⼀定不要觉得背下模板就能解决所有⼆分问题。⼆分问题最重要的就是要分析题意,然后确定要搜索的区间,根据分析问题来写出⼆分查找算法的代码。

  • 要分析题意,确定搜索区间,不要死记模板,不要看左闭右开什么乱七⼋糟的题解
  • 要分析题意,确定搜索区间,不要死记模板,不要看左闭右开什么乱七⼋糟的题解
  • 要分析题意,确定搜索区间,不要死记模板,不要看左闭右开什么乱七⼋糟的题解

重要的事情说三遍。
 

【模板记忆技巧】

  • 1. 关于什么时候⽤三段式,还是⼆段式中的某⼀个,⼀定不要强⾏去⽤,⽽是通过具体的问题分析情况,根据查找区间的变化确定指针的转移过程,从⽽选择⼀个模板。
  • 2. 当选择两段式的模板时: 在求 mid 的时候,只有 right - 1 的情况下,才会向上取整(也就是 +1 取中间数)

以上便是关于 二分查找 算法的全部知识讲解!大家多加练习,立即这部分算法还是很轻松的!

 

 

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

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

相关文章

Sql Server导出数据库到另一个数据库

1.打开sql server数据库&#xff0c;连接到服务器后&#xff0c;找到需要导出的数据库&#xff0c;右击后选择 任务->导出数据。 2.点击 下一步。 3.身份验证可以使用SQL Server身份验证&#xff0c;就是当时建立连接时的用户名和密码&#xff0c;数据库名称使用默认的&…

C++实现YOLOP

C实现YOLOP 一、简介 使用OpenCV部署全景驾驶感知网络YOLOP&#xff0c;可同时处理交通目标检测、可驾驶区域分割、车道线检测&#xff0c;三项视觉感知任务&#xff0c;依然是包含C和Python两种版本的程序实现 onnx文件从百度云盘下载&#xff0c;链接&#xff1a;https://…

<C++> STL_deque

<c> STL_deque 1.deque的使用 deque(双端队列)&#xff1a;是一种双开口的"连续"空间的数据结构&#xff0c;双开口的含义是&#xff1a;可以在头尾两端进行插入和 删除操作&#xff0c;且时间复杂度为O(1)&#xff0c;与vector比较&#xff0c;头插效率高&a…

一生一芯9——ubuntu22.04安装valgrind

这里安装的valgrind版本是3.19.0 下载安装包 在选定的目录下打开终端&#xff0c;输入以下指令 wget https://sourceware.org/pub/valgrind/valgrind-3.19.0.tar.bz2直至下载完成 解压安装包 输入下面指令解压安装包 tar -xvf valgrind-3.19.0.tar.bz2.tar.bz2注&#xf…

Keepalived+Lvs(dr)调度器主备配置小实验

目录 前言 一、实验拓扑图 二、配置LVS&#xff08;dr&#xff09;模式 三、配置调配器热备 四、测试 总结 前言 Keepalived和LVS&#xff08;Linux Virtual Server&#xff09;是两个常用的开源软件&#xff0c;通常结合使用以提供高可用性和负载均衡的解决方案。 Keepalive…

身为程序员,你有哪些提高写代码效率的工具?

首先&#xff0c;每个程序员都是会利用工具的人&#xff0c;也有自己囊里私藏的好物。独乐乐不如众乐乐&#xff0c;今天笔者整理了3个辅助我们写代码的黑科技&#xff0c;仅供参考。如果你有更好的工具&#xff0c;欢迎评论区分享。 1、Google/Stackoverflow——搜索解决方案的…

【运维】linux安装oracle客户端、安装mysql

文章目录 一. 下载二. 配置1. 配置环境变量2. 配置tnsnames.ora文件 三. 测试1. 链接语法2. 连接测试 四. 通过rpm安装mysql 一. 下载 下载地址 基础包 连接工具 二. 配置 上传、解压、配置环境变量 这里安装在/data01目录下 unzip instantclient-sqlplus-linux.x64-19.2…

Spring Boot(Vue3+ElementPlus+Axios+MyBatisPlus+Spring Boot 前后端分离)【三】

&#x1f600;前言 本篇博文是关于Spring Boot(Vue3ElementPlusAxiosMyBatisPlusSpring Boot 前后端分离)【三】的分享&#xff0c;希望你能够喜欢 &#x1f3e0;个人主页&#xff1a;晨犀主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我…

keepalived+lvs(DR)

目录 一、作用 二、安装 1、在192.168.115.3 和192.168.115.4 上安装ipvs和keepalived&#xff1a; 2、配置keepalived 3、查看lvs节点状态 4、web节点配置 5、在web节点上调整ARP参数 6、配置虚拟IP地址与添加回环路由 7、配置nginx网页文档 8、启动服务 9、测试 一…

上位机采集8通道模拟量模块数据

模拟量模块和上位机的配合使用可以实现对模拟量数据的采集、传输和处理。下面是它们配合使用的一般步骤&#xff1a;1. 连接模拟量模块&#xff1a;将模拟量模块与上位机进行连接。这通常涉及将模拟量模块的输入通道与被监测的模拟信号源连接起来&#xff0c;如传感器、变送器等…

14. Docker中实现CI和CD

目录 1、前言 2、什么是CI/CD 3、部署Jenkins 3.1、下载Jenkins 3.2、启动Jenkins 3.3、访问Jenkins页面 4、Jenkins部署一个应用 5、Jenkins实现Docker应用的持续集成和部署 5.1、创建Dockerfile 5.2、集成Jenkins和Docker 6、小结 1、前言 持续集成(CI/CD)是一种…

18-使用钩子函数判断用户登录权限-登录前缀

钩子函数的两种应用: (1). 应用在app上 before_first_request before_request after_request teardown_request (2). 应用在蓝图上 before_app_first_request #只会在第一次请求执行,往后就不执行, (待定,此属性没调试通过) before_app_request # 每次请求都会执行一次(重点…

【Three.js + Vue 构建三维地球-Part One】

Three.js Vue 构建三维地球-Part One Vue 初始化部分Vue-cli 安装初始化 Vue 项目调整目录结构 Three.js 简介Three.js 安装与开始使用 实习的第一个任务是完成一个三维地球的首屏搭建&#xff0c;看了很多的案例&#xff0c;也尝试了用 Echarts 3D地球的模型进行构建&#xf…

设计模式中的关系

文章目录 一、依赖概念 二&#xff0c;关联概念 三、聚合概念 四、组合概念 五、实现概念 六、继承概念 图总结整体总结 一、依赖 概念 依赖是一种临时使用关系&#xff0c;代码层体现为作为参数。 具体体现&#xff1a;依赖者调用被依赖者的局部变量、参数、静态方法&#…

docker项目实战

目录 1、使用mysql:5.6和 owncloud 镜像&#xff0c;构建一个个人网盘。 1&#xff09;拉取mysql:5.6和owncloud镜像 2&#xff09;后台运行容器 3&#xff09;通过ip:端口的方式访问owncloud 2、安装搭建私有仓库 Harbor 1&#xff09;首先准备所需包 2&#xff09;安装h…

Lua与C++交互(一)————堆栈

Lua与C交互&#xff08;一&#xff09;————堆栈 Lua虚拟机 什么是Lua虚拟机 Lua本身是用C语言实现的&#xff0c;它是跨平台语言&#xff0c;得益于它本身的Lua虚拟机。 虚拟机相对于物理机&#xff0c;借助于操作系统对物理机器&#xff08;CPU等硬件&#xff09;的一…

HTML番外篇(四)-HTML5新增元素-CSS常见函数-理解浏览器前缀-BFC

一、HTML5新增元素 1.HTML5语义化元素 在HMTL5之前&#xff0c;我们的网站分布层级通常包括哪些部分呢&#xff1f; header、nav、main、footer ◼ 但是这样做有一个弊端&#xff1a; 我们往往过多的使用div, 通过id或class来区分元素&#xff1b;对于浏览器来说这些元素不…

雅思作文复习

目录 我使用的词汇&#xff1a; 上升&#xff1a; 下降&#xff1a; 波动&#xff1a; 保持&#xff1a; 幅度 大变化&#xff1a; 小变化&#xff1a; 雅思评价标准改变 小作文一般花费20分钟&#xff0c;我觉得自己能在18分钟解决是最好 考生在雅思考试中的小作文&a…

嵌入式系统存储体系

一、存储系统概述 主要分为三种&#xff1a;高速缓存&#xff08;cache&#xff09;、主存和外存。 二、高速缓存Cache 高速缓冲存储器中存放的是当前使用得最多得程序代码和数据&#xff0c;即主存中部分内容的副本&#xff0c;其本身无自己的地址空间。在嵌入式系统中Cac…

别在说自己不知道docker了,全文通俗易懂的给你说明白docker的基础与底层原理

docker介绍 Docker 是一个开源的应用容器引擎&#xff0c;基于Go语言进行开发实现并遵从Apache2.0 协议开源&#xff0c;基于 Linux 内核的 cgroup&#xff0c;namespace&#xff0c;以及 OverlayFS 类的 Union FS 等技术&#xff0c;对进程进行封装隔离&#xff0c;属于 操作…