如何分析、统计算法的执行效率和资源消耗?算法
时间、空间复杂度分析。数据结构
为何须要复杂度分析?数据结构和算法
你可能会有些疑惑,我把代码跑一遍,经过统计、监控,就能获得算法执行的时间和占用的内存大 小。为何还要作时间、空间复杂度分析呢?这种分析方法能比我实实在在跑一遍获得的数据更准 确吗? 首先,我能够确定地说,你这种评估算法执行效率的方法是正确的。不少数据结构和算法书籍还给 这种方法起了一个名字,叫过后统计法。可是,这种统计方法有很是大的局限性。函数
1.测试结果很是依赖测试环境。测试
2.测试结果受数据规模影响很大spa
3.大O复杂度表示法code
1 int cal(int n) { 2 int sum = 0; 3 int i = 1; 4 for (; i <= n; ++i) { 5 sum = sum + i; 6 } 7 return sum; 8 }
从 CPU 的角度来看,这段代码的每一行都执行着相似的操做:读数据 - 运算 - 写数据。尽管每行代码 对应的 CPU 执行的个数、执行的时间都不同,可是,咱们这里只是粗略估计,因此能够假设每 行代码执行的时间都同样,为 unit_time 。在这个假设的基础之上,这段代码的总执行时间是多少 呢? 第 2 、 3 行代码分别须要 1 个 unit_time 的执行时间,第 4 、 5 行都运行了 n 遍,因此须要 2nunit_time 的执行时间,因此这段代码总的执行时间就是 (2n+2)*unit_time 。能够看出来,全部代 码的执行时间 T(n) 与每行代码的执行次数成正比。blog
按照这个分析思路,咱们再来看这段代码。内存
1 int cal(int n) { 2 int sum = 0; 3 int i = 1; 4 int j = 1; 5 for (; i <= n; ++i) { 6 j = 1; 7 for (; j <= n; ++j) { 8 sum = sum + i * j; 9 } 10 } 11 }
咱们依旧假设每一个语句的执行时间是 unit_time 。那这段代码的总执行时间 T(n) 是多少呢? 第 2 、 3 、 4 行代码,每行都须要 1 个 unit_time 的执行时间,第 5 、 6 行代码循环执行了 n 遍,须要资源
2n * unit_time的执行时间,第7,8行执行了n^2遍,因此须要2n^2*unit_time的执行时间。因此整段代码执行时间
T(n)=(2n^2+2n+3)*unit_time.
尽管咱们不知道 unit_time 的具体值,可是经过这两段代码执行时间的推导过程,咱们能够获得一 个很是重要的规律,那就是,全部代码的执行时间 T(n) 与每行代码的执行次数 n 成正比。 咱们能够把这个规律总结成一个公式。
大O记号
其中, T(n)表示代码执行的时间; n 表示数据规模的大小; f(n) 表示每行代码执行的次数总和。由于这是一个公式,因此用 f(n) 来表示。公式中的O ,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。 因此,第一个例子中的 T(n) = O(2n+2) ,第二个例子中的 T(n) = O(2n +2n+3) 。
这就是大 O 时间复杂度表示法。
大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增加的变化趋势,因此,也叫做渐进时间复杂度( asymptotic time complexity ),简称时间复杂度。 当 n 很大时,你能够把它想象成 10000 、 100000 。而公式中的低阶、常量、系数三部分并不左右增 长趋势,因此均可以忽略。咱们只须要记录一个最大量级就能够了,若是用大 O 表示法表示刚讲的 那两段代码的时间复杂度,就能够记为: T(n) = O(n) ; T(n) = O(n ) 。
时间复杂度分析
如何分析一段代码的时间复杂度?有三个比较实用的方法。
只关注循环执行次数最多的一段代码 大 O 这种复杂度表示方法只是表示一种变化趋势。咱们一般会忽略掉公式中的常量、低阶、系数,只须要记录一个最大阶的量级就能够了。因此,咱们在分析一个算法、一段代码的时间复杂度的时候,也只关注循环执行次数最多的那一段代码就能够了。这段核心代码执行次数的 n的量级,就是整段要分析代码的时间复杂度。
那前面的第一个例子来讲,其中第 2 、 3 行代码都是常量级的执行时间,与 n 的大小无关,因此对于复杂度并无影响。循环执行次数最多的是第 4 、 5 行代码,因此这块代码要重点分析。前面咱们也讲过,这两行代码被执行了 n 次,因此总的时间复杂度就是 O(n)
加法法则:总复杂度等于量级最大的那段代码的复杂度
//前100个数相加 int cal(int n) { int sum_1 = 0; int p = 1; for (; p < 100; ++p) { sum_1 = sum_1 + p; } //前n个数 int sum_2 = 0; int q = 1; for (; q < n; ++q) { sum_2 = sum_2 + q; } // int sum_3 = 0; int i = 1; int j = 1; for (; i <= n; ++i) { j = 1; for (; j <= n; ++j) { sum_3 = sum_3 + i * j; } } return sum_1 + sum_2 + sum_3; }
这个代码分为三部分,分别是求 sum_1 、 sum_2 、 sum_3 。咱们能够分别分析每一部分的时间复杂 度,而后把它们放到一起,再取一个量级最大的做为整段代码的复杂度。
第一段的时间复杂度是多少呢?这段代码循环执行了 100 次,因此是一个常量的执行时间,跟 n 的 规模无关。 这里我要再强调一下,即使这段代码循环 10000 次、 100000 次,只要是一个已知的数,跟 n 无 关,照样也是常量级的执行时间。当 n 无限大的时候,就能够忽略。尽管对代码的执行时间会有很 大影响,可是回到时间复杂度的概念来讲,它表示的是一个算法执行效率与数据规模增加的变化趋 势,因此无论常量的执行时间多大,咱们均可以忽略掉。由于它自己对增加趋势并无影响。 那第二段代码和第三段代码的时间复杂度是多少呢?答案是 O(n) 和 O(n )。
综合三段代码的时间复杂度,咱们取最大的量级,即总的复杂度为O(n^2)
也就是说:总的时间复杂度就等于量级最大的那段代码的时间复杂度。那咱们将这个规律抽 象成公式就是: 若是 T1(n)=O(f(n)) , T2(n)=O(g(n)) ;那么 T(n)=T1(n)+T2(n)=max(O(f(n)), O(g(n))) =O(max(f(n),g(n))).
3.乘法法则:嵌套代码的复杂度等于嵌套内外复杂度的乘积。
若是T1(n)=O(f(n)),T2(n)=O(g(n));
那么T(n)=T1(n)xT2(n)=O(f(n))xO(g(n))=O(f(n)xg(n)). 也就是说,假设T1(n)=O(n),T2(n)=O(n^2),则T1(n)xT2(n)=O(n^3)。
落实到具体的代码上
1 int cal(int n) { 2 int ret = 0; 3 int i = 1; 4 for (; i < n; ++i) { 5 ret = ret + f(i); 6 } 7 } 8 9 int f(int n) { 10 int sum = 0; 11 int i = 1; 12 for (; i < n; ++i) { 13 sum = sum + i; 14 } 15 return sum; 16 }
咱们单独看cal()函数。假设f()只是一个普通的操做,那第4~6行的时间复杂度就是,T1(n)=O(n)。
但f()函数自己不是一个简单的操做,它的时间复杂度是T2(n)=O(n),因此,整个cal)函数的时间复杂度就是,T(n)=T1(n)xT2(n)=O(nxn)=O(n2)。
常见的时间复杂度实例分析
分类:
多项式量级:
非多项式量级:O(2n)和O(n!)。
咱们把复杂度为非多项式量级的算法问题叫作NP(Non-Deterministic Polynomial,非肯定多项式)问题。
当数据规模n愈来愈大时,非多项式量级算法的执行时间会急剧增长,求解问题的执行时间会无限增加。因此,非多项式时间复杂度的算法实际上是很是低效的算法。所以,关于NP时间复杂度问题略。
主要来看几种常见的多项式时间复杂度。
Next…