算法之二分查找算法

news2025/1/15 16:35:33

二分查找算法简介

1. 首先说明二分查找算法是比较恶心, 细节很多, 很容易写出死循环的算法, 但熟悉了之后是最简单的算法.

2. 其次我们可能听说过二分查找的前提是数组有序的前提下进行, 但其实不一定.

3. 二分查找算法有一套模板:

  • 朴素的二分模板: 比较简单, 但是有局限性
  • 查找左边界的二分模板, 查找右边界的二分模板: 万能模板, 但是细节很多.

二分查找算法的关键是"二段性" , 当我们发现一个规律, 根据这个规律能把这个数组分为两部分, 根据规律能有选择性的舍去一部分, 进而能在另一个部分继续查找, 可以看到这其中并没有提到数组是否有序, 关键在于数组是否有"二段性". 

此外, 我们对于选择区间划分点mid的位置也并没有具体的描述必须选择在中间点, 三分之一点, 四分之一点....都可以, 因为只要我们找到的这个位置能把区间分为两部分即可, 即数组有二段性即可.

但是选择中间点作为划分点时间复杂度是最好的.


题目1: 二分查找

朴素二分查找的步骤:

a. 定义 leftright 指针, 分别指向数组的左右区间.
b. 找到待查找区间的中间点 mid , 找到之后分三种情况讨论:

朴素二分查找核心:

1. arr[mid] == target 说明正好找到, 返回 mid 的值;

2. arr[mid] > target 说明 [mid, right] 这段区间都是大于 target 的,因此舍去右边区间, 在左边 [left, mid -1] 的区间继续查找,即让 right = mid -1 ,然后重复 2 过程;
3. arr[mid] < target 说明 [left, mid] 这段区间的值都是小于 target 的, 因此舍去左边区间, 在右边 [mid + 1, right] 区间继续查找, 即让 left = mid +1 ,  然后重复 2 过程;

c. 当 left 与 right 错开时, 说明整个区间都没有这个数, 返回 -1 .

相关细节问题: 

1. 循环结束的条件: left>right, 也就是这个区间不存在的时候, 说明没找到; 注意: 当left==right的时候循环没结束, 此时代表区间内只有一个数, 这个数也是要判断的.

2. 为什么时间复杂度低? 

程序执行次数 1次 ->  区间长度n/2, 2次->n/2^2, 3次->n/2^3, ..., x次->n/2^x, 假设在第x次区间长度为1, 也就最坏的情况下找到了这个数, 那么n/2^x = 1 -> x = logn, 所以时间复杂度为O(logn).

class Solution {
public:
    int search(vector<int>& nums, int target) 
    {
        int left = 0, right = nums.size()-1;
        while(left <= right)
        {
           // int mid = (left+right)/2;//加法有溢出的风险
            int mid = left + (right-left)/2;
            if(nums[mid] < target) left = mid + 1;
            else if(nums[mid] > target) right = mid -1;
            else return mid;
        }
        return -1;//没找到
    }
};

 关于朴素二分查找的模板:

    while(left <= right)
    {
        int mid = left + (right-left)/2;
        if(...)
            left = mid + 1;
        else if(...)
            right = mid -1;
        else
            return ...;
    }

其中...根据题目所分析出的二段性进行填写. 其中两个细节:

1. 条件判断为left<=right.

2. 求中间点的方式为left +(right-left)/2防溢出.

3. 朴素二分查找求中间点用 left+(right-left)/2 和 left+(right-left+1)/2 没有差别, 这两种求中间点的方法在区间长度为奇数的时候, 求出的mid是一样的, 唯一的区别在于区间长度为偶数的时候, 第一种求出的mid偏左, 第二种求出的mid偏右, 对于朴素二分查找这两种没有区别.


题目2: 在排序数组中查找元素的第一个和最后一个位置

 这道题用朴素二分法无法求解, 因为要求的是一个区间, 朴素二分求到的那个值无法确定位于区间的哪一个位置, 但是还是可以用二分的, 关键是找到数组的二段性:

1. 查找区间的左端点:

可以发现, 我们可以把区间按照大于等于target小于target分为两部分:

当nums[mid]的值落在小于target的区间内时, 如何去更新值呢? 能去更新right吗?

不能, 因为区间左端点的位置在大于等于target的区间内, 更新right就错过了这个点, 所以要更新left, 而left左边区间的值包括left都小于target, 所以left = mid+1, 最好的情况就是left恰好在区间的右端, 此时mid对应的值就一定等于要找的那个位置.

 当nums[mid]的值落在大于等于target的区间内时, 如何去更新right呢?

right不能等于mid-1, 因为如果mid恰好落在区间的左端点, 那么mid-1就错过了这个位置, 而right要尽可能的往左移取找到区间的左端点, 所以right=mid.

细节处理:

1. 循环条件必须是left<right, 而不能是left<=right, 因为当left==right的时候, 一定是要找的区间左端点, 如果条件还取判断是否相等, 就会陷入死循环.

2.  求中点的操作:

上面我们说了 left+(right-left)/2 和 left+(right-left+1)/2 两种取中点的方式, 这里必须要选择左边那种, 否则又会进入死循环, 因为当区间不断缩减并只剩两个元素的时候,

1. 假如存在区间左端点: 第二种取中点取的一定是right的值, 就会陷入死循环

2. 假如不存在区间: 假如区间内的值都大于target, right一直向左移动, 最终也会达到区间长度为2的, 和上面一样陷入死循环; 假如区间内的值都小于target, left一直向左移动, 最终不会死循环, 但是前面两种情况都会死循环.

 

2. 查找区间的右端点:

操作和查找区间左端点类似, 也是寻找二段性, 把区间分为小于等于target和大于target两部分, 和上面相反当mid落在小于等于target区间, left=mid, 落在大于3的区间, right = mid-1.

循环也是left < right, 但是取中点的方式必须是第二种, 道理和之前类似, 只剩两个元素时会进入死循环.

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target)
    {
        //处理边界情况
        if(nums.empty())
            return {-1,-1};
        
        int begin = -1, end = -1;
        int left = 0, 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)
            begin = left;
        
        left = 0, right = nums.size()-1;//这里left可以不用重置, 因为left已经在begin位置
        //寻找区间右端点
        while(left < right)
        {
            int mid = left + (right-left+1)/2;
            if(nums[mid] > target)
                right = mid - 1;
            else
                left = mid;
        }
        if(nums[right] == target)
            end = right;

        return {begin,end};
    }
};

 寻找区间左端点右端点模板:

//寻找区间左端点
        while(left < right)
        {
            int mid = left + (right-left)/2;
            if(...)
                left = mid+1;
            else
                right = mid;
        }
 //寻找区间右端点
        while(left < right)
        {
            int mid = left + (right-left+1)/2;
            if(...)
                right = mid - 1;
            else
                left = mid;
        }

...处根据二段性填入

 这两种划分方式选择哪种呢?

根据题目去判断要找的值最终是 >=target or <=target的, 因为left和right相遇的位置一定是包含等于的那个区间, 所以要找的结果一定是要落在包含等于的区间内的.


题目3: 搜索插入位置

解法一: 朴素二分查找, 先用朴素二分查找看看是否能找到target, 能找到直接返回; 找不到的话, 如果循环结束前区间为1的值大于target, right向左移动, 此时的left就是要插入的位置, 如果小于target, left向右移动, left也是要插入的位置 , 所以返回left即可.

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) 
    {
        int left = 0, right = nums.size()-1;
        int mid;
        while(left<=right)
        {
            mid = left +(right- left)/2;
            if(nums[mid] < target)
                left = mid+1;
            else if(nums[mid] > target)
                right = mid-1;
            else 
                return mid;
        }
        return left;
    }
};

解法二: 二段性划分: 对于能找到target插入位置在数组中插入位置在数组最左侧的情况, 要返回的位置要么是=target的位置, 要么是第一个大于target的位置, 所以把区间划分为 小于target和大于等于target两部分:

注意, 需要单独处理插入位置在最右边的, 此时整个数组都在小于target的区间, 最终一定是落在数组最后一个位置, 要将返回值+1.

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) 
    {
        int left = 0, right = nums.size()-1;
        while(left < right)
        {
            int mid = left + (right-left)/2;
            if(nums[mid] < target)
                left = mid + 1;
            else 
                right = mid;
        }
        if(target > nums[left])
            return left+1;
        else 
            return left;
    }
};

题目4: x的平方根

 二分查找方法依然是去寻找数组的二段性, 要去[1,x]的数组中去寻找x的平方根, 可以把数组划分为平方<x和平方>=x 平方<=x和平方>x ,哪一种划分是正确的? 因为此题返回的是平方根的整数部分, 比如8的算术平方根是2.82842, 应该返回2, 返回的数值是<=实际的平方根的, 所以应该划分为平方<=x和平方>x.

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) 
    {
        int left = 0, right = nums.size()-1;
        while(left < right)
        {
            int mid = left + (right-left)/2;
            if(nums[mid] < target)
                left = mid + 1;
            else 
                right = mid;
        }
        
        if(target > nums[left])//单独处理插入在右边界
            return left+1;
        else 
            return left;
    }
};

题目5: 山峰数组的峰顶索引.

暴力解法:

对于数组每一个元素, 比较arr[i] > arr[i-1]是否成立, 出现的第一个不成立的位置记为k, 则k-1就是山峰位置. 

二分查找:

观察山脉数组的定义, 可以发现对于山峰左侧包括山峰的位置, arr[i] > arr[i-1], 对于山脉的右侧, arr[i] < arr[i-1], 所以由此可以把数组分为两段, 求区间的右端点.

class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) 
    {
        int left = 0, right = arr.size()-1;
        while(left < right)
        {
            int mid = left + (right-left+1)/2;
            if(arr[mid] < arr[mid-1])
                right = mid-1;
            else
                left = mid;
        }
        return left;
    }
};

题目6:寻找峰值

暴力解法: 

 1. 第一个位置后如果下降, 则第一个位置就是山峰

2. 第一个位置后上升, 遇到第一个arr[i] < arr[i-1]的位置, i-1即为山峰

3. 第一个位置到最后一直上升, 最后一个位置就是山峰.

二分查找: 

 此题和上一题类似, 但是可能有多个山峰, 也可能是一路上升或者一路下降, 但只需要找到一种情况即可:

arr[i] > arr[i-1]时, 修正left = mid; arr[i] < arr[i-1]时, 修正right = mid-1, 其实就算有多个山峰, 经过区间不断地缩小, 最终一定会变成上一题的只有一个山峰的情况, 无非多了两种一路上升和一路下降,  所以代码和上一题一模一样.

class Solution {
public:
    int findPeakElement(vector<int>& nums) 
    {
        int left = 0, right = nums.size()-1;
        while(left < right)
        {
            int mid = left + (right-left+1)/2;
            if(nums[mid] < nums[mid-1])
                right = mid-1;
            else
                left = mid;
        }
        return left;
    }
};

题目7: 搜索旋转排序数组中的最小值

以这段数组为例, 以最小值为分界点, 左侧区间内的值一定大于D点的值, 右侧区间内的值一定小于等于D点的值, 根据这个性质就可以把数组分为两段, 题目变成求右区间的左端点:

class Solution {
public:
    int findMin(vector<int>& nums) 
    {
       int left = 0, right = nums.size()-1;
       int end = right;
       while(left < right)
       {
            int mid = left + (right-left)/2;
            if(nums[mid] < nums[end])
                right = mid;
            else
                left = mid+1;
       }
       return nums[left];
    }
};

如果我们用A点作为判断点, 左侧区间内的值都大于等于A点, 右侧区间内的值都小于A点, 可以吗?

不完全可以, 这种情况下需要单独去判断如果数组是否是完全递增的情况, 这时left会一直++直到最右侧, 此时是区间的最大值. 而用D点作为判断点则不用考虑这个情况.

class Solution {
public:
    int findMin(vector<int>& nums) 
    {
       int left = 0, right = nums.size()-1;
       int end = right;
       while(left < right)
       {
            int mid = left + (right-left)/2;
            if(nums[mid] >= nums[0])
                left = mid+1;
            else
                right = mid;
       }    
       if(nums[end]-nums[0] > 0)
            return nums[0];
       return nums[left];
    }
};

题目8: 点名

此题以缺失的那个值作为分界点, 可以把区间划分为两块, 左区间内的值 下标数组内对应的值 是相等的, 右区间 下标数组内对应的值 是不相等的, 以此来将数组划分为两块. 需要注意, 如果最后求出来的返回值 下标 和 数组内对应的值 还是相等的, 说明缺失的是学号为n-1的那个人, 要返回left+1:

class Solution {
public:
    int takeAttendance(vector<int>& records) 
    {
        int left = 0, right = records.size()-1;
        while(left < right)
        {
            int mid = left + (right - left)/2;
            if(records[mid] == mid)
                left = mid+1;
            else
                right = mid;
        }
        if(records[left] == left)
            return left+1;
        return left;
    }
};

 此题还有若干时间复杂度为O(n)的方法:

1. 遍历数组哈希表存储出现次数, 没出现的就是缺失的

2. 直接遍历找结果

3. 位运算, 根据相同的数字异或等于0的性质, 将前n-1个数异或求和, 然后与数组中的数再异或, 最终的结果就是缺失的值

4. 等差数列求和, 求前n-1项的和再减去数组元素的和, 得到的就是缺失的值.


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

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

相关文章

【Office学习】Word参考文献交叉引用之连续引用

Word中为实现连续多个参考文献的的引用&#xff0c;如[1-3] 通过交叉引用至内容中的【1】和【3】不必多说&#xff0c;关键问题是如何变成[1-3]&#xff0c;如果强行删掉两边括号&#xff0c;更新后会恢复原样不说&#xff0c;打印或者转为PDF无法识别到删除的修改。 例如 1…

北斗卫星在桥隧坡安全监测领域的应用及前景展望

北斗卫星在桥隧坡安全监测领域的应用及前景展望 北斗卫星系统是中国独立研发的卫星导航定位系统&#xff0c;具有全球覆盖、高精度定位和海量数据传输等优势。随着卫星导航技术的快速发展&#xff0c;北斗卫星在桥隧坡安全监测领域正发挥着重要的作用&#xff0c;并为相关领域…

C++ 网络编程学习五

C网络编程学习五 网络结构的更新单例模式懒汉单例模式饿汉单例模式懒汉式指针智能指针设计单例类 服务器优雅退出asio的多线程模型IOServiceasio多线程IOThreadPoolepoll 和 iocp的一些知识点 网络结构的更新 asio网络层&#xff0c;会使用io_context进行数据封装&#xff0c;…

Typora设置文本颜色

目录 总共分为三种方法 1.使用markdown语法的内联公式 2.使用HTML语法 3.借助第三方软件&#xff08;不建议&#xff0c;操作没那么顺滑&#xff09; 总共分为三种方法 1.使用markdown语法的内联公式 <1>首先需要在设置中勾选Markdown扩展语法下的内联公式&#xff…

QML| QML 组件

# | QML 组件 | 组件是可重用的、封装好的QML类型,并提供了定义好的接口。组件一般使用一个.qml文件定义。前面讲到的使用QML文档定义对象类型,其实就是创建了一个组件。这种使用独立QML文件创建组件的方法这里不再讨论。除了使用单独的QML文件,还可以使用Component类型在一…

DVWA 靶场搭建

文章目录 环境说明1 DVWA 简介2 DVWA 安装 环境说明 操作系统&#xff1a;Windows 10PHPStudy 版本: 8.1.1.3Apache 版本&#xff1a;2.4.39MySQL 版本 5.7.26 1 DVWA 简介 DVWA&#xff08;Damn Vulnerable Web App&#xff09;是一个基于 “PHP MySQL” 搭建的Web应用程序&…

【linux】进程(一)

先看预备知识&#xff0c;对本篇文章更有帮助。 目录 进程概念&#xff1a;了解动态运行的概念&#xff1a;进程的本身内部属性&#xff1a;启动进程&#xff1a;关闭进程&#xff1a; 如何创建进程&#xff1a;进程状态&#xff1a;直接看进程状态&#xff1a;僵尸进程与孤儿…

L2-002 链表去重(Python)

给定一个带整数键值的链表 L&#xff0c;你需要把其中绝对值重复的键值结点删掉。即对每个键值 K&#xff0c;只有第一个绝对值等于 K 的结点被保留。同时&#xff0c;所有被删除的结点须被保存在另一个链表上。例如给定 L 为 21→-15→-15→-7→15&#xff0c;你需要输出去重后…

golang中new和make的区别

1. 先看一个例子 package mainimport "fmt"func main() {var a *int*a 10fmt.Println(*a) }运行结果是啥呢&#xff1f; 问&#xff1a;为什么会报这个panic呢&#xff1f; 答&#xff1a;因为如果是一个引用类型&#xff0c;我们不仅要声明它&#xff0c;还要为…

林木园区改造VR仿真培训课件提高人们的专业素质

森林经营VR模拟体验摆脱了传统森林经营周期长、实践难及耗材大等问题&#xff0c;借助VR虚拟仿真技术为人们提供一种全新的、沉浸式的森林经营体验&#xff0c;让人们更好地了解森林经营的全周期。 提高人们的环保意识 通过亲身参与森林经营的过程&#xff0c;人们可以更直观地…

【学习心得】字符验证码逆向思路

一、验证码验证流程 首先在破解验证码之前&#xff0c;需要知己知彼才能百战百胜&#xff0c;所以通过下面这张图来了解一下验证码是如何起作用的。下面以登录业务为例。 &#xff08;1&#xff09;简单流程 &#xff08;2&#xff09;请求与响应数据包流程 二、破解验证码一般…

Linux中防火墙相关操作

一、查看防火墙状态 可通过两种方式查看防火墙状态&#xff0c;一种通过systemctl命令&#xff0c;另一种是通过firewall-cmd命令。 1、systemctl status firewalld 2、firewall-cmd --state 二、关闭防火墙 1、暂时关闭&#xff1a;设置暂时关闭防火墙将会在下次重启系统后失…

永热爱 敢向前 | Paraverse平行云的2023 年终总结

永热爱&#xff0c;敢向前 值此新年&#xff0c;回顾2023&#xff0c;仅以此句&#xff0c;献给所有XR产业信仰者 2023 年&#xff0c;是XR产业技术和场景承上启下的关键之年 在这场波澜壮阔的技术潮中 「Paraverse平行云」踏浪前行 已是第八个年头&#xff0c;让我们一起…

BMW配送流程:通过EDI对接VDLP

BMW的汽车配送流程始于汽车“生产结束”&#xff0c;结束于“交付给宝马经销商”。BMW与其物流服务供应商之间没有直接的接口&#xff0c;EDI信息将会通过BMW的EDI供应商提供的VDLP&#xff08;车辆分销物流平台&#xff09;进行交换。 近期我们收到来自国内某汽车行业供应商L公…

【DFS算法】排列数字——acwing 842

问题描述 给定一个整数 n&#xff0c;将数字 1∼n 排成一排&#xff0c;将会有很多种排列方法。 现在&#xff0c;请你按照字典序将所有的排列方法输出。 输入格式 共一行&#xff0c;包含一个整数 n。 输出格式 按字典序输出所有排列方案&#xff0c;每个方案占一行。 数…

力扣题目训练(20)

2024年2月13日力扣题目训练 2024年2月13日力扣题目训练594. 最长和谐子序列598. 区间加法 II599. 两个列表的最小索引总和284. 窥视迭代器287. 寻找重复数135. 分发糖果 2024年2月13日力扣题目训练 2024年2月13日第二十天编程训练&#xff0c;今天主要是进行一些题训练&#x…

Android图片加载-Glide4,Android多进程从头讲到尾

open fun load(context: WeakReference, url: String?, image: ImageView?, transformation: BitmapTransformation) { if (image null) return // 具体图片加载逻辑 } open fun load(holder: Int, context: WeakReference, url: String, image: ImageView?, width: Int, …

JAVA 服务可观测性最佳实践

前言 本次实践主要是介绍 Java 服务通过无侵入的方式接入观测云进行全面的可观测。 环境信息 系统环境&#xff1a;Ubuntu&#xff08;主机环境&#xff09;开发语言&#xff1a;JDK 11.0.18Web 框架&#xff1a;SpringBoot日志框架&#xff1a;LogbackAPM 探针&#xff1a;…

C语言-存储期

C语言中&#xff0c;变量都是有一定的生存周期的&#xff0c;所谓生存周期指的是从分配到释放的时间间隔。为变量分配内存相当于变量的诞生&#xff0c;释放其内存相当于变量的死亡。从诞生到死亡就是一个变量的生命周期。 根据定义方式的不同&#xff0c;变量的生命周期有三种…

C#,子集和问题(Subset Sum Problem)的算法与源代码

1 子集和问题(Subset Sum Problem) 给定一组非负整数和一个值和,确定给定集合中是否存在和等于给定和的子集。 示例: 输入:set[]={3,34,4,12,5,2},sum=9 输出:真 有一个子集(4,5)和9。 输入:设置[]={3,34,4,12,5,2},和=30 输出:False 没有加起来…