算法| 再也不怕被问时间复杂度了 (上)

如果说算法是程序员的根本,那么时间复杂度就是算法的根本,所以在学习算法前,我们一定要先学会如何分析一个算法的时间复杂度

为什么需要复杂度分析?

可能对于第一次接触这个概念的同学来说,第一个问题就是,我为什么要需要这个东西呢?我把代码跑一次,统计一下消耗时间不就完了吗?这样不是更准确吗?如果你现在也是这么想的,那么请耐心的往下看

首先来说,直接统计代码的运行时间绝对是没问题的,相应的空间消耗程度也可以通过监控内存来获取。这种统计方法甚至还有一个专门的名字叫"事后统计法"。但是这种方法存在两个很大的弊端

  1. 受环境影响严重,如机器配置

一个相同的算法,在不同配置的机器上运行时间肯定是不同的。这一点不用多说

  1. 受数据量影响严重

举个极端的例子如下:

public void demo01(List<Integer> list) {
    for (Integer integer : list) {
        System.out.println(integer);
    }
}


public void demo02(List<Integer> list) {
    int count = 0;
    for (Integer integer : list) {
        count++;
        System.out.println(integer);
        if(count>100000){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

我们可以看到demo01,demo02两个算法(姑且叫它们算法,只是为了说明这种情况,实际情况下没人会这么写)

这两个算法的目的都是为了遍历集合中的所有元素,当元素的数量不高于100000时,这两个算法运行的效率肯定

是差不多的,但是一旦数据规模超出100000时,这两个算法的效率就没有可比性了

综上所述,为了屏蔽外在环境的影响,只是单纯的评判算法的优劣,我们的时间空间复杂度应运而生

如何对一个算法进行复杂度分析?

在知道了为什么会有复杂度的出现后,我们就需要学习下怎么对一个算法进行复杂度的分析。

先来看一个简单的例子:

public int sum(int n) {
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += n;
    }
    return sum;
}

如果对时间复杂度有所了解的话,一眼就能看出结果为O(n)。如果你对复杂度不了解的话,不用急,跟着我的思路来~

我们可以假设每行代码所有消耗的时间都为一个时间单元—u,那么对于这个程序其复杂度不难得出为:(1+n+n+1)*u=(2n+2)*u,第一行程序运行了1次,第二行跟第三行都运行了n次,return语句执行一次,所以结果为(2n+2)*u=n*(2u)+2u(其中u为常数),那么其时间复杂度就为O(n)

可能有的同学会有疑问:

  1. 为什么你可以假设每行代码的运行时间都一样呢?很明显for (int i = 0; i < n; i++)int sum = 0消耗的时间长呀!这样的假设也能成立吗?
  2. 为什么计算结果为n*(2u)+2u(其中u为常数),其时间复杂度就为O(n)呢?
在这里我们先解释第二个问题,什么是大O?

粗略的讲,大O代表的算法的复杂度变化的趋势,在上面的例子中,我们计算出结果为n*(2u)+2u(其中u为常数),这是一个一次函数,我们可以称其变化是线性的,当n趋向无穷大时,我们可以忽略常数项,记为O(n)。同样,当我们计算出结果为一个二次函数时,这个时间我们可以忽略其低阶项跟常数项,记为:
O ( n 2 ) O(n^2)
这个在之后会再介绍,这里只是提一下

当我们了解到了 什么是大O后,我相信第一个问题也很好解释了

我们可以假设第一行代码消耗的时间为x,那么第二行代码消耗的时间可以表示为k*x(其中k大于0,k为常数),第三行的代码同样可以表示为z*x(z>0,k为常数),第四行的同样表示为v*x(v>0,k为常数),那么总消耗时间可以表示为
x + n k x + n z x + v x = x ( 1 + n k + n z + v ) x+n*k*x+n*z*x+v*x=x(1+nk+nz+v)
很明显,这也是一个关于n的1次函数,所以我们也可以记其时间复杂度为O(n)

通过上面的例子,我们已经对时间复杂度及其计算有了一个粗略的了解,下面我们继续通过几个例子来深入了解时间复杂度,并学习几种常见的时间复杂度

几种简单的时间复杂度:
  1. 常数阶:
    O ( 1 ) O(1)

首先,我们要明确一点,O(1)并不代表代码只执行一行,它的意思是说,该算法的执行时间跟外界因素(数据大小,调用次数)无关,举例如下:

 public int sum(int n) {
        int sum = 0;
        for (int i = 0; i < 3; i++) {
            sum += n;
        }
        return sum;
    }

在上面的例子中,不如n多大,该方法被调用多少次,每次调用的复杂度永远是1+2*3+1=8,是一个常数,这种情况下,我们记其时间复杂度为O(1)

  1. 线性:
    O ( n ) O(n)

之前的例子就已经分析过了,这里不再赘述

  1. 二次:
    O ( n 2 ) O(n^2)

我们看一个例子:

public int sum(int n) {
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += n;
        for (int j = 0; j < n; j++) {
            sum += j;
        }
    }
    return sum;
}

我们按照我们的方法计算其复杂度:

第一行代码执行一次:1

第二行代码执行n次:n

第三行代码执行n次:n

第四行代码执行:n乘n次(外圈每执行一次,内圈执行n次,所以为n乘n次)

第五行代码也是:n乘n次

return语句执行1次:1

所以时间复杂度为:
1 + 2 n + n 2 1+2n+n^2
根据我们之前说的,忽略其低阶项及常数项,可记为:
O ( n 2 ) O(n^2)
到这里我们可以发现,因为我们往往要忽略低阶项跟常数项,只保留最高阶,相当于我们只需统计执行次数最多的代码,在上面的例子中我们很容易发现,执行最多次数的代码就是内嵌的循环,为二次函数,所以很快得出结论,时间复杂度为O(n^2)

  1. 三次:

在学习了线性及二次时间复杂度了之后,三次也很容易理解了,最简单的例子就是循环嵌套了三层,这里不再赘述

  1. 指数:(增长速度非常恐怖),这里给出一个简单的例子,读者自行思考即可,不恰当的递归也会导致指数级时间复杂度
    public int sum(int n) {
        int sum = 0;
        int count = 2;
        for (int i = 0; i <n ; i++) {
            count = count*2;
        }

        for (int i = 0; i < count; i++) {
            sum += n;
            for (int j = 0; j < n; j++) {
                sum += j;
            }
        }
        return sum;
    }
稍微复杂点的时间复杂度:
  1. l o g N logN

  2. l o g 2 n log^2n

  3. N l o g n Nlogn

上面的时间复杂度都涉及到了对数,所以为了了解上面几种时间复杂度,我们首先要知道什么情况下会出现对数的时间复杂度,示例如下:

public void test(int n){
    int i = 1;
    while (i<n){
        i = i*2;
    }
}

根据我们之前的分析,我们主要关系i=i*2这行代码的运行次数,假设其运行次数为x,则有如下公式:
2 x &lt; = n 2^x&lt;=n
则:
x &lt; = l o g 2 n x&lt;=log_2n
所以此示例中时间复杂度可记为:
O ( l o g 2 n ) O(log_2n)
我们知道,任意对数的底是可以替换的,如:
l o g 2 n = l o g 2 3 l o g 3 n log_2n = log_23*log_3n
所以,我们统一将这种时间复杂度记为:
l o g n logn
到现在,我们已经知道了对数级的时间复杂度的由来,那么形如:
n l o g n nlogn
这种形式的时间复杂度又是怎么来的呢?

我们要记住一个公式:

嵌套代码的复杂度等于嵌套内外代码复杂度的乘积,什么意思呢?我们举例说明:

public void test(int n){
    for (int i = 0; i < n; i++) {
        int x = 1;
        while (x<n){
            x= x*2;
        }
    }

}

可以看到,我们将一个时间复杂度为logn的算法循环执行了n次,那么这个算法的复杂度就为:
n l o g n nlogn
同样,我们也能知道:
l o g 2 n log^2n
的由来了,我也不再赘述了

在文章的最后,我想应该将常用的算法复杂度按增长速度从底到高整理了一下,让大家对这些复杂度能有个评判标准,由底到高分别为:

  1. 长度,O(n)

  2. 对数,O(logn)

  3. 对数平方数
    l o g 2 n log^2n

  4. 线性:O(n)

  5. n l o g ( n ) nlog(n)

  6. 二次

  7. 三次

  8. 指数

结尾:

在这篇文章中,主要跟大家一起学习了一些基础的时间复杂度的分析,但是知道这些是远远不够的,复杂度的分析是我们学习算法的基石,预计还会有一篇文章,主要对最好,最坏,平均,均摊时间复杂度进行分析,另外会跟大家一起分析一个算法复杂度的题目,希望大家能跟我一起学到东西!

码字不易,喜欢的同学,加个关注,点个赞吧,多多留言交流哦,万分感谢!

下一篇:再也不怕被问时间复杂度了 (下)

发布了135 篇原创文章 · 获赞 84 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_41907991/article/details/97443681