帕先生之手撕二分查找

79 阅读1分钟

hello, 大家好, 我是帕鲁, 今天带大家彻底领悟二分查找

Just practice No Skill

1. 何为二分查找

二分查找, 又为折半查找, 通过比较中间值, 使每一次比较都使搜索范围缩小一半.

时间复杂度为 O(Logn)

最坏情况下时间复杂度为 O(Logn)

最好情况下时间复杂度为 O(1) 直接就找到了

在给定的一串有序(默认升序)数组 nums 中, 给定一个 target , 如果值存在, 则返回下标值, 如果不存在, 返回 -1

步骤:

  1. 在有序数组中值 midmid 查找, 如果找到, 则直接返回
  2. target>midtarget > mid , 则扔掉 leftleft, 反之扔掉 rightright 部分
  3. 在剩下的数组范围 执行 步骤1

如果上述循环穿透了整个数组范围, 仍没有返回, 说明没找到, return -1

2. 举例说明

[1,3,4,5,6,7,8,10,13,14][1, 3, 4, 5, 6, 7, 8, 10, 13, 14] 中找到 target=4target = 4 的位置

image.png

步骤:

已知: target=4,mid=7target = 4, mid = 7

  1. 找到numsnums的中值 midmid , 判断 4==74 == 7 , 不等
  2. 4 比 7小, 所以要在7的左半边查找
  3. 新的数组为 [1,3,4,6,7][1, 3, 4, 6, 7] , 同时更新 mid = 4, 返回步骤1
  4. 发现找到了, returnreturn indexindex == 22

因为整个数组是有序的, 目标值比 midmid 小, 那么我们只要在 midmid 左半边内找即可, 这就是为什么又被称为折半查找, 每次在一半内找, 然后舍弃另一半

3. 代码模版

left: 左边界 right: 右边界 此处和下文均代表index

    def search(self, nums: List[int], target: int) -> int:
        left = 0
        right = len(nums) - 1
        while left <= right:
            mid = left + (right - left) // 2
            if nums[mid] > target:
                right = mid - 1   //  更新右边界
            elif nums[mid] < target:
                left = mid + 1  // 更新左边界
            else:
                return mid
        return -1

4. 彻底领悟 ( 重点!)

这里有3个关键: 悟了之后闭着眼也能写出来

  1. rightrightlen(nums) len(nums) 还是 len(nums)1len(nums) - 1 ?
  2. whilewhile 条件是 << 还是 <=<= 呢 ?
  3. 更新右边界的时候, 到底是 right=mid1right = mid -1 还是 right=midright = mid

首先思考一个问题: while 是什么意思

有效范围内, 就应该一直循环整个数组, 且不遗漏任何元素 , 换句话说, 条件一旦不满足, 就要跳出循环

情况一: 左闭右闭

假设 right=len(nums)1 right = len(nums) - 1, 那么 while left ? right 这里到底是 << 还是 <=<=

解析:

[left,right][ left , right ]

image.png

这里 rightright 很明显指向数组最后一个元素

那么假设 left<rightleft < right 的情况

最后一个元素会被遍历到吗, 很明显, 不会, 因为都等不到 =right = right的时候, 循环就退出了

rightright 该不该被遍历到, 应该 , nums[right]nums[right] 合法, 所以应该

在满足 whilewhile 的情况下, 存在元素没有被遍历到, 那么最后结果肯定是不对的的.

如果是 whilewhile leftleft << rightright 的情况, 元素应该被遍历到 但实际上没有被遍历到, < 错误

再次强调 因为 nums[right]nums[right] 合法 且范围有效, 那么 如果写成 leftleft << rightright, 那么就会遗漏 nums[right] 的值

所以: whilewhile leftleft <=<= rightright

每次更新 rightright 边界的时候, 如果是 写成 right=midright = mid, 那么下次 whilewhile 循环 midmid 就会被计算进去, 但实际上 midmid 在上一次循环就已经被计算过了 (rightright 是闭区间), 不应该被重复计算, 所以应该是 midmid 前面一位 mid1mid -1

首先要保证数组每个元素都被遍历到

其次 right=len(nums)1right = len(nums) - 1 代表 rightright 指向数组最后一位

那么自然数组最后一个元素是需要被访问到的

所以是 whilewhile leftleft <=<= rightright

如果仅仅是 << , 那么 rightright 所指向的元素永远都不会被访问到

可能有点绕, 但希望读者能细细品味

如果稍微对上面说的有点理解, 那么我们来看还有一种情况:

情况二: 左闭右开

假设 right=len(nums) right = len(nums), 那么 while left ? right 这里到底是 << 还是 <=<=

注意: 和情况一不一样的是, 这里的 right 并不是指向数组最后一位元素

解析:

[left,right)[left , right)

image.png

那么再来假设 left<rightleft < right 的情况

最后一个元素会被遍历到吗, 很明显, , 因为 right1 right - 1 才指向数组最后一个元素, 而 right1 right - 1whilewhile 的有效范围内

rightright 该不该被遍历到, 不应该 , nums[right]nums[right] 不合法, 所以不应该

nums=[10,20,30],right=len(nums),nums[3]nums = [10, 20, 30] , right = len(nums), nums[3] 显然不合法

在满足 whilewhile 的情况下, 元素都被遍历到了, pass

如果是 whilewhile leftleft <=<= rightright 的情况, nums[right]nums[right] 不合法, <= 错误

所以: whilewhile leftleft << rightright

每次更新 rightright 边界的时候, 注意这里 right 是开区间, 那么 right=midright = mid 就显得非常合理, midmid 在上一次 whilewhile 就已经计算过, 此次循环不会再次计算, 而是计算 mid1 mid - 1

思考: 如果这里写成 right=mid1right = mid -1 , 会是什么情况?

如果写成 right=mid1right = mid -1 , 由于 rightright 是开区间, 所以 下标 mid1mid -1 所在的值永远不会被访问到, 存在遗漏

5. 一些细节

  1. 在计算中位数 midmid 的时候, 可能会存在整数溢出, 故采用

mid = left + (right - left) // 2

  1. 更新左边界: 把 leftleft 指向的位置从 mid+1 mid +1 开始, 自然就扔掉了左边一半原数组
  2. 更新右边界: 把 rightright 指向的位置从 mid1 mid -1 或者( midmid )开始, 自然就扔掉了右边一半原数组

6. 总结和回顾

  1. 如何 定义 rightright 的指向位置
  2. whilewhile 的有效范围内, rightright 下标是否有效, 是否会被计算
  3. 更新 rightright 边界时, 由 right 定义 决定 right=mid right = mid (开区间) 还是 right=mid1 right = mid - 1 (闭区间)

7. 帕鲁讲完了, 你领悟了吗