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

news2024/11/27 7:41:02

序言:

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

目录

(一)基本介绍

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.coloradmin.cn/o/940599.html

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

相关文章

微信好友误删别担心,这几个方法助你轻松加回!

有些人一生气&#xff0c;一怒之下就把好友给删了。事后又想加回来&#xff0c;那要怎么办呢&#xff1f; 删除的好友怎么才能加回来&#xff1f;还是有一些办法的&#xff0c;小伙伴们可以试试&#xff01; 对方的微信号肯定是记不住的&#xff0c;要加回来肯定是一脸糊涂了。…

【C++】C++ 引用详解 ⑧ ( 普通引用与常量引用 | 常量引用概念与语法 )

文章目录 一、普通引用1、概念说明2、代码示例 - 普通引用 二、常量引用1、常量引用引入2、常量引用概念与语法2、代码示例 - 常量引用不可修改 一、普通引用 1、概念说明 之前的 【C】C 引用详解 ① ~ ⑦ 博客中 , 讲解的都是 普通引用 , 也就是 将 普通变量 赋值给 引用 , 过…

docker容器一直在restarting

chown: changing ownership of ‘/var/lib/mysql/’: Permission denied 问题也同样处理 参考

【Zblog搭建博客网站】windows环境搭建博客并上线

文章目录 1. 前言2. Z-blog网站搭建2.1 XAMPP环境设置2.2 Z-blog安装2.3 Z-blog网页测试2.4 Cpolar安装和注册 3. 本地网页发布3.1. Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 1. 前言 想要成为一个合格的技术宅或程序员&#xff0c;自己搭建网站制作网页是绕…

【管理运筹学】第 6 章 | 运输问题(3,表上作业法 | 最优解的判断)

文章目录 引言二、表上作业法2.2 最优解的判断2.2.1 闭回路法2.2.2 位势法 写在最后 引言 承接前文&#xff0c;我们继续学习表上作业法的第二步 —— 最优解的判断。 二、表上作业法 表上作业法的求解工作在运输表上进行&#xff0c;运输问题解的每一个分量&#xff0c;都唯…

MFC-RIBBON/QT混合编程‘完美’方案

[工程建立] 在&#xff36;&#xff23;用向导生成一个&#xff31;&#xff34; &#xff27;&#xff35;&#xff29;程序&#xff0c;将里面的代码文件等删除&#xff08;.vcxproj等保留&#xff09;在&#xff36;&#xff23;用向导生成另一个&#xff2d;&#xff26;…

怎么学习黑客?最全黑客自学路线

谈起黑客&#xff0c;可能各位都会想到&#xff1a;盗号&#xff0c;其实不尽然&#xff1b;黑客是一群喜爱研究技术的群体&#xff0c;在黑客圈中&#xff0c;一般分为三大圈&#xff1a;娱乐圈 技术圈 职业圈。 娱乐圈&#xff1a;主要是初中生和高中生较多&#xff0c;玩网恋…

乱糟糟的YOLOv8-detect和pose训练自己的数据集

时代在进步&#xff0c;yolo在进步&#xff0c;我还在踏步&#xff0c;v8我浅搞了一下detect和pose&#xff0c;记录一下&#xff0c;我还是要吐槽一下&#xff0c;为啥子这个模型就放在了这个文件深处&#xff0c;如图。 以下教程只应用于直接应用yolov8&#xff0c;不修改。…

k8s之存储篇---数据卷Volume

数据卷概述 Kubernetes Volume&#xff08;数据卷&#xff09;主要解决了如下两方面问题&#xff1a; 数据持久性&#xff1a;通常情况下&#xff0c;容器运行起来之后&#xff0c;写入到其文件系统的文件暂时性的。当容器崩溃后&#xff0c;kubelet 将会重启该容器&#xff…

C++中为什么有模板的函数不能和.h文件分离,即分别声明和定义

目录 1.查看问题 2.探索问题 3.解决问题 1.查看问题 1.先看下面三个文件 stack.h stack.cpp test.cpp 2.探索问题 有了解的小伙伴应该知道大概率是在预处理&#xff0c;编译&#xff0c;汇编&#xff0c;链接中编译环节出错了&#xff0c;它在其他文件中无法通过定义找到函…

高效UI设计必备的4个UI设计软件,真的好用!

随着UI设计工作的不断发展&#xff0c;工作中的需求变得更加多样&#xff0c;一个好用的 UI 设计软件将极大减轻设计师的工作负担&#xff0c;提高设计师的工作效率&#xff0c;今天本文精选了4款好用的UI设计软件&#xff0c;并将逐一介绍的它们各自的特点和用法&#xff0c;给…

linux+QT+FFmpeg 6.0,把多个QImage组合成一个视频

直接上代码吧: RecordingThread.h#ifndef RECORDINGTHREAD_H #define RECORDINGTHREAD_H #include "QTimer" #include <QObject> #include <QImage> #include <QQueue>extern "C"{//因为FFmpeg是c语言,QT里面调用的话需要extern "C…

基于Java swing和mysql实现的学生选课管理系统(源码+数据库+运行指导视频)

一、项目简介 本项目是一套Java swing和mysql实现的学生选课管理系统&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Java学习者。 包含&#xff1a;项目源码、项目文档、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严…

浮点动作 转 布尔动作(FloatActionToBooleanAction)

文章目录 思维导图具体步骤1、“浮点动作”的创建2、“布尔动作”的创建3、用“FloatToBoolean.cs”把两个动作联系起来 注意 思维导图 具体步骤 1、“浮点动作”的创建 “右手柄 扳机键 按下”需借助“Input.UnityInputManager.1DAxisAction”预设体成为FloatAction&#xf…

Java 读取TIFF JPEG GIF PNG PDF

Java 读取TIFF JPEG GIF PNG PDF 本文解决方法基于开源 tesseract 下载适合自己系统版本的tesseract &#xff0c;官网链接&#xff1a;https://digi.bib.uni-mannheim.de/tesseract/ 2. 下载之后安装&#xff0c;安装的时候选择选择语言包&#xff0c;我选择了中文和英文 3.…

代码随想录打卡—day44—【DP】— 8.28 完全背包基础

1 完全背包基础 完全背包和01背包问题唯一不同的地方就是&#xff0c;每种物品有无限件。 而完全背包的物品是可以添加多次的&#xff0c;所以要内嵌的背包容量循环从小到大去遍历&#xff0c;即&#xff1a;&#xff08;至于为什么&#xff0c;内嵌背包容量循环从小到大遍历…

mysql 计算两点之间距离

先说一下我们可能会用到的一些场景&#xff0c;这样同学们可以先评估&#xff0c;该篇文章是否对你有帮助&#xff01; 场景&#xff1a; 假设 美团&#xff0c;我点外卖时&#xff0c;系统会让我先进行定位&#xff0c;比如我定位在了 A 点&#xff0c;系统就会给我推荐&…

八路参考文献:[八一新书]许少辉.乡村振兴战略下传统村落文化旅游设计[M]北京:中国建筑工业出版社,2022.

八路参考文献&#xff1a;&#xff3b;八一新书&#xff3d;许少辉&#xff0e;乡村振兴战略下传统村落文化旅游设计&#xff3b;&#xff2d;&#xff3d;北京&#xff1a;中国建筑工业出版社&#xff0c;&#xff12;&#xff10;&#xff12;&#xff12;&#xff0e;

多商户门店会员卡充值收款营销公众号开发

多商户门店会员卡充值收款营销公众号开发 一、功能特色&#xff1a; 专注于收款码支持商户入驻功能&#xff0c;降低运营成本资金可以直接到达商户&#xff0c;支持微信分账方式分配佣金&#xff0c;完全符合支付结算相关法规充值、充次支持给赠值设置有效期&#xff0c;促进…

防雷浪涌保护器选型方案

防雷浪涌保护器是一种用于保护电气设备免受雷电或其他电源干扰引起的过电压或过电流的装置。防雷浪涌保护器的选型应根据国家标准、设备要求和实际工程条件进行&#xff0c;以达到既满足防雷验收要求&#xff0c;又能有效保护设备的目的。地凯科技介绍一些常用的防雷浪涌保护器…