时间复杂度
时间复杂度是衡量算法好坏的一个重要指标。
衡量代码好坏,包括两个非常重要的指标:
1、运行时间;
2、占用空间;
由于运行环境和输入规模的影响,代码的绝对执行时间是无法评估的,但我们却可以预估代码基本操作执行次数。
基本操作次数
场景一:一条长10寸的面包,每3天吃掉1寸,那么吃掉整个面包需要几天?
答案是3 * 10 = 30天。如果面包的长度是N寸呢?此时吃掉整个面包,需要3 * n = 3n。如果用一个函数来表达这个相对时间,可以记作T(n) = 3n。
执行次数是线性的。
场景二:一条长16寸的面包,每5天吃掉剩余面包剩余长度的一半,第一次吃掉8寸,第二次吃掉4寸,第三次吃掉2寸…….那么把面包吃得只剩1寸,需要多少天?
这个问题翻译一下,就是数字16不断地除以2,除第几次以后的结果等于1?这里涉及到数字当中对数,以2为底,16的对数,可以简写log16。因此,把面包吃得只剩1寸,需要5 * log16 = 5 * 4 = 20天。如果面包的长度是N寸呢?需要5 * logn = 5logn,记作T(n) = 5logn。
执行次数是对数的。
场景三:一条长10寸的面包和一个鸡腿,每2天吃掉一个鸡腿。那么吃掉整个鸡腿需要多少天呢?
答案自然是2天。因为吃说吃掉鸡腿,和10寸的面包没有关系。如果面包的长度是N寸呢?无论面包有多长,吃掉鸡腿的时间仍然是2天,记作T(n) = 2。
执行次数是常量的。
场景四:一条10寸的面包,吃掉第一个1寸需要1天时间,吃掉第二个1寸需要2天时间,吃掉第三个1寸需要3天时间……每多吃一寸,所花的时间也多1天,那么吃掉整个面包需要多少天呢?
答案是从1累加到10的总和,也就是55天。如果面包的长度是N呢?此时吃掉整个面包,需要1+2+3+….+n-1+n = (1+2)*n/2 = 0.5n^2 + 0.5n,记作T(n) = 0.5n^2 + 0.5n。
执行次数是一个多项式
渐进时间复杂度
有了基本操作的执行次数函数T(n),是否就可以分析和比较一段代码的运行时间了呢?还是有一定困难的。
比如算法A的相对时间是T(n) = 100n,算法B的相对时间是T(n) = 5n^2,这两个到底谁运行时间更长一些?这就要看n的取值了。
所以,这个时候有了渐进时间复杂度的概念,官方的定义如下:
若存在函数f(n),使得当n趋近于无穷大时,T(n) / f(n)的极限值为不等于0的常数,则称f(n)是T(n)的同数量级函数。
记作T(n) = O(f(n)),称O(f(n))为算法的渐进时间复杂度,简称时间复杂度。
渐进时间复杂度用大写O来表示,所以也被称为大O表示法。
如何推导出时间复杂度呢?有如下几个原则:
1、如果运行时间是常数量级,用常数1表示;
2、只保留时间函数中的最高阶项;
3、如果最高阶项存在,则省去最高阶项前面的系数;
场景一:T(n) = 3n,最高阶项为3n,省去系数3,转化的时间复杂度为:T(n) = O(n)。
场景二:T(n) = 5logn,最高阶项为5logn,省去系数5,转化为时间复杂度为:T(n) = O(logn)。
场景三:T(n) = 2,只有常数量级,转化为时间复杂度为:T(n) = O(1)。
场景四:T(n) = 0.5n^2 + 0.5n,最高阶项为0.5n^2,省去系数0.5,转化为时间复杂度为:T(n) = O(n^2)。
这四种时间复杂度究竟谁用时更长,谁更省时间呢?稍微思考一下就可以得出结论:
O(1)< O(logn)< O(n) < O(n^2)
现在计算机硬件性能越来越强,为什么还重视时间复杂度呢?
我们来举个例子:
1、算法A的相对时间规模是T(n) = 100n,时间复杂度是O(n);
2、算法B的相对时间规模是T(n) = 5n^2,时间复杂度是O(n^2);
算法A运行在家里的老旧电脑上,算法B运行在某台超级计算机上,运行速度是老旧电脑的100倍。
那么,随着输入规模n的增长,两种算法谁运行更快呢?
从表格中可以看出,当n的值很小的时候,算法A的运行用时要远大于算法B;当n的值达到1000左右,算法A和算法B的运行时间已经很接近;当n的值越来越大,达到十万、百万时,算法A的优势开始显现,算法B则越累越慢,差距越来越明显。