算法篇----二分查找

由于我们之前已经在学习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;
   else
     return ...;
}

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 在 [A,B] 区间的时候,也就是 mid 位置的值严格大于 D 点的值,下一次查询区间在 [mid + 1,right] 上; 当 mid 在 [C,D] 区间的时候,也就是 mid 位置的值严格小于等于 D 点的值,下次查询区间在 [left,mid] 上。

参考代码:

复制代码
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) 
    {
        //解法5
        int 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;
    }
};

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

相关推荐
ai.Neo10 分钟前
牛客网NC21989:牛牛学取余
c++·算法
啊我不会诶14 分钟前
二分交互题总结
算法·交互
AndrewHZ37 分钟前
【ISP算法精粹】什么是global tone mapping和local tone mapping?
人工智能·深度学习·算法·计算机视觉·视觉算法·isp算法·色调映射
虾球xz1 小时前
游戏引擎学习第299天:改进排序键 第二部分
c++·学习·算法·游戏引擎
IC 见路不走1 小时前
LeetCode 第61题:旋转链表
算法·leetcode·链表
阿方.9181 小时前
《C 语言 sizeof 与 strlen 深度对比:原理、差异与实战陷阱》
算法
制冷男孩1 小时前
机器学习算法-聚类K-Means
算法·机器学习·聚类
2401_878624792 小时前
机器学习 KNN算法
人工智能·算法·机器学习
Cachel wood2 小时前
算法与数据结构:质数、互质判定和裴蜀定理
数据结构·算法·microsoft·机器学习·数据挖掘·langchain
李长渊哦2 小时前
双指针法高效解决「移除元素」问题
数据结构·算法