从入门到入土:二分查找算法

本质:二分查找是用来在有序数组中快速查找目标值的最优算法

前提:只能用于有序数组/有序列表

核心思想:1.取数组的中间元素和目标值比较

2.根据大小关系,直接排除一般的元素

3.重复上述步骤,直到找到目标/确认不存在结果

cpp 复制代码
如果 nums[mid] == target,找到
如果 nums[mid] < target,说明目标在右半边
如果 nums[mid] > target,说明目标在左半边

因为每次都是直接排除一半的元素->时间复杂度:

cpp 复制代码
O(log n)

朴素模板:查找Target是否存在

704. 二分查找 - 力扣(LeetCode)

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int binarySearch(vector<int>& nums, int target)
{
    int left = 0;
    int right = nums.size() - 1;

    while (left <= right)
    {
        int mid = left + (right - left) / 2;

        if (nums[mid] == target)
        {
            return mid;
        }
        else if (nums[mid] < target)
        {
            left = mid + 1;
        }
        else
        {
            right = mid - 1;
        }
    }

    return -1;
}

需要注意的点是:1.循环判断条件为 **left<=right 这是因为当left=right,**还有一个元素需要检查

2.**mid = left+(right-left)/2,**防止整型溢出

特殊模板:左端点和右端点

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

这道题如果我们使用朴素模板的话,会发现无从下手。为了解决此类问题,我们引出了第二类二分查找模板 :第二类查找模板的差异主要体现在中点 mid 和 更新位置上

左端点:

先看示例,我们可以把数组分为两部分:小于target部分和大于等于target部分,这一部分主要用来查找符合条件的左端点即最小满足条件点!!!

如果我们的mid 落到了左侧区域(小于target 区域),我们明确知道左端点一定不会在此区域内,所以:

cpp 复制代码
left = mid + 1 ;

如果我们的mid 落到了右侧区域(大于target区域), 左端点是一定出现在这个区域内的,我们在更新right 的时候就需要注意会不会跳过可能端点,所以:

cpp 复制代码
right = mid;

right 直接更新到mid的位置就可以避免跳过情况的发生

接着是中点 mid

在**nums.size()**为奇数的前提下,我们不用考虑中点的位置问题,但是如果是偶数的前提下,就需要商榷了

如图所示, 如果是偶数的话,我们就需要考虑是左边的中点还是右边的中点了

在上述查找左端点的前提下,我们选择的端点就需要为 左侧的中点

因为:如果我们选择右侧的中点,那么right就会一直更新为mid这个位置,而永远不会存在left>right 这个情况的发生,导致进入死循环!!! 且 我们需要明确的一点是 当left = right 时,就是左端点,所以循环条件应该为while(left<right),如果等于直接会导致死循环!!!

右端点的判断跟左端点的判断如出一辙:

查找右端点即最大满足条件点

如果**mid<target(左侧区域),**满足条件点一定位于此区域,所以在更新left的时候需要注意:不要越过满足条件点!

cpp 复制代码
left = mid;

如果**mid>target(右侧区域),**满足条件点一定不存在此区域

cpp 复制代码
right = mid-1;

接着是中点问题

这两个都是偶数的中点啊,在右端点的情况下我们怎么选择呢?

当然是选择右侧的这个点了,原因跟左端点选择中点类似。如果选择左侧中点left会一直更新为mid ,而永远不会存在left = right的可能,导致陷入死循环,所以我们选择右侧中点

关于左端点还是右端点模板的代码其实还是非常简单的,唯一需要注意的就是left,right 的更新和mid的取值问题

我这里给出一个比较容易的判断方法,如果在left和right的更新中见到了 '-1'那么mid 的取值中就需要+1,即 mid = left+(right-left+1)/2;

下面给出本题的代码:

cpp 复制代码
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        //
        if(nums.size()==0) return {-1,-1};
        //1.寻找左端点
        int begin = 0;
        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直接继承就行,不用更改
        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};
    }
};

二分查找算法题:

此类问题的关键在于:找出 二段性

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

这道题我们先来判断是朴素模板还是左端点模板或者是右端点模板

根据题意:让我们返回一个非负整数的平方根,而且类似高斯函数,返回满足条件的最大点 ,那么就很清晰了,选择右端点模板,注意mid更新为右侧中点

cpp 复制代码
class Solution {
public:
    int mySqrt(int x) {
        if(x<1) return 0;
        int left = 1,right = x;
        //根据题意我们发现二段性,即左边是小于等于,右边是大于,我们需要找到左边的右端点,所以划分为
        // [1,2,3][4,5,6,7] target = 10.5
        while(left<right)
        {
            long mid = left+(right-left+1)/2;//防溢出
            if(mid*mid<=x) left = mid;
            else right = mid-1;//见到-1,上面+1
        }
        return left;
    }
};

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

先来判断选择什么模板,可以观察到插入位置为满足条件的最大点,至于为什么我一下子就看出来了,我来简单说一下我的看法:首先明确二段性,在插入位置的左侧是小于等于target的元素,在右侧是大于target的元素,观察到这个二段性之后,我们知道注意到了插入位置是找最大点,因为数组有序,最大点即为右端点,所以选择右端点模板

cpp 复制代码
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int left = 0,right = nums.size()-1;
        // 很明显我们发现题目具有二段性,即左边是小于target,右边是大于target,我们需要找到的是大于target中的左端点
        //我们发现插入的位置右边的数字统统大于等于目标值,直接选择左端点
        while(left<right)
        {
            int mid = left+(right-left)/2;
            if(nums[mid]<target) left = mid+1;
            else right = mid;
        }
        if(nums[nums.size()-1]<target) return nums.size();
        return left;
        
    }
};

还需要注意边界情况,其实这里可以写一个三目运算符:

cpp 复制代码
return nums[nums.size()-1]<target ?left+1:left;

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

在普通的二分查找中,我们通过比较 arr[mid]target 来决定往哪走。 而在山脉数组中,没有特定的 target,我们需要通过比较 arr[mid] 和它相邻元素(例如 arr[mid + 1])的大小关系,来判断当前处于山峰的左侧(上升段)还是右侧(下降段)

  • 情况一:arr[mid] < arr[mid + 1]

    • 含义 :当前处于上升段(山峰左侧)。

    • 结论 :峰顶一定在 mid 的右边。因此,我们需要将左边界收缩,令 left = mid + 1

  • 情况二:arr[mid] > arr[mid + 1]

    • 含义 :当前处于下降段 (山峰右侧,或者 mid 本身就是峰顶)。

    • 结论 :峰顶在 mid 的左边,或者就是 mid 本身。因此,我们需要将右边界收缩,令 right = mid

通过不断逼近,当 left == right 时,它们指向的位置必然就是峰顶。

cpp 复制代码
class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) {
        //根据分析,我们发现了这道题目的二段性,即左行的大小符合arr[mid]>arr[mid-1]
        //右行的大小符合arr[mid]<arr[mid]-1,所以我们可以使用二分查找
        int left = 0,right = arr.size()-1;
        while(left<right)
        {
            int mid = left+(right-left+1)/2;
            if(arr[mid]>arr[mid-1]) left = mid;
            else right = mid-1;
        }
        return left;
    }
};

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

这道题同理,没有Target,按趋势来作为 二段性 进行二分查找

cpp 复制代码
class Solution {
public:
    int findPeakElement(vector<int>& nums) {
        int left = 0,right = nums.size()-1;
        while(left<right)
        {
            int mid = left+(right-left)/2;
            //这里我们并不是很明确到底是寻找哪一个端点的话,我们可以tong过口诀 见-1 就 +1
            //对于题意的思考中,我们发现在第一个 nums[mid]>nums[mid+1]中,在他的左边一定有结果,所以我们不能让right = mid-1,必须等于mid
            //当 nums[mid]<=nums[mid+1],这时候右边一定右结果,所以left等于mid+1即可
            if(nums[mid]>nums[mid+1]) right = mid; 
            else left = mid+1;
        }
        return left;
    }
};

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

核心思路:与"右边界"进行博弈

在普通的二分查找中,我们通常拿 nums[mid]target 比。在这道题里,由于没有 target,我们要拿 nums[mid] 和当前区间的右边界 nums[right] 进行比较,以此判断 mid 落在第一段还是第二段。

  • 情况一:nums[mid] > nums[right]

    • 含义mid 落在左边较大的那段递增区间内(例如 [4, 5, 6, 7, 0, 1, 2] 中的 6)。

    • 结论 :既然 mid 比右边界还大,说明最小值一定在 mid右侧 。因此,我们要收缩左边界:left = mid + 1

  • 情况二:nums[mid] < nums[right]

    • 含义mid 落在右边较小的那段递增区间内,或者数组本身就是完全升序的(没有旋转)。

    • 结论 :此时 mid 可能是最小值,也可能最小值的左侧还有更小的值(都在 mid 的左边)。因此,最小值在 mid左侧(包含 mid 自身) 。我们要收缩右边界:right = mid

相关推荐
L_090712 小时前
【C++】数据结构之哈希表(散列表)
数据结构·c++·散列表
仰泳之鹅12 小时前
【C语言】动态内存管理
c语言·数据结构·算法
心中有国也有家12 小时前
CANN 学习新范式:cann-learning-hub 如何让昇腾入门不再「劝退」
人工智能·经验分享·笔记·学习·算法
LB211212 小时前
C++通讯录课设(西安石油大学)
开发语言·c++·算法
AI算法沐枫12 小时前
机器学习知识点:正则化
人工智能·pytorch·python·深度学习·神经网络·算法·机器学习
手写码匠12 小时前
从零实现一个轻量级向量搜索引擎(Python 版)
人工智能·深度学习·算法·aigc
_Evan_Yao13 小时前
数据结构太难了?用画图的方式理解链表和栈和树和图
数据结构·学习·链表
学习中.........13 小时前
多目标优化:遗传算法详解
人工智能·算法·机器学习
心中有国也有家14 小时前
hixl:昇腾分布式推理的「快递专线」
人工智能·经验分享·笔记·分布式·学习·算法