算法篇:二分查找

目录

介绍

查找数组中值算法模板

左右边界模板

实例

二分查找(easy)

在排序数组中查找元素的第一个和最后一个(medium)

搜索插入位置(easy)

[x 的平方根(easy)](#x 的平方根(easy))

山峰数组的峰顶(easy)

寻找峰值(medium)

搜索旋转排序数组中的最小值(medium)

[0〜n-1 中缺失的数字(easy)](#0〜n-1 中缺失的数字(easy))


介绍

核心思想:每次都通过比较中间元素,将查找范围缩小一半,直到找到目标或确认目标不存在。

必要条件:

  • 数据结构一定要是有序的(升序或降序)
  • 支持随机访问(如数组、列表),链表不适合

查找数组中值算法模板

左右边界模板

左边界:

右边界:

如何确定题目到底是用哪个边界的算法呢?

  • 可以直接假设mid在我们想找的答案上,然后根据上面的模板和判断条件的要求,看看要缩小那一边的区域。
  • 左边界找的是满足判断条件的第一个数,右边界是找满足条件的第二个数

实例

二分查找(easy)

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

该题目中的数组是升序的,可以考虑用二分查找。

算法流程:

a. 定义 left , right 指针,分别指向数组的左右区间。

b. 找到待查找区间的中间点 mid ,找到之后分三种情况讨论:

  • arrmid == target 说明正好找到,返回 mid 的值;
  • arrmid > target 说明 mid, right 这段区间都是大于 target 的,因此舍去右边区间,在左边 left, mid -1 的区间继续查找,即让 right = mid -1 ,然后重复 2 过程;
  • arrmid < target 说明 left, mid 这段区间的值都是小于 target 的,因此舍去左边区间,在右边 mid + 1, right 区间继续查找,即让 left = mid +1 ,然后重复 2 过程;

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

复制代码
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;
    }
};

在排序数组中查找元素的第一个和最后一个(medium)

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

题目解析:在一个升序数组中,找到一段全是目标值的区间,返回全是目标值的第一个下标和最后一个目标值的下标。

假设左边界下标是resleft,目标值为x,右边界下标为resright

寻找左边界思路:

根据左边界进行划分成两个区域:

  • left,resleft-1都是小于x的
  • resleft,right都是大于等于x的

因此根据mid的落点,我们可以分为两个情况

  • 当mid落在left,resleft-1区域内是,即numsmid<x,那么这个区域内的值是可以全部舍去的,所以让left=mid+1,继续在mid+1,right中寻找左边界
  • 当mid落在resleft,right区域内,即numsmid>=x,说明mid+1,right可以舍去,但mid位置可能是最终结果,所以right更新成mid,继续在left,mid中寻找左边界

寻找右边界思路:

根据右边界进行划分成两个区域:

  • left,resright都是小于等于x的
  • resright+1,right都是大于x的

因此根据mid的落点,我们可以分成两种情况:

  • 当mid落在left,resright区域内是,即numsmid<=x,那么left,mid-1的值是可以全部舍去的,但mid位置可能是最终结果,所以让left=mid,继续在mid,right中寻找右边界
  • 当mid落在resright+1,right区域内,即numsmid>x,说明mid,right可以舍去,所以right更新成mid-1,继续在left,mid-1中寻找右边界

细节部分:

中间值有两种取法:

mid=left+(right-left)/2 //向下取整

mid=left+(right-left+1)/2//向上取整

当left和right即将相遇时,可能出现死循环:

寻找左边界时:假设left=1,right=2,left=mid+1,right=mid,如果mid=left+(right-left+1)/2,那么mid=2,因为right=mid,导致一直right都没有向前走一步,让left==right让循环结束,所以我们寻找左边界的时候要向下取整。

同理可得右边界

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

        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};
    }
};

搜索插入位置(easy)

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

题目解析:在一个升序数组中找到target的下标,如果target不存在就返回它在数组中处在什么下标可以让数组依旧是升序所在的下标。

根据第二个案例,假设mid在下标为1的地方,然后当前数组中的下标为1的数大于target(3>2),判断条件是numsmid与target的关系,3>2,说明题意要求的值在左边,所以可以直接舍去右边的值,这时候就是right移动,right有两种移动,一个是right=mid,另一个是right=mid-1。如果选择第二个,那么正确答案就被排除在外了,所以选第一个,第一个在左右边界模板中寻找左边界模板。

解题思路:

假设target所在数组中的下标是i;

那么我们可以把数组分为两个区域:

  • left,i-1:全部小于target;
  • i,right都是大于等于target

根据这个区域划分,我们可以知道其实这个题相当于找左边界。

因此根据mid的落点,我们可以分为两个情况

  • 当mid落在left,i-1区域内是,即numsmid<x,那么这个区域内的值是可以全部舍去的,所以让left=mid+1,继续在mid+1,right中寻找

  • 当mid落在i,right区域内,即numsmid>=x,说明mid+1,right可以舍去,但mid位置可能是最终结果,所以right更新成mid,继续在left,mid中寻找

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

x 的平方根(easy)

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

题目解析:找出x的平方根,即给x开根号,如果x开根号后有小数就只保留整数。

根据第二个案例,假设mid在等于2的地方,然后当前数组中的2*2的数小于target(4<8),找的值在右边,,所以舍去左边的值,这时候就是left移动,left有两种移动,一个是left=mid,另一个是left=mid+1。如果选择第二个,那么正确答案就被排除在外了,所以选第一个,第一个在左右边界模板中寻找右边界模板。

解题思路:假设x的最终平方根结果是index

把区域划分为两部分:

left,index的平方根全部都是小于等于x的

index+1,right的平方根全部大于x

具有二分性

因此根据mid的落点,我们可以分成两种情况:

  • 当mid落在left,i区域内是,即numsmid<=x,那么left,mid-1的值是可以全部舍去的,但mid位置可能是最终结果,所以让left=mid,继续在mid,right中寻找

  • 当mid落在i+1,right区i域内,即numsmid>x,说明mid,right可以舍去,所以right更新成mid-1,继续在left,mid-1中寻找

    class Solution {
    public:
    int mySqrt(int x) {
    long long left=1,right=x;
    if(x==0)return 0;
    while(left<right)
    {
    long long mid=left+(right-left+1)/2;
    if(mid*mid<=x)left=mid;
    else right=mid-1;
    }
    return left;
    }
    };

山峰数组的峰顶(easy)

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

题目解析:在一个先单调递增然后单调递减的数组中,找到单调递增的结束的最大值。

峰值有个明显特征就是只要比后面的那个数大就是峰值。

方法一:暴力解法

复制代码
class Solution {
public:
int peakIndexInMountainArray(vector<int>& arr) {
    int n = arr.size();
    // 遍历数组内每⼀个元素,直到找到峰顶
    for (int i = 1; i < n - 1; i++)
    // 峰顶满⾜的条件
        if (arr[i] > arr[i - 1] && arr[i] > arr[i + 1])
        return i;
    // 为了处理 oj 需要控制所有路径都有返回值
    return -1;
}
};

方法二:

根据峰值所在位置可以把数组分成两部分,一部分单调递增,一部分单调递减,具有二分性。

根据第二个案例,假设mid在下标为1的地方,然后当前数组中的下标为1的数大于后一个数,说明目标值在左边(前半部分单调递增),可以舍去右边的区域,这时候就是right移动,right有两种移动,一个是right=mid,另一个是right=mid-1。如果选择第二个,那么正确答案就被排除在外了,所以选第一个,第一个就是左右边界模板中寻找左边界模板。

解题思路:

假设target所在数组中的下标是i;

那么我们可以把数组分为两个区域:

  • left,i-1:全部小于target;
  • i,right都是大于等于target

根据这个区域划分,我们可以知道其实这个题相当于找左边界。

因此根据mid的落点,我们可以分为两个情况

  • 当mid落在left,i-1区域内是,即numsmid<x,那么这个区域内的值是可以全部舍去的,所以让left=mid+1,继续在mid+1,right中寻找

  • 当mid落在i,right区域内,即numsmid>=x,说明mid+1,right可以舍去,但mid位置可能是最终结果,所以right更新成mid,继续在left,mid中寻找

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

寻找峰值(medium)

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

解题方法和上题一样

复制代码
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 right;
    }
};

搜索旋转排序数组中的最小值(medium)

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

题目解析:找一个数组中的最小值。这个数组中有两个都是单调递增的

方法一:暴力解法

一直标记一个最小值进行比较

复制代码
class Solution {
public:
    int findMin(vector<int>& nums) {
        int n = nums.size();
        int minnum=INT_MAX;
        int index=-1;
    // 遍历数组内每⼀个元素,直到找到最小值
    for (int i = 0; i < n; i++)
    {
        if(nums[i]<minnum)
        {
            minnum=min(minnum,nums[i]);
            index=i;
        }
    }
    return nums[index];
    }
};

方法二:二分查找

该数组有两部分单调递增,具有二分性,可以考虑二分查找

判断条件的选择:分析数组,我们发现在最后的一个数的前面的值,只有它前面几个数比它小,其他都比它大,所以我们的判断条件就是和最后一个数比较。

左右边界的判断:我们要找的是第一个满足要求的数,所以用左边界。

解题思路:

假设target所在数组中的下标是i;

那么我们可以把数组分为两个区域:

  • left,i-1:全部小于target;
  • i,right都是大于等于target

根据这个区域划分,我们可以知道其实这个题相当于找左边界。

因此根据mid的落点,我们可以分为两个情况

  • 当mid落在left,i-1区域内是,即numsmid<x,那么这个区域内的值是可以全部舍去的,所以让left=mid+1,继续在mid+1,right中寻找

  • 当mid落在i,right区域内,即numsmid>=x,说明mid+1,right可以舍去,但mid位置可能是最终结果,所以right更新成mid,继续在left,mid中寻找

    class Solution {
    public:
    int findMin(vector& 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)right=mid;
    else left=mid+1;
    }
    return nums[left];
    }
    };

0〜n-1 中缺失的数字(easy)

LCR 173. 点名 - 力扣(LeetCode)

题目解析:在一个按顺序排列的数组中找缺的数。数组单调递增,可以考虑二分查找。

方法一:暴力解法

复制代码
class Solution {
public:
    int takeAttendance(vector<int>& records) {
        if(records.size()==1&&records[0]==0)return 1;
        for(int i=0;i<records.size();i++)
        {
            if(i!=records[i])return i;
        }
        int n=records.size()-1;
        if(n==records[n])
        return n+1;
        else
        return -1;
    }
};

方法二:二分查找

判断条件:可以舍去一部分的条件,如果mid==recordsmid是不是说明前面一部分的都没有缺人,所以前面一部分可以舍去,让left=mid+1

假设target所在数组中的下标是i;

那么我们可以把数组分为两个区域:

  • left,i-1:全部小于target;
  • i,right都是大于等于target

根据这个区域划分,我们可以知道其实这个题相当于找左边界。

因此根据mid的落点,我们可以分为两个情况

  • 当mid落在left,i-1区域内是,即numsmid==mid,那么这个区域内的值是可以全部舍去的,所以让left=mid+1,继续在mid+1,right中寻找

  • 当mid落在i,right区域内,即numsmid!=mid,说明mid+1,right可以舍去,但mid位置可能是最终结果,所以right更新成mid,继续在left,mid中寻找

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

相关推荐
Darling噜啦啦1 分钟前
列表转树算法深度解析:从 Map 到 Reduce 的两种实现,面试高频考点
数据结构·算法·面试
clint4562 小时前
C++进阶(1)——前景提要
c++
用户497863050733 小时前
(一)小红的数组操作
算法·编程语言
夜悊6 小时前
C++代码示例:进制数简单生成工具
c++
怕浪猫6 小时前
Electron 系列文章封面图
算法·架构·前端框架
郝学胜_神的一滴7 小时前
CMake 021: IF 条件判据详诠
c++·cmake
徐小夕8 小时前
JitWord 3.0 正式发布,高精度Word异构解析+复杂组件兼容,打造web端协同Word编辑器
前端·vue.js·算法
_wyt00121 小时前
洛谷 B3930 [GESP202312 五级] 烹饪问题 题解
c++·gesp
通信小呆呆1 天前
当算法有了“五感”:多模态数据融合如何向人体感官协同学习?
人工智能·学习·算法·机器学习·机器人