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)
- 题干: