数据结构与算法-复杂度分析(上)

以前学数据结构的时候,学了一学期(当然,期间也会偶尔逃逃课、玩玩手机~),糊涂了一学期,主要是感觉数据结构太枯燥了,还有就是,当时觉得学了没什么用。不过,就在前几天,在公号上看见 stromzhangFenng 都推荐极客时间的一个专栏 – 《数据结构与算法之美》,并且说数据结构多重要,这个专栏多好。当然,看完推荐后,还是心动了,但是 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复杂度表示法,通过几段代码对几个代表性的复杂度量级进行了说明,这次先到这。

《数据结构与算法-复杂度分析(上)》 专栏.jpg

    原文作者:这里有颗小螺帽
    原文地址: https://www.jianshu.com/p/e5e23d4bd494
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞