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


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

    为什么需要复杂度分析?

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

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

    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(n2) O(n^2)
    这个在之后会再介绍,这里只是提一下

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

    我们可以假设第一行代码消耗的时间为x,那么第二行代码消耗的时间可以表示为k*x(其中k大于0,k为常数),第三行的代码同样可以表示为z*x(z>0,k为常数),第四行的同样表示为v*x(v>0,k为常数),那么总消耗时间可以表示为
    x+nkx+nzx+vx=x(1+nk+nz+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(n2) 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+2n+n2 1+2n+n^2
    根据我们之前说的,忽略其低阶项及常数项,可记为:
    O(n2) 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. logN logN

    2. log2n log^2n

    3. Nlogn Nlogn

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

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

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

    我们要记住一个公式:

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

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

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

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

    1. 长度,O(n)

    2. 对数,O(logn)

    3. 对数平方数
      log2n log^2n

    4. 线性:O(n)

    5. nlog(n) nlog(n)

    6. 二次

    7. 三次

    8. 指数

    结尾:

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

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

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

  • 相关阅读:
    超过经理收入的员工
    搜索插入位置
    整数反转
    俩数之和
    tar 命令参数解释
    【记录】Transaction rolled back because it has been marked as rollback-only
    【转载】BIO、NIO、AIO
    【转载】JDK自带的log工具
    【转载】java8中的Calendar日期对象(LocalDateTime)
    【对象属性复制】BeanUtils.copyProperties(obj1, obj2);
  • 原文地址:https://www.cnblogs.com/daimzh/p/12854416.html
Copyright © 2020-2023  润新知