算法一词,其实最早来源于数学领域。在数学领域中,我们最熟知的算法莫过于“高斯算法”了,即等差数列求和公式。那么在计算机领域中,也有众多的算法,排序、查找等都算。
算法其实是有好坏之分的。衡量算法的好坏有很多的指标,其中最重要的就是算法的时间复杂度和空间复杂度 。这一节主要说算法的时间复杂度。
时间复杂度可直观理解为代码执行时间的长短。实际上,由于受运行环境和输入规模的影响,代码的绝对执行时间是无法预估的,因此我们往往通过计算代码的基本操作执行次数来预估代码的执行时间。
以下面几种场景来说明时间复杂度的计算方法:
场景一:有一个长度为 10cm 的面包,程序员A每 3min 吃掉 1cm,那么吃掉整个面包就需要要花费 3 * 10 = 30min,如果面包的长度为 n cm 的话,那么吃掉整个面包就需要 3 * n = 3n min。如果用一个函数来表达吃掉整个面包所需要的时间,可以记作 ,n 为面包的长度。
场景二:有一个长度为 16cm 的面包,程序员A每 5min 吃掉剩下的面包的一半,即第 5min 吃掉 8cm,第 10min 吃掉剩下的 8cm 的一半 4cm......那么把面包吃到只剩下 1cm 的话需要几分钟?这个场景中涉及到的数学表达式就是 16 不断的去除以 2,除几次之后的结果为 1?这就涉及到数学中的对数的概念了,即以 2 为底 16 的对数()。因此把面包吃得只剩 1cm,需要 20min 时间。如果面包的长度是 n cm,那么共需要时间 , 表示以 2 为底 n 的对数。如果用函数表示的话记作。
场景三:给程序员A一个长度为 10cm 的面包和一瓶饮料,要求其 2min 喝完饮料,那么喝完饮料的用时为 2min。如果面包的长度为 n cm 呢?用时也是 2min。无论面包多长,程序员喝完饮料的用时都是 2min,与面包的长度无关。用函数表示的话记为。
场景四:有一个长度为 10cm 的面包,程序员A吃掉第一个 1cm 需要 1min,吃掉第二个 1cm 需要 2min......每吃掉 1cm 所花的时间就比上一个 1cm 所花的时间多 1min,那么吃掉整个面包所花的时间根据高斯算法可得 55min,用函数可表示为。
分析吃东西所花费的时间这一思想同样适用于对程序基本操作执行次数的统计。设 T(n) 为程序基本操作执行次数的函数(也可以认为是程序的相对执行时间函数),n 为输入规模,上述的 4 个场景分别对应了程序中最常见的 4 种执行方式,即线性、对数、常量、多项式。
有了基本操纵执行次数的函数 T(n) ,仍然还是无法比较不同的算法的执行时间。比如算法A的执行次数是 ,算法B的执行次数是 ,这俩算法到底哪个运行时间更长呢,这就取决于 n 的大小了。因此,为了解决时间分析的难题,便引进了渐近时间复杂度。其定义如下:若存在函数 f(n),使得当 n 趋近于无穷大时,T(n)/f(n) 的极限值为不等于零的常数,则称 f(n) 是 T(n) 的同量级函数。记作 T(n) = O(f(n)),O 为算法的渐近时间复杂度,简称为时间复杂度。因为渐近时间复杂度用大写 O 来表示,所以也被称为大O表示法。直观点来讲就是时间复杂度就是把程序的相对执行时间函数 T(n) 简化为一个数量级,这个数量级可以是 n、n^2、n^3 等。
推导时间复杂度时有几个规则:
- 如果运行时间是常量级,则用常数 1 表示
- 只保留时间函数中的最高阶项
- 如果最高阶项存在,则省去最高阶项前面的系数
那么上面所说的这四个场景分别对应的时间复杂度函数为:
场景一:,最高阶项为 3n,省去系数 3,则转化的时间复杂度为:
场景二:,最高阶项为 ,省去系数 5,则转化的时间复杂度为:。
场景三:,只有常数量级,则转化的时间复杂度为:。
场景四:,最高阶项为 0.5n^2,省去系数 0.5,则转化的时间复杂度为:
。
以上这 4 种时间复杂度在不知道 n 的大小的情况下,我们是无法直观的去判断哪种算法的执行用时更长,但是当 n 的取值足够大时,我们就会发现:
< < <
除了这几个最基本的时间复杂度外,还有其他一些不太常用的算法的时间复杂度,比如:
、、、、
虽然现在计算机硬件的性能越来越强了,但是我们仍然需要重视时间复杂度。因为高效的算法和低效的算法实际上有很大的差距,比如:算法 A 的执行次数是,时间复杂度是,算法 B 的执行次数是,时间复杂度是。假如算法 A 是运行在某台老旧的计算机上的,算法 B 是运行在某台超级计算机上,超级计算机的运行速度是老旧计算机的 100 倍。那么,随着输入规模 n 的增长,两种算法的运行速度对比如下:
n = 1 | 10000 | 5 |
---|---|---|
n = 5 | 50000 | 125 |
n = 10 | 100000 | 500 |
n = 100 | 10000000 | 50000 |
n = 1000 | 10000000 | 5000000 |
n = 10000 | 100000000 | 500000000 |
n = 100000 | 10000000000 | 50000000000 |
n = 1000000 | 10000000000 | 5000000000000 |
可以看到,当 n 的值很小时,算法 A 的执行用时要远大于算法 B 的;当 n 的值在 1000 左右时,算法 A 和算法 B 的执行用时已经比较接近;随着 n 的值越来越大,甚至达到十万、百万级时,算法 A 的优势便开始显现出来了,而算法 B 的执行速度却越来越慢,差距也越来越明显。这就是不同时间复杂度带来的差距。