❤️数据结构入门❤️初章 - 算法时间复杂度 (建议收藏)

Posted 英雄哪里出来

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了❤️数据结构入门❤️初章 - 算法时间复杂度 (建议收藏)相关的知识,希望对你有一定的参考价值。

🙉饭不食,水不饮,题必须刷🙉

C语言免费动漫教程,和我一起打卡!
🌞《光天化日学C语言》🌞

LeetCode 太难?先看简单题!
🧡《C语言入门100例》🧡

数据结构难?不存在的!
🌳《数据结构入门》🌳

LeetCode 太简单?算法学起来!
🌌《夜深人静写算法》🌌

究极算法奥义!深度学习!
🟣《深度学习100例》🟣

一、前言

  「数据结构」「算法」 是密不可分的,两者往往是相辅相成的,所以,在学习 「数据结构」 的过程中,不免会遇到各种「算法」
  零基础学算法的最好方法,莫过于刷题了。当然,刷题是不够的,刷的过程中也要多多总结,多多思考,养成 「经常思考」 的习惯,这就是所谓的 「 流水不腐,户枢不蠹 」,任何事情都是需要坚持的,刷题也一样,没有刷够足够的题,就很难做出系统性的总结。所以上大学的时候,我花了三年的时间来刷题, 工作以后还是会抽点时间出来刷题。
  千万不要用工作忙来找借口,时间挤一挤总是有的
  很多人觉得算法难,是因为被困在了时间和空间这两个维度上。如果不考虑时间和空间的因素,其实我们可以把所有问题都通过「穷举法」 来解决,也就是你告诉计算机你要做什么,然后通过它强大的算力帮你计算。
  那么,说到了时间,今天我就和大家来聊一下 「 算法时间复杂度 」

二、穷举法

1、单层循环

  • 所谓穷举法,就是我们通常所说的枚举,就是把所有情况都遍历了(跑到)的意思。举个最简单的例子:

【例题1】给定 n ( n ≤ 1000 ) n(n \\le 1000) n(n1000) 个元素 a i a_i ai,求其中 奇数 有多少个。

  • 判断一个数是偶数还是奇数,只需要求它除上 2 的余数是 0 还是 1,那么我们把所有数都判断一遍,并且对符合条件的情况进行计数,最后返回这个计数器就是答案,这里需要遍历所有的数,这就是穷举。如图二-1-1所示:
    图二-1-1
  • c/c++ 代码实现如下:
int countOdd(int n, int a[]) {
    int cnt = 0;
    for(int i = 0; i < n; ++i) {
        if(a[i] & 1)
            ++cnt;
    }
    return cnt;
}

2、双层循环

  • 经过上面的例子,相信你对穷举法已经有一定的理解,那么我们来看看稍微复杂一点的情况。

【例题2】给定 n ( n ≤ 1000 ) n(n \\le 1000) n(n1000) 个元素 a i a_i ai,求有多少个二元组 ( i , j ) (i,j) (i,j),满足 a i + a j a_i + a_j ai+aj 是奇数 ( i < j ) (i \\lt j) (i<j)

  • 我们还是秉承穷举法的思想,这里需要两个变量 i i i j j j,所以可以枚举 a i a_i ai a j a_j aj,再对 a i + a j a_i + a_j ai+aj 进行奇偶性判断,所以很快设计出一个利用穷举的算法。如图二-2-1所示:
    图二-2-1
  • c/c++ 代码实现如下:
int countOddPair(int n, int a[]) {
    int cnt = 0;
    for(i = 0; i < n; ++i) {
        for(j = i+1; j < n; ++j) {
            if( (a[i] + a[j]) & 1)
                ++cnt;
        }
    }
    return cnt;
}

3、三层循环

  • 经过这两个例子,是不是对穷举已经有点感觉了?那么,我们继续来看下一个例子。

【例题3】给定 n ( n ≤ 1000 ) n(n \\le 1000) n(n1000) 个元素 a i a_i ai,求有多少个三元组 ( i , j , k ) (i,j,k) (i,j,k),满足 a i + a j + a k a_i + a_j + a_k ai+aj+ak 是奇数 ( i < j < k ) (i \\lt j \\lt k) (i<j<k)

  • 相信聪明的你也已经猜到了,直接给出代码:
int countOddTriple(int n, int a[]) {
    int cnt = 0;
    for(i = 0; i < n; ++i) {
        for(j = i+1; j < n; ++j) {
            for(int k = j+1; k < n; ++k) {
                if( (a[i] + a[j] + a[k]) & 1 )
                    ++cnt;
            }
        }
    }
    return cnt;
}
  • 这时候,相信聪明的你,已经意识到一个问题;
  • 它就是:
  • 是的,随着循环嵌套的增多,时间消耗会越来越多,并且是三个循环是乘法的关系,也就是遍历次数随着 n n n 的增加,呈立方式的增长。

4、递归枚举

  【例题4】给定 n ( n ≤ 1000 ) n(n \\le 1000) n(n1000) 个元素 a i a_i ai 和一个整数 k ( k ≤ n ) k (k \\le n) k(kn),求有多少个有序 k k k 元组,满足 它们的和 是偶数。

  • 一层循环,两层循环,三层循环, k k k 层循环?
  • 我们需要根据 k k k 的不同,决定写几层循环, k k k 的最大值为 1000,也就意味着我们要写 1000 的 if else 语句。
  • 显然,这样是无法接受,比较暴力的做法是采用到递归;
  • c/c++ 代码实现如下:
int dfs(int n, int a[], int start, int k, int sum) {
    if(k == 0)
        return (sum & 1) ? 0 : 1;             // (1)
    int s = 0; 
    for(int i = start; i < n; ++i)
        s += dfs(n, a, i+1, k-1, sum + a[i]); // (2)
    return s;
}
  • 这是一个经典的深度优先遍历的过程,对于初学者来说可能比较难理解,这个过程比较复杂,我来简单解释一下。

  • ( 1 ) (1) (1) dfs(int n, int a[], int start, int k, int sum)这个函数的含义是:给定 n n n 元素的数组 a [ ] a[] a[],从下标 s t a r t start start 开始,选择 k k k 个元素,得到的和为 s u m sum sum 的情况下的方案数,当 k = 0 k=0 k=0 时代表的是递归的出口;

  • ( 2 ) (2) (2) 当前第 i i i 元素选择以后,剩下就是从 i + 1 i+1 i+1 个元素开始选择 k − 1 k-1 k1 个的情况,递归求解。

  • 我们简单分析一下, n n n 个元素选择 k k k 个,根据排列组合,方案数为: C n k C_n^k Cnk,当 n = 1000 , k = 500 n=1000,k=500 n=1000k=500 时已经是天文数字,这段代码是完全出不了解的。

  • 当然,对于初学者来说,这段代码如果不理解,问题也不大,只是为了说明穷举这个思想。

三、时间复杂度

1、时间复杂度的表示法

  在进行算法分析时,语句总的执行次数 T ( n ) T(n) T(n) 是关于问题规模 n n n 的函数,进而分析 T ( n ) T(n) T(n) 随着 n n n 的变化情况而确定 T ( n ) T(n) T(n) 的数量级。
  算法的时间复杂度,就是算法的时间度量,记作: T ( n ) = O ( f ( n ) ) T(n) = O(f(n)) T(n)=O(f(n)) 用大写的 O 来体现算法时间复杂度的记法,我们称之为 大 O 记法。

1、时间函数

时间复杂度往往会联系到一个函数,自变量 表示规模,应变量 表示执行时间。

  • 这里所说的执行时间,是指广义的时间,也就是单位并不是 “秒”、“毫秒” 这些时间单位,它代表的是一个 “执行次数” 的概念。我们用 f ( n ) f(n) f(n) 来表示这个时间函数。

2、经典函数举例