用「二分法」详解时间复杂度 O(logN)

1,140 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情

背景

年后,笔者恢复了刷力扣题的节奏,每周会刷 2~4 道题。在这个过程中,笔者对时间复杂度 O(logN) 的算法流程总是不大理解,于是专门去查阅了一些资料(尤其是 左程云老师 的算法课程,对笔者帮助很大),沉淀了知识,最终对 O(logN) 的计算有了深入的认识,现分享出来,希望对掘友们有所帮助🍺

为了解释清楚 O(logN) 时间复杂度的计算过程,笔者以 二分法 为例展开说明。

二分法

二分法是一个经典的算法,考虑这么一个查找问题

给你一个升序的数组 nums,里面都是数字,再给你一个数字 target,请在 nums 里检查 target 是否存在,如果存在则返回 target 的下标,如果不存在则返回 -1。

暴力解决

这道题如果暴力解决的话,就是将 nums 从左到右遍历一遍,每走一步就比较当前数字target 是否相等,如果相等就返回 true,如果不相等就继续走(重复比较策略)..。如果走到末尾还没有相等的情况出现,说明 target 不在 nums 里,返回 false

假设数据规模是 N

对于这样的算法流程,考虑最坏情况,target 是数组的最后一个元素,那么就需要遍历 N 个元素才有结果,时间复杂度是 O(N)

「有序」的利用

仔细研究此查找问题会注意到,升序这个条件在暴力解法里没有用到,这个条件是很关键的,有了它才能执行今天的主角 ---- 二分法

二分法解决此查找问题的流程是这样的:

  1. 准备两个指针 L,R,分别指向数组开头末尾

  2. 计算 L,R 的中间坐标 M,规则是向下取整

  3. 比较 nums[M]target, 有 3 种情况:

    • nums[M] === target,返回 M 即可。
    • nums[M] < target,由于数组升序M 前面的元素只会更比target小,所以直接让 L 前进到 M+1 位置。
    • nums[M] > target,由于数组升序M 后面的元素只会更比target大,所以直接让 R 后退到 M-1 位置。
  4. 重复 步骤2,3,直到 返回M 或者 L和R错过去(L>R)

计算复杂度

以数组长度 16 为例,执行前文所述二分查找的流程,假设最坏情况:

  砍半次数  剩余待查找数据
     0       16            砍一半,再对比 nums[M] 和 target, 此过程时间复杂度 O(1)
     1        8            砍一半,再对比, O(1)
     2        4            砍一半,再对比, O(1)
     3        2            砍一半,再对比, O(1)
     4        1            对比, O(1)

每次都是对半砍,总共砍了 4 次,每次砍都只是来一次 O(1) 的对比,所以这个流程的时间复杂度取决于砍半的次数

计算砍半的次数也就是从 16 开始,要砍多少次才能到达1

我们可以反过来想:从 1 开始,要乘以多少个2,才能到达 16

1     2^0   
2     2^1   
4     2^2   
8     2^3   
16    2^4  

要乘以多少个2,这个2的个数,就是右边这一列2的指数。

2的指数的数学公式是:以 2 为底,结果的对数,把结果(数据规模)16 上升到 N,公式就是:

m11.jpeg

而在算法中,以 2 为底, N 的对数简写为 logN,写成时间复杂度,那就是 O(logN)

总结

本文以二分法入手,介绍了时间复杂度 O(logN) 的计算方法,二分查找在力扣上也有详细的讨论,地址是 力扣704.二分查找,掘友们有兴趣可以去查阅更多内容。

最后,附上笔者实现的二分查找的代码,希望大家多多指教:

function search(nums, target) {
    let L = 0;
    let R = nums.length - 1;
    
    // L,R 相等时也要进来,因为 L/R 上一轮刚刚移动过,这一轮是在「还没检查的位置」。
    while (L <= R) {
        // 1. 不用 (L+R)/2, 是因为 L+R 可能会溢出。
        // 2. 用 >> 1 即能「加快速度」,又实现了「向下取整」。
        const M = L + ((R - L) >> 1);

        if (nums[M] === target) {
            return M;
        }
        else if (nums[M] < target) {
            // M 位置的数一定比 target 小了,所以 L 直接前进到 M+1
            L = M + 1;
        }
        else {
            // M 位置的数一定比 target 大了,所以 R 直接后退到 M-1
            R = M - 1;
        }
    }
    
    // 二分查找完了也没找到
    return -1;
};