美文网首页iOS面试算法数据结构
(数据结构)十分钟搞定时间复杂度(算法的时间复杂度)

(数据结构)十分钟搞定时间复杂度(算法的时间复杂度)

作者: raymondCaptain | 来源:发表于2017-11-02 10:17 被阅读15432次

我们假设计算机运行一行基础代码需要执行一次运算。

int aFunc(void) {
    printf("Hello, World!\n");      //  需要执行 1 次
    return 0;       // 需要执行 1 次
}

那么上面这个方法需要执行 2 次运算

int aFunc(int n) {
    for(int i = 0; i<n; i++) {         // 需要执行 (n + 1) 次
        printf("Hello, World!\n");      // 需要执行 n 次
    }
    return 0;       // 需要执行 1 次
}

这个方法需要 (n + 1 + n + 1) = 2n + 2 次运算。

我们把 算法需要执行的运算次数 用 输入大小n 的函数 表示,即 T(n) 。
此时为了 估算算法需要的运行时间 和 简化算法分析,我们引入时间复杂度的概念。

定义: 存在常数 c,使得当 N >= c 时 T(N) <= f(N),表示为 T(n) = O(f(n)) 。
如图:


当 N >= 2 的时候,f(n) = n^2 总是大于 T(n) = n + 2 的,于是我们说 f(n) 的增长速度是大于或者等于 T(n) 的,也说 f(n) 是 T(n) 的上界,可以表示为 T(n) = O(f(n))。

因为f(n) 的增长速度是大于或者等于 T(n) 的,即T(n) = O(f(n)),所以我们可以用 f(n) 的增长速度来度量 T(n) 的增长速度,所以我们说这个算法的时间复杂度是 O(f(n))。

算法的时间复杂度,用来度量算法的运行时间,记作: T(n) = O(f(n))。它表示随着 输入大小n 的增大,算法执行需要的时间的增长速度可以用 f(n) 来描述。

显然如果 T(n) = n^2,那么 T(n) = O(n^2),T(n) = O(n^3),T(n) = O(n^4) 都是成立的,但是因为第一个 f(n) 的增长速度与 T(n) 是最接近的,所以第一个是最好的选择,所以我们说这个算法的复杂度是 O(n^2) 。

那么当我们拿到算法的执行次数函数 T(n) 之后怎么得到算法的时间复杂度呢?

  1. 我们知道常数项并不影响函数的增长速度,所以当 T(n) = c,c 为一个常数的时候,我们说这个算法的时间复杂度为 O(1);如果 T(n) 不等于一个常数项时,直接将常数项省略。
比如
第一个 Hello, World 的例子中 T(n) = 2,所以我们说那个函数(算法)的时间复杂度为 O(1)。
T(n) = n + 29,此时时间复杂度为 O(n)。
  1. 我们知道高次项对于函数的增长速度的影响是最大的。n^3 的增长速度是远超 n^2 的,同时 n^2 的增长速度是远超 n 的。 同时因为要求的精度不高,所以我们直接忽略低此项。
比如
T(n) = n^3 + n^2 + 29,此时时间复杂度为 O(n^3)。
  1. 因为函数的阶数对函数的增长速度的影响是最显著的,所以我们忽略与最高阶相乘的常数。
比如
T(n) = 3n^3,此时时间复杂度为 O(n^3)。

综合起来:如果一个算法的执行次数是 T(n),那么只保留最高次项,同时忽略最高项的系数后得到函数 f(n),此时算法的时间复杂度就是 O(f(n))。为了方便描述,下文称此为 大O推导法。

由此可见,由执行次数 T(n) 得到时间复杂度并不困难,很多时候困难的是从算法通过分析和数学运算得到 T(n)。对此,提供下列四个便利的法则,这些法则都是可以简单推导出来的,总结出来以便提高效率。

  1. 对于一个循环,假设循环体的时间复杂度为 O(n),循环次数为 m,则这个
    循环的时间复杂度为 O(n×m)。
void aFunc(int n) {
    for(int i = 0; i < n; i++) {         // 循环次数为 n
        printf("Hello, World!\n");      // 循环体时间复杂度为 O(1)
    }
}

此时时间复杂度为 O(n × 1),即 O(n)。

  1. 对于多个循环,假设循环体的时间复杂度为 O(n),各个循环的循环次数分别是a, b, c...,则这个循环的时间复杂度为 O(n×a×b×c...)。分析的时候应该由里向外分析这些循环。
void aFunc(int n) {
    for(int i = 0; i < n; i++) {         // 循环次数为 n
        for(int j = 0; j < n; j++) {       // 循环次数为 n
            printf("Hello, World!\n");      // 循环体时间复杂度为 O(1)
        }
    }
}

此时时间复杂度为 O(n × n × 1),即 O(n^2)。

  1. 对于顺序执行的语句或者算法,总的时间复杂度等于其中最大的时间复杂度。
void aFunc(int n) {
    // 第一部分时间复杂度为 O(n^2)
    for(int i = 0; i < n; i++) {
        for(int j = 0; j < n; j++) {
            printf("Hello, World!\n");
        }
    }
    // 第二部分时间复杂度为 O(n)
    for(int j = 0; j < n; j++) {
        printf("Hello, World!\n");
    }
}

此时时间复杂度为 max(O(n^2), O(n)),即 O(n^2)。

  1. 对于条件判断语句,总的时间复杂度等于其中 时间复杂度最大的路径 的时间复杂度。
void aFunc(int n) {
    if (n >= 0) {
        // 第一条路径时间复杂度为 O(n^2)
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < n; j++) {
                printf("输入数据大于等于零\n");
            }
        }
    } else {
        // 第二条路径时间复杂度为 O(n)
        for(int j = 0; j < n; j++) {
            printf("输入数据小于零\n");
        }
    }
}

此时时间复杂度为 max(O(n^2), O(n)),即 O(n^2)。

时间复杂度分析的基本策略是:从内向外分析,从最深层开始分析。如果遇到函数调用,要深入函数进行分析。

最后,我们来练习一下

一. 基础题
求该方法的时间复杂度

void aFunc(int n) {
    for (int i = 0; i < n; i++) {
        for (int j = i; j < n; j++) {
            printf("Hello World\n");
        }
    }
}

参考答案:
当 i = 0 时,内循环执行 n 次运算,当 i = 1 时,内循环执行 n - 1 次运算……当 i = n - 1 时,内循环执行 1 次运算。
所以,执行次数 T(n) = n + (n - 1) + (n - 2)……+ 1 = n(n + 1) / 2 = n^2 / 2 + n / 2。
根据上文说的 大O推导法 可以知道,此时时间复杂度为 O(n^2)。

二. 进阶题
求该方法的时间复杂度

void aFunc(int n) {
    for (int i = 2; i < n; i++) {
        i *= 2;
        printf("%i\n", i);
    }
}

参考答案:
假设循环次数为 t,则循环条件满足 2^t < n。
可以得出,执行次数t = log(2)(n),即 T(n) = log(2)(n),可见时间复杂度为 O(log(2)(n)),即 O(log n)。

三. 再次进阶
求该方法的时间复杂度

long aFunc(int n) {
    if (n <= 1) {
        return 1;
    } else {
        return aFunc(n - 1) + aFunc(n - 2);
    }
}

参考答案:
显然运行次数,T(0) = T(1) = 1,同时 T(n) = T(n - 1) + T(n - 2) + 1,这里的 1 是其中的加法算一次执行。
显然 T(n) = T(n - 1) + T(n - 2) 是一个斐波那契数列,通过归纳证明法可以证明,当 n >= 1 时 T(n) < (5/3)^n,同时当 n > 4 时 T(n) >= (3/2)^n。
所以该方法的时间复杂度可以表示为 O((5/3)^n),简化后为 O(2^n)。
可见这个方法所需的运行时间是以指数的速度增长的。如果大家感兴趣,可以试下分别用 1,10,100 的输入大小来测试下算法的运行时间,相信大家会感受到时间复杂度的无穷魅力。

相关文章

  • 数据结构&算法小谈

    一、数据结构&算法 二、数据结构名词 三、时间复杂度术语: 时间复杂度:算法执行所需要的多少时间,使用O(......

  • 常见数据结构及排序算法时间空间复杂度

    时间复杂度趋势变化 常见数据结构复杂度 常见排序算法复杂度

  • 数据结构与算法-复杂度分析

    时间、空间复杂度:衡量算法执行小路的指标,数据结构与算法离不开时间、空间复杂度分析,复杂度分析是算法的精髓。 为什...

  • 一位算法工程师的自我修养

    数据结构与算法 基本算法思想动态规划贪心算法回溯算法分治算法枚举算法 算法基础 时间复杂度 空间复杂度 最大复杂度...

  • 快速排序

    分类:排序算法 数据结构:不定 最坏时间复杂度:O(n^2) 最优时间复杂度:O(n log n) 平均时间复杂度...

  • Python-100天(二)-Python语言进阶

    数据结构和算法 算法:解决问题的方法和步骤 评价算法的好坏:渐近时间复杂度和渐近空间复杂度。 渐近时间复杂度的大O...

  • 数据结构与算法-C语言4-算法时间和空间复杂度

    数据结构与算法-目录 1.时间复杂度的定义 算法时间复杂度,也就是算法的时间量度。记作:T(n)=O(f(n))。...

  • 数据结构学习大纲

    第一章 绪论 数据结构基本概念数据结构基本概念算法的基本概念算法的时间复杂度与空间复杂度分析基础时间复杂度分析空间...

  • 插入排序

    分类:排序算法 数据结构:数组 最坏时间复杂度:O(n^2) 最优时间复杂度:O(n) 平均时间复杂度:O(n^2...

  • 冒泡排序

    分类:排序算法 数据结构:数组 最坏时间复杂度:O(n^2) 最优时间复杂度:O(n^2) 平均时间复杂度:O(n...

网友评论

  • aa8c211803e7:注册账号就是为了给你评论一下
  • aa8c211803e7:决定赠予博主称号"启明星辰"
  • aca32d4342ea:进阶题的“假设循环次数为 t,则循环条件满足 2^t < n。
    可以得出,执行次数t = log(2)(n),即 T(n) = log(2)(n),可见时间复杂度为 O(log(2)(n)),即 O(log n)。”看不懂。
    新人求教 谢谢
    raymondCaptain:评论里有更加详尽的解析,你可以看看
  • GD围城:“显然 T(n) = T(n - 1) + T(n - 2) 是一个斐波那契数列,通过归纳证明法可以证明” 这个归纳证明法又是个啥子
    raymondCaptain:数学归纳法(Mathematical Induction, MI)是一种数学证明方法,通常被用于证明某个给定命题在整个(或者局部)自然数范围内成立。
    你可以百度《数学归纳法》。
  • GD围城:“因为f(n) 的增长速度是大于或者等于 T(n) 的,即T(n) = O(f(n))” 这推导表示看不懂
    raymondCaptain:@GD围城 T(n) = O(f(n)) 表示 存在常数 c,使得当 N >= c 时 T(N) <= f(N)。
    怎么理解比较简单,文中有写,你可以参考下。
    GD围城:@raymondCaptain 这个T(n) = O(f(n)) 当中 O的作用是不是仅仅是个符号而已没有不会进行运算吧?
    raymondCaptain:当 N >= 2 的时候,f(n) = n^2 总是大于 T(n) = n + 2 的,于是我们说 f(n) 的增长速度是大于或者等于 T(n) 的,也说 f(n) 是 T(n) 的上界,可以表示为 T(n) = O(f(n))。

    所以 f(n) 的增长速度是大于或者等于 T(n) ,可以表示为 T(n) = O(f(n))。
  • GD围城:“当 N >= 2 的时候,f(n) = n^2 总是大于 T(n) = n + 2 的,于是我们说 f(n) 的增长速度是大于或者等于 T(n) 的,也说 f(n) 是 T(n) 的上界,可以表示为 T(n) = O(f(n))。 ” 这个f(n) = n^2 是怎么来的? T(n)=n+2 不是 T(n)=2n+2么 上面那个函数》
    raymondCaptain:谢谢阅读。
    两个函数都跟上面的内容都没关系,都是凭空举例,来说明 T(n) = O(f(n)) 的含义。
    f(n) = n^2,T(n)=n+2 都是凭空捏来,你可以代替为合适的其他函数,只要符合 T(n) = O(f(n)) 的意思。
  • 9531990ac0c4:理解不来,我是新人勿怪
    raymondCaptain:再次进阶的理解不必强求
    需要的更多是数学能力
  • 9531990ac0c4:在电脑上看到觉得不错,特意又在手机APP上看,前面都理解,就最后的再次进阶,无论是否满足if,都直接返回值了,相当于就执行了一次啊
    raymondCaptain:时间复杂度分析的基本策略是:从内向外分析,从最深层开始分析。如果遇到函数调用,要深入函数进行分析。

    再次进阶里的return,是进行了两次函数调用之后才返回的。
    时间复杂度的计算是不能忽略函数调用的。
  • 4809bbb41d23:请问为什么不考虑常数因子c呀
    raymondCaptain:因为 常数因子c 对函数的增长速度的影响并没有 函数的阶次 显著。

    时间复杂度只是一种描述,并不需要非常严谨,所以忽略这个影响比较小的因素。
  • hygge_254d:三. 再次进阶:
    答案:
    T(1) = 1;
    T(n) = T(n-1) + T(n-2)
    = T(n-2) + T(n-3) + T(n-3) + T(n-4)
    = T(n-3) + T(n-4) + T(n-4) + T(n-5) + T(n-4) + T(n-5) + T(n-5) + T(n-6)
    ....
    = T(1) + T(1) + ... + T(1)
    共需要n次推导,第一次推导后有2项,第二次推导后有4项,第3次推导后有8项,因此经过n次推导后共有2^n项T(1), 由T(1) = 1知,T(n) = 2^n,即时间复杂度等于O(2^n).
    raymondCaptain:我感觉思路非常好,但是有误。
    n次推导后并非有2^n项T(1)。
    比如T(n-1) + T(n-2),T(n-2)的拆分次数比T(n-1)少一次,所以也比T(n-1)少拆分一个T(1)。
  • 淡出了少年:不错不错,看前面的时候还有点懵,看完后面再回看前面,感觉理解了不少,多谢👍
  • LingHun:不错
  • 78f0812abbd0:我想问for(int i=0;i<n;i++) 不应该是被执行了n次吗?为什么是n+1次?谢谢
    78f0812abbd0:@raymondCaptain 了解,多谢多谢
    raymondCaptain:你有疑问是对的。
    这里严格来说确实不准确,
    for(int i=0;i<n;i++) 其实是
    int i=0;
    while(i<n){
    ......
    i++;
    }
    运算次数其实不少,但是时间复杂度是衡量程序运行时间随输入大小变化的量级,所以不需要太准确。

    就好像我们说这辆车的车速很快,超过120。
    于是我们用 超过120 这个量级表示 这个车的速度。
    这种时候,具体是121,122,125并不重要。

    用 A 来表示 B,从而可以忽略不重要的东西,把精力集中在更加重要的地方。
    时间复杂度就是这样,通俗地理解就是用 一个函数(随x的增长速度) 来表示一段程序的运行时间随输入大小变化的速度。
    淡出了少年:因为最后当i=n的时候还是要再判断一次的
  • 78a4b6e5750a:我专门登录进来赞你一下:smile:
  • AOS加贝:谢谢
  • 916d3b5a7922:运算次数就是执行次数 基础题中的执行次数 与 开头讲的执行次数 是不是有矛盾
    raymondCaptain:用的都是同一个概念和意思。
    矛盾在哪里?
  • Paycation:请问能否给这句话加上证明?“显然如果 T(n) = n^2,那么 T(n) = O(n^2),T(n) = O(n^3),T(n) = O(n^4) 都是成立的,但是因为第一个 f(n) 的增长速度与 T(n) 是最接近的,所以第一个是最好的选择,所以我们说这个算法的复杂度是 O(n^2) 。”
    raymondCaptain:如果 T(n) = n^2,那么 T(n) = O(n^2),T(n) = O(n^3),T(n) = O(n^4) 都是成立的。
    这是根据文中T(n) = O(f(n))的定义得来的,通俗地说,就是如果存在一个数,当x大于这个数的时候,T(x)总是小于f(x),这时候我们说T(n) = O(f(n))。你可以看上面的图示和图下面的两段话,比较容易理解。
    第一个 f(n) 的增长速度与 T(n) 是最接近的,这个求下导数就知道哪个增长速率接近了。
  • 菜鸟的觉醒:第一个的for循环时间复杂度不是O(n)吗
    raymondCaptain:哪个?
    对于一个循环,假设循环体的时间复杂度为 O(n),循环次数为 m,则这个
    循环的时间复杂度为 O(n×m)。

    如果循环n次,忽略循环体的时间复杂度,比如for (int i = 0; i < n; i++) { },那么时间复杂度就是 O(n)。
  • 4b87655edcbc:通过归纳证明法可以证明,当 n >= 1 时 T(n) < (5/3)^n,同时当 n > 4 时 T(n) >= (3/2)^n。

    请问这两步是怎么推导出来的, 能讲下过程吗 谢谢
    ec49a58f6375:f(n) = f(n-1) + f(n-2), 想成一颗二叉树, 总次数=左次数+右次数。于是问题转化为“求叶子节点个数”,即2^n。
  • f08ca1b852d7:归纳证明法是什么鬼,学校学过已经交给老师了。。。:joy:
  • werrial:想请教一个问题,比如len(list)这一类函数的时间复杂度也是O(1)么? n^n呢?
    raymondCaptain:@没有浴缸 你说得对
    75679b6dc4f0:看list怎么实现,如果有专门常量记录长度length就是O(1),如果每次都是遍历然后取最后值,O(n)
  • ac568e38d31a:T(n)与T(N) 在本文含义是一样儿么?
    raymondCaptain:你好,是不一样的。
    T(n) 是用 n 表示的函数,如 T(n) = n, T(n) = n^2, T(n) = 2n + 1。
    大写的 N 代表某个特定的值,T(N)表示 T(n) 这个函数在 n=N 这个点上面的值。
    比如 N = 2, 那么 T(N) = T(2)。
  • MarvelousAngel:简单明了,小白的福音,哈哈哈!!!
  • 9962262e4a1a:进阶题中,是不是把i++去掉比较合理呀
  • laravel:每次循环 i*=2 所以 t次循环 i = 2^t 这个值要小于n,所以近似推导得出 2^t=n,解方程得到 t=log2 n ,所以时间复杂度为O(log n),大概是这样的对吧
  • laravel:t=log2 n 是怎么推导得出来的
    aca32d4342ea:@omg帅气不 多谢大佬
    6131c3dc9e7b:因为循环体里面有一句i=i*2,改变了循环变量的值,就等于改变了循环次数。
    每一次循环变量乘以2,是不是很像一个等比数列,那么,
    执行第t次是不是就是2^t,循环终止条件是小于n,
    是不是就是2^t<n,
    转换一下就是你要的结果
  • 7709b8d8e7bb:进阶题求该方法的时间复杂度 中2^t < n怎么得来的。
    raymondCaptain:@卢卡尔的小西装 这个 t 是这个循环算法循环的次数,使用这个 t 是为了表达你所说的总的执行次数 T(n)。显然,T(n) = 循环体运行次数 * (循环体语句数 + 循环判定语句数),文章中说了,常数不影响函数的增长速度,所以这个函数的时间复杂度可以用“循环体运行次数”的函数来表达,而这个循环的运行次数满足 2^t < n。可以得出,循环的次数 t = log(2)(n)。进而得出时间复杂度为 O(log n)。
    7709b8d8e7bb:@RosexS 这个t应该是总的次数,这个t为循环判读语句和执行语句次数之和,而不只是单指循环那一层吧,如果t单指判读语句或者执行语句的次数,那还说得过去,可是楼主上面说的例子就是总数吧
    RosexS:i=i*2,当i=n的时候,就是i乘了t次2,就是说2的t次方要小于n,才能跳出循环,so
  • 23d470ca477c:写的很好!
    raymondCaptain:谢谢,这对我是很大的鼓励哈哈

本文标题:(数据结构)十分钟搞定时间复杂度(算法的时间复杂度)

本文链接:https://www.haomeiwen.com/subject/fyrepxtx.html