以前学数据结构的时候,学了一学期(当然,期间也会偶尔逃逃课、玩玩手机~),糊涂了一学期,主要是感觉数据结构太枯燥了,还有就是,当时觉得学了没什么用。不过,就在前几天,在公号上看见 stromzhang 、 Fenng 都推荐极客时间的一个专栏 - 《数据结构与算法之美》,并且说数据结构多重要,这个专栏多好。当然,看完推荐后,还是心动了,但是 68 元的专栏价格还是让我冷静了下来(谁有钱,能交个朋友吗)。后来,很多公号都在推这个专栏,我在各个公号的留言区潜伏了两天,并且去极客时间 APP 上试听了一下,「买」,对,我最终把这个专栏买了下来,算是第一次知识付费。
为什么要进行复杂度分析
学习数据结构与算法主要是为了让自己的程序跑得更快,占用的内存更小,实现两点离不开 时间复杂度 和 空间复杂度 的分析。
可能有人会问,我写了一个程序,跑起来的时候我可以计算它的运行时间,也可以监控内存的占用情况,但这些都是在程序跑起来的时候才会获取到的,并且我也不知道程序中哪段代码拖慢或加快了运行速度,也不知道哪个地方占用内存多,完全是一个黑箱操作。还有就是,程序在不同的机器上运行的速度也不一样,处理的数据量和数据组合方式不同,运行的速度也不一样。那 时间复杂度 和 空间复杂度 该怎么分析。
为了回答这个疑问,下面就具体讲解一下时间复杂度和空间复杂度的分析方法。
时间复杂度分析
先看一段简单的代码。
1 int cal(int n) {
2 int sum = 0 ;
3 int i = 1;
4 for ( ; i <= n; i++) {
5 sum += i;
6 }
7 return sum;
8 }
这是一段求从 1 到 n 的累加和的代码,下面我们分析一下这段代码的时间复杂度。假设将每一行的运行时间作为一个 单位时间(unit_time) ,所以,第 2、3 行各执行一个 unit_time ,第 4、5 行执行 n 个 unit_time ,需要的总执行时间 T(n) = 2n + 2 。
再看一段代码。
1 int cal(int n) {
2 int sum =0 ;
3 int i = 1;
4 int j = 1;
5 for ( ; i <= n; i++) {
6 for ( ; j<=n; i++) {
7 sum = sum + i +j;
8 }
9 }
10 }
在这段代码中,第 2、3、4 行各执行一个 unit_time ,第 5 行执行 n 个 unit_time ,由于第 6、7 行是嵌套在第 5 行代码中,所以,第 6、7 行各执行 n² 个 unit_time ,所以,需要的总执行时间 T(n) = 2n² + n + 3 。
通过对这两段代码的分析可以知道, 代码的总执行时间 T(n) 与 每行代码的执行时间成正比 ,这样,我们可以总结成一个公式 T(n) = O(f(n)) ,这就是 大O时间复杂度表示法。大O时间复杂度只是表示 代码执行时间随数据规模的增长的变化趋势,所以也叫 渐进时间复杂度 (asymptotic time complexity), 简称 时间复杂度。
那公式中的 f(n) 该怎么求呢 ? 在第一段代码中, T(n) = 2n + 2 ,对应的 f(n) = n ,也就是取 T(n) 中的高阶项,并且忽略掉高阶项前的系数 , 用大O时间复杂度法表示为 T(n) = O(n) 。同理,第二段代码的时间复杂度表示为 T(n) = O(n²) 。
当然,时间复杂度不只是这两种形式,下面再看一段代码。
1 i = 1;
2 while (i <= n) {
3 i = 2 * i;
4 }
这段代码第一行执行一个 unit_time ,现在着重看一下第三行, 每运行一次第三行的代码, i 都会乘以 2,所以结果是 2 的 k 次方的形式 。那么运行到什么时候结束呢,看 while 里面的约束条件,即当 2 的 k 次方大于 n 时,运行结束,所以很容易就知道了 k = log2(n) ( log 以 2 为底 n 的对数),用大O表示法表示为 T(n) = O(log n) 。
还有一种情况,即代码中有两个未赋值的变量 m 和 n, 该怎样求时间复杂度,下面看一段代码。
1 int cal(int n, int m) {
2 int sum = 0 ;
3 int i = 1;
4 int j = 1;
5 for ( ; i <= n; i++) {
6 sum += i;
7 }
8 for ( ; j <= m; j++) {
9 sum += j;
10 }
11 }
这里出现了两个未赋值的变量 m 和 n, 阶数都为一阶,时间复杂度该怎么表示呢 ? 由于是同阶的,且都不是常量,所以时间复杂度表示为 T(n) = O(f(m) + f(n)) 。
如果代码中只有赋值的变量,时间复杂度该怎么求 ,再看一段代码。
1 int i = 6;
2 int j = 8;
3 sum = i + j
这里每一行都执行了一个 unit_time ,那样时间复杂度表示为 O(3) 吗 ?这是不对的,应该表示为 O(1),我解释一下为什么是 O(1)。只要代码的执行时间不随 n 的增大而增长,这样的代码的时间复杂度都记作 O(1)。或者说,一般情况下,只要算法中不存在循环语句、递归语句,即使成千上万行代码,其时间复杂度也是 O(1)。
这里只是选取了一些比较有代表性的复杂度量级,下面说一下比较常见的复杂度量级。
常量阶 O(1)
对数阶 O(logn)
线性阶 O(n)
线性对数阶 O(nlogn)
平方阶 O(n^2) 、立方阶 O(n^3) 、 ... k次方阶 O(n^k)
指数阶 O(2^n)
阶乘阶 O(n!)
其中,指数阶 和 阶乘阶属于非多项式量级 ,其他的属于 多项式量级 ,非多项式量级算法的执行时间会随数据规模的增长而急剧增加,因此是非常低效的算法。
空间复杂度分析
有了上面的分析之后,对于空间复杂度的理解就比较容易了。空间复杂度表示算法的存储空间与数据规模之间的增长关系,下面用一段代码说明一下。
1 void print (int n) {
2 int i = 0;
3 int [ ] a = new int [n];
4 for ( ; i < n; i++){
5 a[i] = i * i;
6 print out a[i];
7 }
8 }
代码中,第 2 行申请了一个空间存储变量 i,第 3 行,申请了一个大小为 n 的整型数组,除此之外,其他的代码没有占用额外的存储空间,类似于前面分析的时间复杂度的求法,这里的空间复杂度表示为 O(n)。
常见的空间复杂度有 O(1)、 O(n)、 O(n^2),像 O(logn)、 O(nlogn),平常基本用不到。
小结
本文主要讲解了一下时间复杂度和空间复杂度的求法,引入了大O复杂度表示法,通过几段代码对几个代表性的复杂度量级进行了说明,这次先到这。