【LeetCode】一文吃透二分查找(附练习)

二分查找深入浅出,一文吃透!

原文地址:https://github.com/EricPengShuai/Interview/blob/main/algorithm/二分查找.md

0. 引言

  1. 基本概念:二分查找就是从一个排好序的序列中查找一个元素,和顺序查找不同的是,二分查找通过逐步缩小搜索区间的方式将搜索范围逐渐缩小,最终定位到我们的目标值或者目标位置,在STL中,lower_bound()upper_bound() 就是利用二分的思想,前者在有序数组中找到第一个大于等于的目标值的位置,后者找到第一个大于目标值的位置
  2. 排版说明:
    • 首先是套用模板法,这个很简单,就是记住一个二分查找的模板,然后根据题意套用即可,可以解决99%的问题,但对于个别特殊的问题无法解决,主要参考的是 代码随想录图解二分
    • 其次是直接分析法,这个方法需要忘记所有的模板,没有所谓的「左闭右开」「左闭右闭」的概念,重点是认真理解题意,根据题目的条件分析如何缩减区间,主要参考的是 liweiwei

1. 套用模板法1

首先说明两个概念:

  1. 左闭右闭:搜索区间范围是 [left, right],此时循环条件是left <= right,因为left == right时区间也是存在的

    参考代码:

    int searchInsert(vector<int>& nums, int target) {
    	int n = nums.size();
    	int left = 0;
        int right = n; // 定义target在左闭右闭的区间里,[left, right]
        while (left <= right) {
            int middle = left + ((right - left) >> 1);
            if (nums[middle] > target) {
                right = middle - 1; // target 在左区间,在[left, middle-1]中
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,在[middle+1, right]中
            } else {
                return middle; // 数组中找到目标值的情况,直接返回下标
            }
        }
        // 这里需要根据题意分析!!
        return right+1;
    }
    
  2. 左闭右开:搜索区间范围是 [left, right),此时循环条件是left < right,因为left == right时区间不存在

    参考代码:

    int searchInsert(vector<int>& nums, int target) {
        int n = nums.size();
        int left = 0;
        int right = n; // 定义target在左闭右开的区间里,[left, right) 
        while (left < right) {
            int middle = left + ((right - left) >> 1);
            if (nums[middle] > target) {
                right = middle; // target 在左区间,在[left, middle)中
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,在 [middle+1, right)中
            } else {
                return middle; // 数组中找到目标值的情况,直接返回下标
            }
        }
        // 循环结束时有 right>=left,大多数情况下 left == right,但是为了不越界往往返回right
        return right;
    }
    

2. 套用模板法2

统一将判断条件定位 while(left < right),根据划分区间的不同选择不同的模板

  1. 模板一

    将区间 [left, right]划分为分成 [left, mid][mid + 1, right],此时计算 mid 不需要 +1

    int bsearch_1(int l, int r)
    {
        while (l < r)
        {
            int mid = (l + r)/2;
            if (check(mid)) r = mid;
            else l = mid + 1;
        }
        return l;
    }
    
  2. 模板二

    将区间 [left, right]划分为分成 [left, mid-1][mid, right],此时计算 mid 需要 +1

    int bsearch_2(int l, int r)
    {
        while (l < r)
        {
            int mid = ( l + r + 1 ) /2;
            if (check(mid)) l = mid;
            else r = mid - 1;
        }
        return l;
    }
    

说明:在大多数情况下,这里使用模板一可以求出左边界,使用模板二可以求出右边界,为什么是大多数情况呢?因为有的题目很特殊需要特别注意判断条件

vector<int> searchRange(vector<int>& nums, int target) {
    if(nums.empty()) return {-1,-1};
    int l = 0, r = nums.size() - 1; // 二分范围
    while (l < r)			        // 查找元素的开始位置
    {
        int mid = (l + r ) / 2;
        if(nums[mid] >= target) r = mid;
        else l = mid + 1;
    }
    if (nums[r] != target) return {-1,-1};  // 查找失败
    int L = r;
    
    l = 0, r = nums.size() - 1;     // 二分范围
    while (l < r)                   // 查找元素的结束位置
    {
        int mid = (l + r + 1)/2;
        if(nums[mid] <= target ) l = mid;
        else r = mid - 1;
    }
    return {L,r};
}

3. 直接分析法

这里需要忘记前面提到的左闭右闭和左闭右开区间,根据题意分析 right 的初始值,然后分析下一轮的搜索区间是左侧还是右侧

这里根据经验,往往设置的是 while(left < right),因为只有这样退出循环时才有 left >= right,往往会有 left == right成立, 为了不越界就使用 right 作为目标位置

常见问题

  1. 二分查找不一定要求数组有序

力扣上如「山脉数组」「选择有序数组」等可以根据 nums[mid] 的值推测两侧元素的性质,进而缩小搜索区间,这类数组都是接近有序的数组

还有如「寻找重复数」不在输入数组上做二分,而是在输入数组的最小值 min 和最大值 max 组成的连续整数数组上查找一个数,也就是搜索区间是 [min...max]

  1. 二分查找取 mid 时何时+1

首先何时+1何时不+1是为了避免死循坏,对于有偶数个元素的区间来说,使用 mid = (left+right)/2 只能取到左边的中位数,如果想要取到右边的中位数就必须+1

再来看为什么有时需要取到右边的首位数,考虑只有两个元素的区间,利用 mid 将区间分为 [left, mid-1][mid, right]时,如果不+1则无法缩减区间,进而进入死循环

总结

  • 写成 while(left < right),退出循环的时候有 left == right 成立,好处是:不用判断应该返回 left 还是 right;

  • 区间 [left..right] 划分只有以下两种情况:

    • 分成 [left..mid][mid + 1..right],分别对应 right = midleft = mid + 1
    • 分成 [left..mid - 1][mid..right],分别对应 right = mid - 1left = mid,这种情况下。需要使用 int mid = (left + right + 1) / 2,否则会出现死循环,这一点不用记,出现死循环的时候,把 left 和 right 的值打印出来看一下就很清楚了;
  • 退出循环 left == right,如果可以确定区间 [left...right] 一定有解,直接返回 left 就可以,否则还需要对 left 这个位置单独做一次判断;

  • 始终保持不变的是:在区间 [left...right] 里查找目标元素。

4. 常见题型

4.1 二分求下标

在给定数组中查找符合条件的元素下标

题目说明题解
704.二分查找简单的二分即可,常规思路
35.搜索插入位置找到插入位置的索引,可以是len,因此可以设置right=len
300.最长上分子序列这题DP思路比较简单,二分查找比较难
34.排序数组查找第一个和最后一个位置可以总结为两个模板:查找左边界查找右边界
611.有效三角形的个数二分有两种思路:左边界和右边界,双指针:固定最长边逆序解1 解2
659.找到K个最接近的元素二分找到左边界,双指针
436.寻找右区间使用哈希表记录第一个元素位置之后在[:][0]二分查找[:][1]
1237.找出给定方程的正整数解二分利用一个变量递增,双指针利用两个变量都递增
4.寻找两个正序数组的中位数直接归并比较简单,通过二分找到__分割线__比较难

「选择数组」和「山脉数组」:局部单调性

题目说明题解
33.搜索旋转排序数组直接在循环里面定位比较直接,在外面定位思考很细节
81.搜索旋转排序树组II在 33 基础上通过++left去重,或者直接 while去重
153.旋转排列数组最小值通过比较 mid 和 right 判断最小值在左还是右
154.旋转排序数组最小值II在 153 基础上通过 -- right 去重
852.山脉数组的峰顶索引找到最小满足 nums[i] > nums[i+1] 的下标
1095.山脉数组找目标值三个二分:找到峰顶下标,升序找target,降序找target
4.2 二分找答案

在给定的序列中找一个满足条件的答案,通过二分查找逐渐缩小范围,最后逼近到一个数

题目说明题解
69.x的平方根使用除法可以避免溢出,另有一个 牛顿迭代法
287.寻找重复数二分思路有点绕,循环链表思路比较直接
374.猜数字大小比较简单的二分,注意一下题意就好
275.H指数 II注意向左找还是向右找的条件
1283.使结果不超过阈值的最小除数注意不整除才+1
1292. 元素和小于等于阈值的正方形的最大边长二维前缀和,遍历二分
  • 其实按照 liweiwei 的总结还有第三个题型,这里就不继续刷了,先消化消化

5. 总结

其实二分的题目刷多了就有经验了,最主要的分析出二分判断的条件,根据题意判断出是在左区间还是右区间查找元素。另外对于二分意图不明显的题目需要尝试分析出题目的隐藏题意

6. 参考

  1. 大部分参考自:写对二分查找不是套模板并往里面填空,需要仔细分析题意
  2. 另模板1参考自:代码随想录
  3. 另模板2参考自:图解二分
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值