算法:02 二分法模板及 Leetcode 二分法全解

Leetcode 二分法全解



前言

 本篇博客旨在归纳总结二分法问题,从 Leetcode 上的题目来看使用二分法时的注意事项


1、何时应该想到二分法

  • 二分法的题目,在题干处都有十分明显的暗示:
    • 限定了要搜索的数组是 有序的、部分有序的、非完全无序的
    • 限定了算法复杂度是 O(log n) 级别的
  • 在题目类型上,大致有如下几个类型:
    • 在区间内查找 符合条件的元素索引
    • 题干经过变形后回到 ”在区间内查找符合条件的元素索引“ 问题
    • 其他衍生类型
  • 下面,以 Leetcode 中不同类型的题目来看二分法的使用及注意事项


2、Leetcode 二分法全解

2.1 二分法模板

 这是 最基本 的一类题型,我们将从这个题型开始,总结二分法模板,并拓展开来。

  • 最基础题:Leetcode 第704题:二分查找.
    • 题干:

    • 分析:
      • 1). 它符合 ”在区间内查找符合条件的元素索引“ 这一类型
      • 2). 数组完全有序
      • 3). 可以使用二分法,因为数组是升序的、且所有元素不重复:
        • 查找某一元素 target 时,将数组 nums 一分为二记为 nums1、nums2,则 target 要么在 nums1 中、要么在 nums2 中
        • 那么 target 到底在哪里呢?只需要判断 target 与 nums1 中最后一个元素的大小 或者 target 与 nums2 中第一个元素的大小 即可
        • 如果发现 target 归属于 nums1,就再进一步将 nums1 一分为二同理计算;至于 nums2,丢弃即可,这很像分治,只不过是减治, 这样,每次我们都丢掉了数组的一半,使算法复杂度达到了 O(log n) 级别
    • 代码解构:
      • 1). 首先需要记录我们二分查找的区间边界值:
        int left = 0;  // 数组第一个元素的索引
        int mid;  // mid 可以是子数组 nums1 的最后一个元素的索引,也可以是子数组 nums2 的第一个元素的索引,具体判别请往下看
        int right = nums.size() - 1;  // 数组最后一个元素的索引
      
      • 2). 计算 mid 值,这是二分法的关键
        • 正常来说,我们的取整方式都是 下取整, 当数组长度为偶数时,例如为6:
       if(nums.size() % 2 == 0)   // 6 % 2 = 0
       	mid = (left + right) / 2;   // mid = (0 + 5) / 2 = 2
      
      • 可以看到,mid 的值被 下取整 了,因为参与计算的数字都是 int 型,而 5 / 2 的结果 2.5 是 浮点型(float or double), 但 mid 被初始化成了 int 型,根据类型转换,小数点后的数字都被舍弃了,故 2.5 变成了 2,被 下取整 了,这是语言本身的问题,那么对我们做二分法时有什么影响呢,请往下看
      • 当然,数组长度为奇数时就没有这个烦恼,因为此时 mid 是实打实的中间索引
      • 3). 确定了 mid 的值后,就可以比较 target 和 nums[mid] 的大小了,在前面我们也说了,mid 可以表示 nums1 的最后元素、也可表示 nums2 的第一个元素,为了简化思想,我们敲定 mid 表示为 nums1 的最后元素
      • 4). 当 nums[mid] < target 时,target 一定在 nums2 中,此时放缩左边界 left,使 left = mid + 1,则 left 就变成了 nums2 的第一个元素的索引;我们下一轮的搜索就会在 [mid + 1, right]、也就是 nums2 中搜索
      • 5). 当 nums[mid] >= target 时,此时分两种情况 大于 和 等于,但是为了简化思想,我们直接用 “else” 来归总这两种情况;因为,target 不在 [mid + 1, right]中,就一定在它的反面 [0, mid] 中,此时,放缩右边界 right,使 right = mid;我们不管 target 等不等于 nums[mid] 这一个特定值,它都一定在区间 [0, mid] 内、也就是子数组 nums1 中,这样,就简化了 和中值相等情况下讨论 [0, mid - 1] 区间的情形:
       if(nums[mid] < target) 
       	left = mid + 1;
       else
       	right = mid;
      
      • 6). 当你的第一个判断条件是 大于 时,也同样根据上面的思想做出相应的边界收缩行为:
       if(nums[mid] > target) 
       	right = mid - 1;
       else
       	left = mid;
      
      • 7). 关键来了,两种不同的边界收缩行为会造成边界值死循环问题, 举个例子:
        • 对于数组 nums = [1, 2, 3, 4, 5, 6],我们要查询 target = 1 的索引、也就是索引 0
        • 当 mid 采用下取整时,第一轮查询,mid = (0 + 5) / 2 = 2,如果我们使用上面 6) 中的判定条件和边界收缩行为、nums[mid] > target、right = mid - 1 = 1, 那么下一轮查询区间为 [0, 1]
        • 第二轮查询,mid = (0 + 1) / 2 = 0,此时进入 else、left = mid = 0, 问题来了,left 本来就是 0,经过这轮查询、还是 0,再进入下一轮查询还是这样,发生了死循环
        • 这就是由于 mid 下取整造成的,如果 mid 采取上取整,就不会有这种问题
        • 关于这类死循环问题,请读者自己比划几次,就能知晓大概了,这里就不用大篇幅介绍了,下面给出在模板中我们应该怎么写、就能避免死循环问题
      • 8). mid 值对应的边界收缩行为
       mid = (left + right) / 2;   // mid 下取整时,用下面的边界收缩行为
       if(nums[mid] < target) 
       	left = mid + 1;
       else
       	right = mid;
      
       mid = (left + right + 1) 
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值