二分算法(一) -- 新手可看,图解二分入门

文章详细介绍了如何使用二分查找算法解决两个问题:在递增序列中找到第一个大于等于目标值的下标,以及在排序数组中查找目标元素的第一个和最后一个位置。通过两种不同的区间划分方式,展示了二分查找的过程,并提供了相应的C++代码实现。
摘要由CSDN通过智能技术生成

系列:
二分算法(一) 新手可看,图解二分入门
二分算法(二) 二分答案入门

核心在于划分区间并确定返回值,然后再确定 L 和 R 是什么,最后维护状态设置变化规则。将在下面两个简单的练习中给出解释。

练习一

方式1

问题1:递增序列 v 中找第一个大于等于x的下标。

既然有单调性,就可以使用二分的思想,以找到第一个大于等于 target 的下标为例。我们可以从结果出发,想象最后这个序列是分成两半的,问题就变成如何去划分,结果又怎么从这个划分好的数组中得出。

  1. 因为要找第一个大于等于 target 的下标,我们将其划分为两半,一边是小于 target 的元素,另一边是大于等于 target 的元素,那么答案 ans 就是红色第一个元素了。
    在这里插入图片描述
  2. 确定了答案位置之后,就可以确定 l 和 r 了,如果令最后 l == r(半开半闭),那么 l 和 r 就指向同一个地方,假设都指向 ans,那么 l 的左侧元素都 < target,r 自身和右侧元素都 >= target,我们在后续的变化中需要维护这个规律/状态。
  3. 最后一步根据上述假设写出变化:
    • 当 v[mid] < target,令 l = mid + 1;l 左侧仍然是 < target 的
    • 当 v[mid] >= target,令 r = mid;r 和 r 右侧仍然是 >= target 的
    • 以上两步均维护了状态不变,这种是半开半闭,虽然我觉得这个名称不重要甚至会误导。

关键在于假设 L 和 R 的 状态( L 的左侧所有元素的值均小于 x,R 自身和右侧的元素均大于等于 x),并在变化中 维护状态 不变。

代码实现
// 找到第一个大于等于 x 的下标
int lower_bound(vector<int> v, int x) {
    int n = v.size();
    
    int l = 0, r = n, mid;
    while(l < r) {
        mid = l + (r - l) / 2;
        
        if(v[mid] < x) l = mid + 1; // 变化后l左侧仍然都是小于x的
        else r = mid; // 变化后r和r右侧都仍然是大于x的
    }
    
    return l == n ? -1 : l;
}
方式2

划分方式不止一种,也可以如此划分:
方式2
这种划分的答案就是 r 。

  • l 及其左侧元素都 < target,r 及其右侧元素都 >= target。
  • 当 l 和 r 相邻的时候结束,即 while(l + 1 < r)。初始值设置为 l = -1,r = n(l 不能取0,因为还没判断v[0] 是否满足 < target,同理,r 不能取 n - 1)。
  • 当 v[mid] < target,令 l = mid
  • 当 v[mid] >= target,令 r = mid
  • 这样维护了状态不变,一般也叫开区间写法。
// 找到第一个大于等于 x 的下标
int lower_bound(vector<int> v, int x) {
    int n = v.size();
    
    int l = -1, r = n, mid;
    while(l + 1 < r) {
        mid = l + (r - l) / 2;
        
        if(v[mid] < x) l = mid; 
        else r = mid; 
    }
    
    return r == n ? -1 : r;
}

练习二

34 在排序数组中查找元素的第一个和最后一个位置

判断第一个位置:
  • 划分数组,一边是 < target,另一边是 >= target,那么红色部分的第一个元素就是答案ans,使用半开半闭写法,最后 l == r。
  • 假设 l 和 r 都指向 ans
    • 那么 l 左侧的元素都 < target
    • 那么 r 及其右侧元素都 >= target
  • 根据上述假设写出变化:
    • 当 v[mid] < target,令 l = mid + 1;
    • 当 v[mid] >= target,令 r = mid;
    • 以上两步均维护了状态不变
      划分
判断最后一个位置:

target的位置,可以调用上面的方法,查找第一个 target + 1,然后把答案-1返回,或者再写一个也可以(很容易错):

  • 划分区间,ans 就是 L - 1 或者 R - 1,
    划分
  • 确定 l 和 r 的含义
    • l 左侧所有元素都 <= target
    • r 及其右侧元素都 > target
  • 变化
    • 当 v[mid] <= target,l = mid + 1
    • 当 v[mid] > target,r = mid
    • 以上两步均维护了状态不变

这里补充一个细节:
求最后一个位置有两种可能的写法:

  • 方法1:就是上面的图,划分为 l 左侧元素都 <= target,r 及右侧元素都 >= target。变化就是 l = mid + 1;r = mid; 返回 l - 1
  • 方法2: 划分为 l 及左侧元素都 <= target,r 右侧元素都 > target。l = mid;r = mid - 1;返回 l
  • 这种情况下方法1是对的,2 是错的,因为mid = (l + r) / 2,是向下取整的。有可能出现 l 和 r 相邻,然后mid就等于l,那么如果 v[mid] <= target,那么 l = mid 将没有发生变化,出现 死循环 !!!,如下所示,永远无法跳出循环 while(l < r)。
    错误写法导致死循环
Tip

因为 mid 向下取整的特性,所以应该使用 l 左侧的元素满足xx,r 及右侧元素满足xx的写法。

代码实现
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int n = nums.size(), l ,r, mid;
        if(nums.size() == 0) return {-1, -1};

        // l 左侧都 < target,r 及右侧 >= target 
        l = 0, r = n;
        while(l < r) {
            mid = l + (r - l) / 2;
            if(nums[mid] < target) l = mid + 1;
            else r = mid;
        }
		// 判断是否存在
        if(r == n || nums[r] != target) return {-1, -1};
        int first = r;
        
        // l 左侧 <= target , r 及其右侧 > target
        l = 0, r = n;
        while(l < r) {
            mid = l + (r - l) / 2;
            if(nums[mid] <= target) l = mid + 1;
            else r = mid;
        }

		// 因为上面判断过了第一个target存在,所以最后一个target一定存在
        return {first, l - 1};
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值