当前位置: 首页 > news >正文

算法篇----二分查找

由于我们之前已经在学习C/C++过程中写过一些二分查找的代码,这里不在赘述其定义

1、综述

实不相瞒的说,二分查找是所有算法中最恶心,细节最多,最容易写出死循环的算法,但是当你熟练掌握后,这也是最简单的算法

之前或许网上听到有人说,这种算法只能用于数组有序的情况,实际上则不然,具体应用场景我会从例子中抽象出来,之后便是其有固定的模板,但是建议大家不要死记硬背,要理解之后在记忆!模板总共可以划分为三大类:朴素的二分模板,查找左边界的二分模板,以及查找右边界的二分模板,其中第一种很简单,但是局限性比较大,后两者是万能的,但是细节会比较多!

2.例题详解

2.1 二分查找

这道题要求很简单,就是要求我们在一个数组里面找到指定的数值,相比大家看到这个题应该就有解法了吧?

方法一:暴力破解

我们就遍历数组,值为targrt就返回下标,走了一圈啥也没找到就返回-1

方法二:二分查找

由于已经是升序了,我们就不用再排了

我们还是先设置两个指针,一个指向第一个数,叫left,另一个指向最后一个,叫right,随后我们再设置一个指针mid指向数组的中间位置,这个时候,问题来了,数组元素有可能是奇数也有可能是偶数,那怎么办呢?这个问题我们稍后再讲,留个悬念~

我们先完成主线任务,当我们的arr[mid]<target时,说明我们的mid所指元素比target小,那么mid左侧的数我们是不是就不用看了?直接让left=mid+1就好了,之后再对[left,right]区间重复操作!

当我们的arr[mid]>target时,说明我们的mid所指元素比target大,那么mid右侧的一坨数我们是不是就不用看了?直接让right=mid-1就好了,之后再对[left,right]区间重复操作!

当我们的arr[mid]==target时,说明我们的mid所指元素等于target,那直接返回下标Mid就欧克了。

现在我们看一下mid怎么求,如果你用(left+right+1)/2的话可能会有溢出,这里不建议这样做,这里推荐一种防止溢出的方法,即让Left向右移动一半的数组长度的距离不就可以了吗?所以我们的Mid=left+(right-left)/2.由于这里是朴素的模板写法,所以mid=left+(right-left+1)/2也可以

注意:我们每一次查找的小区间都是未知的,所以循环条件应该是left<=right

代码示例:

class Solution {
public:int search(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 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;elsereturn ...;
}

2.2 查找指定元素的第一个和最后一个位置 

34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

这道题给定我们一个非降序的数组,让我们找两个位置,使其满足题目条件,首先我们要理解好什么是非降序,他与升序有什么区别!!!

解法一)暴力破解:遍历数组元素

我们可以从左到右遍历数组,遇到值为target的数,就返回下标,并求得下标的最小值和最大值就Ok,但是此种方法的时间复杂度为O(N).

解法二)二分查找:朴素版

我们能否使用上一题的朴素解法来解决呢?可以是可以,但是在极端情况下,时间复杂度还是O(N),因为当我们的数组元素要都是target的话,那我们就相当于要遍历一遍整个数组了。

解法三)二分查找:左边界+右边界 

题目既然要求我们找左边界和右边界,那我们换一个想法,不就是相当于找最边界的两个数吗?那要是这样的话,我们为什么不把题目一分为二呢?一个去二分找左边界,另一个二分去找右边界!
 

我们来分析左边界的情况

我们先来分析一下总体的情况,假设我们找到了数组的中间部位,并且其指向元素为x,那么无非会分为以下几种情况:

情况一)x<t,这种情况下,我们的中间值的值要小于target,所以Mid左边的就不用看了,直接让left=mid+1就好,没有必要让left=mid,因为Mid这里的也不符合,会多此一举!

情况二)x>=t,这种情况下,我们的中间值的值要大于等于target,那说明我们mid右侧的元素就不用看了,直接让right=mid就好,注意,这里不能是mid-1,因为我们的mid有可能也是等于target的,这一点要区别于朴素版!

首先就是前面的问题,由朴素版可知,中点有两个公式可以求:

公式一)mid=left+(right-left)/2

公式二)mid=left+(right-left+1)/2

那我们用哪个公式好呢?

我们可以验证一下:

当数组为偶数时,公式一的mid偏左,公式二的mid偏右

假设此时数组里面只有两个元素的时候

倘若用公式一,当x==target的时候,mid指的是1位置,之后直接让right=mid,left直接跳过了mid,不会造成死循环,

但是倘若用公式二,当x==target的时候,mid指的是2位置,本身也是right指向的位置,之后你再让right=mid,相当于你没动,卡死了!

因此在左边界的选择中,我们选公式一!

同理,在分析一下右边界的情况,

我们先来分析一下总体的情况,假设我们找到了数组的中间部位,并且其指向元素为x,那么无非会分为以下几种情况:

情况一)x<=t,这种情况下,我们的中间值的值要小于target,所以Mid左边的就不用看了,直接让left=mid就好,不能让left=mid+1,因为Mid可能也符合!

情况二)x>t,这种情况下,我们的中间值的值要大于target,那说明我们mid右侧的元素就不用看了,直接让right=mid-1就好,注意,这里没有必要是mid,因为我们的mid也是大于target的

首先还是前面的问题,由朴素版可知,中点有两个公式可以求:

公式一)mid=left+(right-left)/2

公式二)mid=left+(right-left+1)/2

那我们用哪个公式好呢?

我们可以验证一下:

当数组为偶数时,公式一的mid偏左,公式二的mid偏右

假设此时数组里面只有两个元素的时候

倘若用公式一,当x==target的时候,mid指的是1位置,本身也是left指向的位置,之后你再让left=mid,相当于你没动,卡死了!

但是倘若用公式二,当x==target的时候,mid指的是2位置,之后直接让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,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 

2.3 x的平方根

 69. x 的平方根 - 力扣(LeetCode)

这道题要求我们找一个数的算术平方根,并返回整数部分即可,并且题目明确指出,不能使用库函数例如pow之类的,那我们应该怎么解决呢?

解法一)暴力破解

这道题的暴力破解方法应该还是比较容易想到的,我们就1开始一一例举每个数的平方就好了,找到符合的数就返回就ok

解法二)二分查找

题目已经给定了我们数x,那么我们只需要在区间[1,x]内寻找即可,二分后无非就是有两种情况,<=x和>x,对于情况一,说明我们二分的这个点的平方小于等于x,那我们就让left=mid就好,之后继续找,对于情况二,说明我们二分的这个点的平方大于x,那我们就让right=mid-1就好,之后继续找

参考代码:

class Solution {
public:int mySqrt(int x) {if(x<1) return 0;  //处理边界情况int left=1,right=x;while(left<right){long long mid=left+(right-left+1)/2;   //防溢出if(mid*mid<=x) left=mid;else right=mid-1;}return left;}
};

2.4 插入、查找数据 

35. 搜索插入位置 - 力扣(LeetCode)

这道题题目要求很简单,找到目标值就返回下标,找不到就按照其应该在的顺序返回下标

解题思路:

题目都指定说时间复杂度要O(log n),那明摆着就是要二分查找了,我们还是分析情况,假设Mid指向的元素为x

情况一:x<t   -> left=mid+1

情况二:x>=t  ->right =mid 

问题解决!套代码!

参考代码:

暴力破解:

class Solution {
public:int searchInsert(vector<int>& nums, int target){int flag=0;int b= nums.back();if(b<target){return nums.size();}else{for(int i=0;i<nums.size();i++){if(nums[i]==target){flag=i;break;}else{if(target>nums[i]&&target<nums[i+1]){flag= i+1;}}}}return flag;}
};

二分查找:

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(nums[left]<target) return right+1;return left;}
};

2.5 山峰数组峰顶索引

852. 山脉数组的峰顶索引 - 力扣(LeetCode)

这道题要求我们找山峰数组的峰顶索引,我们还是有两种方法来解决这个题目

解题思路:

方法一)暴力破解

这道题的解决方法也很简单,就是看arr[i]与arr[i+1]的关系,如果arr[i]>arr[i-1]&&arr[i]<arr[i-1],那么他就是峰顶,我们返回下标就可以了

方法二)二分查找

我们还是先找到中间点,之后判断arr[mid]与arr[mid-1]的关系,

如果arr[mid]>arr[mid-1]  ->  left=mid

如果arr[mid]<arr[mid-1]   ->  right=mid-1

参考代码:

class Solution {
public:int peakIndexInMountainArray(vector<int>& arr) {int left=1,right=arr.size()-2;     //最边上的两个一定不是峰顶while(left<right){int mid=left+(right-left+1)/2;if(arr[mid]>arr[mid-1])  left=mid;else right=mid-1;}return left;}
};

 2.6 寻找峰顶

162. 寻找峰值 - 力扣(LeetCode)

这个题也是呀求我们去找一个峰值,但是与上一题不同的是,这道题的山峰可能是”重峦叠嶂“的,那么我们应该怎么解决这道题吗?

解题思路:

方法一)暴力破解

这道题的暴力解法比较纯粹,就是从第一个位置开始,一直向后走,分情况讨论即可,我们总共可以分为如下三种情况:

方法二)二分查找

这道题说时间复杂度要O(log N),那无疑就是在暗示你用二分查找,通过这个题也证明了一件事,就是二分查找的使用前提条件并不是只能用于有序的数组,像这种无序的也是可以的!

好,我们具体看一下怎么解决这道题:

肯定还是要从山峰的特点下手,假设我们取得了中点Mid,可能就会有两种情况:
1、arr[mid]>arr[mid+1]   -->right=mid

2、arr[mid]<arr[mid+1]   -->left=mid+1

可以参考下图理解:

说明m往右的局部都不符合了,缩小右端点范围

 说明m+1往左的局部都不符合了,缩小左端点范围

 参考代码:

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

2.6搜索旋转排序数组中的最小值

153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)

这道题要我们找最小值的下标,解决方法如下:

方法一)暴力破解

就是挨个访问数组,找出最小值返回下标

方法二)二分查找

旋转后的数组是这样的:

其中 C 点就是我们要求的点因此,当 mid 在 [AB] 区间的时候,也就是 mid 位置的值严格大于 D 点的值,下一次查询区间在 [mid + 1right] 上; 当 mid [CD] 区间的时候,也就是 mid 位置的值严格小于等于 D 点的值,下次查询区间在 [leftmid] 上。 

参考代码:

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

2.7 缺失的数字

LCR 173. 点名 - 力扣(LeetCode)

这道题解题方法有很多种

解题思路

方法一)哈希表

直接把数组元素放到哈希表里面,看哪个位置是0就完事了,之后进行返回

方法二)直接遍历找结果

正常都是后一个比前一个大1,要是突然大2那就是这里有缺失,返回数值就好

方法三)位运算

由C语言我们可知,a^a==0,那么我们造一个完整的数组,让他们一起进行位运算,剩的就是那个缺失的数

方法四)高斯求和

我们可以先求一个完整的数组的和,在挨个减去这个已知数组的每个元素,剩下的就是缺失的值

方法五)二分查找

实际上,这个数组是有二段性的,我们不妨画一下图:

我们发现在缺之前,都是数值和下标相等的,但是缺之后就不相等了,因此我们可以使用二分查找算法,具体操作如下:

当mid落在左区间时,即arr[mid]==mid 时,说明我们要向右查找,因此让left==mid+1 

当mid落在右区间时,即arr[mid]!=mid 时,说明我们要向左查找,因此让right==mid

最后还有一个小的细节,就是假设[0,1,2,3,4]缺的是4,如下图:

这种情况我们在代码里面用一个小的三目表达式就能处理好了!

参考代码:

class Solution {
public:int takeAttendance(vector<int>& nums) {//解法5int left=0,right=nums.size()-1;while(left<right){int mid=left+(right-left)/2;if(nums[mid]==mid)  left=mid+1;else right=mid;}//处理细节问题return nums[left]==left?left+1:left;}
};

 

二分查找到此结束,接下我们将更新前缀和算法!

相关文章:

  • O2O电商变现:线上线下相互导流——基于定制开发开源AI智能名片S2B2C商城小程序的研究
  • #Redis黑马点评#(六)Redis当中的消息队列
  • k8s备份namespace
  • 多模态信息提取:打通数据价值的“最后一公里”
  • TDengine 在新能源领域的价值
  • 限流算法 + dfa敏感词过滤算法
  • MATLAB中的Switch语句讲解
  • 基于matlab/simulink锂电池算法学习集合(SOC、SOH、BMS)
  • React Flow 边的基础知识与示例:从基本属性到代码实例详解
  • LLM笔记(九)KV缓存调研
  • std::is_same
  • 5月18总结
  • leetcode报错原因总结需要背下来的程序片 [更新中]
  • 三:操作系统线程管理之线程概念
  • 2025年全国青少年信息素养大赛C++小学全年级初赛试题
  • 逻辑与非逻辑的弥聚
  • 【Linux】第二十章 管理基本存储
  • 双紫擒龙紫紫红指标源码学习,2025升级版紫紫红指标公式-重点技术
  • 基于单片机路灯自动控制仪仿真设计
  • 创建型:工厂方法模式
  • 三星“七天机”质保期内屏幕漏液被拒保,澎湃介入后已解决
  • 体坛联播|雷霆抢七淘汰掘金,国米错失意甲登顶良机
  • 1块钱解锁2万部微短剧还能日更,侵权盗版难题怎么破?
  • 新闻1+1丨强对流天气频繁组团来袭,该如何更好应对?
  • 上海国际碳中和博览会下月举办,首次打造民营经济专区
  • 继71路之后,上海中心城区将迎来第二条中运量公交