首发于算法专题
探秘时间复杂度计算

探秘时间复杂度计算

在了解时间复杂度计算之前,要先明白计算原理,时间复杂度的计算原理就是函数渐近上界理论。

函数渐近上界理论

设算法计算操作数量为T(n),其是一个关于输入数据大小n的函数。假设T(n)是个一次函数,说明时间增长趋势是线性的,因此易得时间复杂度是线性阶。将线性阶的时间复杂度记为 O(n),代表函数T(n)的渐近上界 「asymptotic upper bound」。要推算时间复杂度,本质上是在计算操作数量函数 T(n)的渐近上界。下面我们先来看看函数渐近上界的数学定义。

本质上看,计算渐近上界就是在找一个函数f(n),使得在n趋向于无穷大时,T(n)和f(n)处于相同的增长级别(仅相差一个常数项c的倍数)


推算方法

由于上述 c⋅f(n)中的常数项c可以取任意大小,因此操作数量T(n)中的各种系数、常数项都可以被忽略。根据此原则,可以总结出以下计数偷懒技巧:

  1. 跳过数量与 n 无关的操作。因为他们都是 T(n)中的常数项,对时间复杂度不产生影响。
  2. 省略所有系数。例如,循环 2n次、5n+1次、……,都可以化简记为 n 次,因为 n 前面的系数对时间复杂度也不产生影响。
  3. 循环嵌套时使用乘法。总操作数量等于外层循环和内层循环操作数量之积,每一层循环依然可以分别套用上述 1.2. 技巧。

最后参考如下表格:


案例汇总

常数阶 O(1)

常数阶的操作数量与输入数据大小 n无关,即不随着n的变化而变化。

/* 常数阶 */
func constant(n int) int {
    count := 0
    size := 100000
    for i := 0; i < size; i++ {
        count++
    }
    return count
}

总结:无论操作数量size有多大,与数据大小n无关。


线性阶 O(n)

线性阶的操作数量相对输入数据大小成线性级别增长。线性阶常出现于单层循环。

/* 线性阶 */
func linear(n int) int {
    count := 0
    for i := 0; i < n; i++ {
        count++
    }
    return count
}

总结:遍历操作均为线性阶。


平方阶 O(n2)

平方阶的操作数量相对输入数据大小成平方级别增长。平方阶常出现于嵌套循环,外层循环和内层循环都为 O(n),总体为O(n2)。

/* 平方阶 */
func quadratic(n int) int {
    count := 0
    // 循环次数与数组长度成平方关系
    for i := 0; i < n; i++ {
        for j := 0; j < n; j++ {
            count++
        }
    }
    return count
}



指数阶 O( 2^{n} )

比如细胞分裂就是指数阶。

/* 指数阶(循环实现)*/
func exponential(n int) int {
    count, base := 0, 1
    // cell 每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)
    for i := 0; i < n; i++ {
        for j := 0; j < base; j++ {
            count++
        }
        base *= 2
    }
    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
    return count
}

使用递归进行运算。

/* 指数阶(递归实现)*/
func expRecur(n int) int {
    if n == 1 {
        return 1
    }
    return expRecur(n-1) + expRecur(n-1) + 1
}


对数阶 O(logn)

对数阶与指数阶正好相反,后者反映“每轮增加到两倍的情况”,而前者反映“每轮缩减到一半的情况”。对数阶仅次于常数阶,时间增长得很慢,是理想的时间复杂度。对数阶常出现于二分查找和分治算法中,体现“一分为多”、“化繁为简”的算法思想。

/* 对数阶(循环实现)*/
func logarithmic(n float64) int {
    count := 0
    for n > 1 {
        n = n / 2
        count++
    }
    return count
}

也同样可以使用递归。


线性对数阶 O(nlog⁡n)

线性对数阶常出现于嵌套循环中,两层循环的时间复杂度分别为 O(log⁡n)和O(n)。主流排序算法的时间复杂度都是 O(nlog⁡n),例如快速排序、归并排序、堆排序等。

/* 线性对数阶 */
func linearLogRecur(n float64) int {
    if n <= 1 {
        return 1
    }
    count := linearLogRecur(n/2) +
        linearLogRecur(n/2)
    for i := 0.0; i < n; i++ {
        count++
    }
    return count
}

编辑于 2023-02-15 16:06・IP 属地上海