LeetCode - 二分查找Binary Search

94 阅读3分钟

二分查找是一种基于分治策略的高效搜索算法。它利用数据的有序性每轮减少一半搜索范围,直至找到目标元素或搜索区间为空为止。该方法时间效率高,且无需额外空间。前提是数组必须是排好序的,同时又不会经常变动,另外,输入并不一定是数组,也有可能是给定一个区间的起始和终止的位置。

图片.png 关键点在于对边界的处理:一定要确保 “循环不变量” 原则,也即在while循环搜索中的区间是左闭右闭or左闭右开 → 对应的right的初始化要注意是否需要长度-1?因为nums.length作为索引是越界的;下一次循环应该如何切断左右两个区间?例如[left, right) 左闭右开,所以当 nums[mid] 被检测之后,下一步的搜索区间应该去掉 mid 分割成两个区间,即 [left, mid)[mid + 1, right)

总结:
初始化 right = nums.length - 1 决定了「搜索区间」是 [left, right],所以决定了 while (left <= right),同时也决定了 left = mid+1right = mid-1

图片.png

图片.png

注意:

  1. 计算 mid 时需要防止溢出,代码中 left + (right - left) / 2(left + right) / 2 的结果相同,但是有效防止了 leftright 太大直接相加导致溢出。此外,JS中一般写为left + (right - left) >> 1
  2. 返回值可以根据while终止循环的条件来看,如while (left <= right)的终止条件是left = right + 1,故返回的leftBorder是right(因为寻找到边界不包括target)

扩展:【寻找左边界】在nums[middle] == target的时候更新right,找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。同理【寻找右边界】在nums[middle] == target的时候更新left,找到target时不要立即返回,而是扩大「搜索区间」的下界left,在区间[mid + 1, right)中继续搜索,使得区间不断右缩,达到锁定右侧边界的目的【cloud.tencent.com/developer/a… 】 类似的例题:mp.weixin.qq.com/s/SfYuFpJpA…

 if (nums[middle] >= target) { // 寻找左边界,在nums[middle] == target的时候更新right
    right = middle - 1;
    leftBorder = right;
 } 

【循环不变量】该想法同样也运用在LeetCode59.螺旋矩阵Ⅱ 每画一条边都要坚持一致的左闭右开,或者左开右闭的原则,这样这一圈才能按照统一的规则画下来;此外,对于正方形的二维数组只需要考虑转多少圈以及最后中间是否剩一个值,如果剩一个值则需要再转完圈后再单独加中间的值;对于矩形二维数组变成一维数组的情况,需要在转完圈后单独考虑最内圈剩的一行或者一列。

【例题 33. 搜索旋转排序数组
尽管有一次旋转,但由于原本是有序的,故还是可以用二分查找,重点在于观察left和mid的大小关系,找到有序的一小半后再来判断target在哪一半中,不在这一半就在另一半

图片.png
【例题 287. 寻找重复数
抽屉原理:如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有  n + 1 个元素放到 n 个集合中去,其中必定有一个集合里至少有两个元素。
通过观察left到mid区间中的苹果个数,看是否对应抽屉个数,如果对应则看另一边,若不对应则继续二分,直到只剩下一个苹果数量不对应的抽屉。二分查找解决的代码时间复杂度是O(nlogn)
优化的方向是使用快慢指针。对于每个 index ,可以将其所对应的数字作为它下一个指向的对象,将这些对象串联为链表。若链表中出现了一个环,说明环的入口即是答案。此时,题目变为142. 环形链表 II,寻找环的入口。