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

一、大 O 复杂度表示法

算法的执行效率,粗略地讲,就是算法代码执行的时间。但是,如何在不运行代码的情况下,用“肉眼”得到一段代码的执行时间呢?这里有段非常简单的代码,求 1,2,3…n 的累加和。现在,来估算一下这段代码的执行时间。

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 遍,所以需要 2n*unit_time 的执行时间,所以这段代码总的执行时间就是 (2n+2)*unit_time。可以看出来,所有代码的执行时间 T(n) 与每行代码的执行次数成正比。按照这个分析思路,我们再来看这段代码。

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  }

我们依旧假设每个语句的执行时间是 unit_time。那这段代码的总执行时间 T(n) 是多少呢?第 2、3、4 行代码,每行都需要 1 个 unit_time 的执行时间,第 5、6 行代码循环执行了 n 遍,需要 2n * unit_time 的执行时间,第 7、8 行代码循环执行了 n2遍,所以需要 2n2* unit_time 的执行时间。所以,整段代码总的执行时间 T(n) = (2n2+2n+3)*unit_time。尽管我们不知道 unit_time 的具体值,但是通过这两段代码执行时间的推导过程,我们可以得到一个非常重要的规律,那就是,所有代码的执行时间 T(n) 与每行代码的执行次数 n 成正比。我们可以把这个规律总结成一个公式。注意,大 O 就要登场了!

T(n) = O(f(n))

解释一下这个公式。其中,T(n) 我们已经讲过了,它表示代码执行的时间;n 表示数据规模的大小;f(n) 表示每行代码执行的次数总和。因为这是一个公式,所以用 f(n) 来表示。公式中的 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。

所以,第一个例子中的 T(n) = O(2n+2),第二个例子中的 T(n) = O(2n2+2n+3)。这就是大 O 时间复杂度表示法。大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度。当 n 很大时,你可以把它想象成 10000、100000。而公式中的低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略。我们只需要记录一个最大量级就可以了,如果用大 O 表示法表示刚讲的那两段代码的时间复杂度,就可以记为:T(n) = O(n); T(n) = O(n2)。

二、时间复杂度分析

前面介绍了大 O 时间复杂度的由来和表示方法。现在我们来看下,如何分析一段代码的时间复杂度?

1. 只关注循环执行次数最多的一段代码

大 O 这种复杂度表示方法只是表示一种变化趋势。我们通常会忽略掉公式中的常量、低阶、系数,只需要记录一个最大阶的量级就可以了。所以,我们在分析一个算法、一段代码的时间复杂度的时候,也只关注循环执行次数最多的那一段代码就可以了。这段核心代码执行次数的 n 的量级,就是整段要分析代码的时间复杂度。为了便于理解,还是拿前面的例子来说明。

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  }

其中第 2、3 行代码都是常量级的执行时间,与 n 的大小无关,所以对于复杂度并没有影响。循环执行次数最多的是第 4、5 行代码,所以这块代码要重点分析。前面我们也讲过,这两行代码被执行了 n 次,所以总的时间复杂度就是 O(n)。

2. 加法法则:总复杂度等于量级最大的那段代码的复杂度

 1 int cal(int n) {
 2    int sum_1 = 0;
 3    int p = 1;
 4    for (; p < 100; ++p) {
 5      sum_1 = sum_1 + p;
 6    }
 7
 8    int sum_2 = 0;
 9    int q = 1;
10    for (; q < n; ++q) {
11      sum_2 = sum_2 + q;
12    }
13
14    int sum_3 = 0;
15    int i = 1;
16    int j = 1;
17    for (; i <= n; ++i) {
18      j = 1;
19      for (; j <= n; ++j) {
20        sum_3 = sum_3 +  i * j;
21      }
22    }
23
24    return sum_1 + sum_2 + sum_3;
25  }

这个代码分为三部分,分别是求 sum_1、sum_2、sum_3。我们可以分别分析每一部分的时间复杂度,然后把它们放到一块儿,再取一个量级最大的作为整段代码的复杂度。第一段的时间复杂度是多少呢?这段代码循环执行了 100 次,所以是一个常量的执行时间,跟 n 的规模无关。这里再强调一下,即便这段代码循环 10000 次、100000 次,只要是一个已知的数,跟 n 无关,照样也是常量级的执行时间。当 n 无限大的时候,就可以忽略。尽管对代码的执行时间会有很大影响,但是回到时间复杂度的概念来说,它表示的是一个算法执行效率与数据规模增长的变化趋势,所以不管常量的执行时间多大,我们都可以忽略掉。因为它本身对增长趋势并没有影响。那第二段代码和第三段代码的时间复杂度是多少呢?答案是 O(n) 和 O(n2)。综合这三段代码的时间复杂度,我们取其中最大的量级。所以,整段代码的时间复杂度就为 O(n2)。也就是说:总的时间复杂度就等于量级最大的那段代码的时间复杂度。那我们将这个规律抽象成公式就是:如果 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. 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

 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) * T2(n) = O(n*n) = O(n2)。

4、几种常见时间复杂度实例分析

 4.1. O(1)

首先你必须明确一个概念,O(1) 只是常量级时间复杂度的一种表示方法,并不是指只执行了一行代码。比如这段代码,即便有 3 行,它的时间复杂度也是 O(1),而不是 O(3)。

 int i = 8;
 int j = 6;
 int sum = i + j;

只要代码的执行时间不随 n 的增大而增长,这样代码的时间复杂度我们都记作 O(1)。或者说,一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)。

4.2. O(logn)、O(nlogn)

对数阶时间复杂度非常常见,同时也是最难分析的一种时间复杂度。通过一个例子来说明一下。

1  i=1;
2  while (i <= n)  {
3    i = i * 2;
4  }

根据我们前面讲的复杂度分析方法,第三行代码是循环执行次数最多的。所以,我们只要能计算出这行代码被执行了多少次,就能知道整段代码的时间复杂度。从代码中可以看出,变量 i 的值从 1 开始取,每循环一次就乘以 2。当大于 n 时,循环结束。还记得我们高中学过的等比数列吗?实际上,变量 i 的取值就是一个等比数列。如果我把它一个一个列出来,就应该是这个样子的:

所以,我们只要知道 x 值是多少,就知道这行代码执行的次数了。通过 2x=n 求解 x 这个问题我们想高中应该就学过了,我就不多说了。x=log2n,所以,这段代码的时间复杂度就是 O(log2n)。

现在把上面的代码改造一下

 i=1;
 while (i <= n)  {
   i = i * 3;
 }

很简单就能看出来,这段代码的时间复杂度为 O(log3n)。实际上,不管是以 2 为底、以 3 为底,还是以 10 为底,我们可以把所有对数阶的时间复杂度都记为 O(logn)。为什么呢?我们知道,对数之间是可以互相转换的,log3n 就等于 log32 * log2n,所以 O(log3n) = O(C * log2n),其中 C=log32 是一个常量。基于我们前面的一个理论:在采用大 O 标记复杂度的时候,可以忽略系数,即 O(Cf(n)) = O(f(n))。所以,O(log2n) 就等于 O(log3n)。因此,在对数阶时间复杂度的表示方法里,我们忽略对数的“底”,统一表示为 O(logn)。

同理,如果一段代码的时间复杂度是 O(logn),我们循环执行 n 遍,时间复杂度就是 O(nlogn) 了。而且,O(nlogn) 也是一种非常常见的算法时间复杂度。比如,归并排序、快速排序的时间复杂度都是 O(nlogn)。

4.3. O(m+n)、O(m*n)

再来看一种跟前面都不一样的时间复杂度,代码的复杂度由两个数据的规模来决定。老规矩,先看代码!

 1 int cal(int m, int n) {
 2   int sum_1 = 0;
 3   int i = 1;
 4   for (; i < m; ++i) {
 5     sum_1 = sum_1 + i;
 6   }
 7
 8   int sum_2 = 0;
 9   int j = 1;
10   for (; j < n; ++j) {
11     sum_2 = sum_2 + j;
12   }
13
14   return sum_1 + sum_2;
15 }

从代码中可以看出,m 和 n 是表示两个数据规模。我们无法事先评估 m 和 n 谁的量级大,所以我们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。所以,上面代码的时间复杂度就是 O(m+n)。针对这种情况,原来的加法法则就不正确了,我们需要将加法规则改为:T1(m) + T2(n) = O(f(m) + g(n))。但是乘法法则继续有效:T1(m)*T2(n) = O(f(m) * f(n))。

三、空间复杂度分析

时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。类比一下,空间复杂度全称就是渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系。

void print(int n) {
  int i = 0;
  int[] a = new int[n];
  for (i; i <n; ++i) {
    a[i] = i * i;
  }

  for (i = n-1; i >= 0; --i) {
    print out a[i]
  }
}

跟时间复杂度分析一样,我们可以看到,第 2 行代码中,我们申请了一个空间存储变量 i,但是它是常量阶的,跟数据规模 n 没有关系,所以我们可以忽略。第 3 行申请了一个大小为 n 的 int 类型数组,除此之外,剩下的代码都没有占用更多的空间,所以整段代码的空间复杂度就是 O(n)。我们常见的空间复杂度就是 O(1)、O(n)、O(n2),像 O(logn)、O(nlogn) 这样的对数阶复杂度平时都用不到。而且,空间复杂度分析比时间复杂度分析要简单很多。

小结:

复杂度也叫渐进复杂度,包括时间复杂度和空间复杂度,用来分析算法执行效率与数据规模之间的增长关系,可以粗略地表示,越高阶复杂度的算法,执行效率越低。常见的复杂度并不多,从低阶到高阶有:O(1)、O(logn)、O(n)、O(nlogn)、O(n2)。

原文地址:https://www.cnblogs.com/lyt0207/p/12652351.html

时间: 2024-10-11 23:01:17

数据结构与算法复杂度分析的相关文章

学好数据结构和算法 —— 复杂度分析

复杂度也称为渐进复杂度,包括渐进时间复杂度和渐进空间复杂度,描述算法随数据规模变化而逐渐变化的趋势.复杂度分析是评估算法好坏的基础理论方法,所以掌握好复杂度分析方法是很有必要的. 时间复杂度 首先,学习数据结构是为了解决“快”和“省”的问题,那么如何去评估算法的速度快和省空间呢?这就需要掌握时间和空间复杂度分析.同一段代码运行在不同环境.不同配置机器.处理不同量级数据…效率肯定不会相同.时间复杂度和空间复杂度是不运行代码,从理论上粗略估计算法执行效率的方法.时间复杂度一般用O来表示,如下例子:计

数据结构基础 算法复杂度分析(一) 概念篇

为什么要进行算法分析? 预测算法所需的资源 计算时间(CPU 消耗) 内存空间(RAM 消耗) 通信时间(带宽消耗) 预测算法的运行时间 在给定输入规模时,所执行的基本操作数量,或者称为算法复杂度(Algorithm Complexity) 如何衡量算法复杂度? 内存(Memory) 时间(Time) 指令的数量(Number of Steps) 特定操作的数量 磁盘访问数量 网络包数量 渐进复杂度(Asymptotic Complexity) 算法的运行时间与什么相关? 取决于输入的数据.(例

数据结构基础 算法复杂度分析(二) 典例篇

示例代码(1) decimal Factorial(int n) { if (n == 0) return 1; else return n * Factorial(n - 1); } [分析] 阶乘(factorial),给定规模 n,算法基本步骤执行的数量为 n,所以算法复杂度为 O(n). 示例代码(2) int FindMaxElement(int[] array) { int max = array[0]; for (int i = 0; i < array.Length; i++)

转 算法复杂度分析

转自 http://www.cnblogs.com/gaochundong/p/complexity_of_algorithms.html 为什么要进行算法分析? 预测算法所需的资源 计算时间(CPU 消耗) 内存空间(RAM 消耗) 通信时间(带宽消耗) 预测算法的运行时间 在给定输入规模时,所执行的基本操作数量. 或者称为算法复杂度(Algorithm Complexity) 如何衡量算法复杂度? 内存(Memory) 时间(Time) 指令的数量(Number of Steps) 特定操作

算法9-4:最大流算法复杂度分析

前面一节介绍了Ford-Fulkerson算法.那么这个算法是否一定能够在有限步骤内结束?要多少步骤呢? 这个问题的答案是,该算法确实能够在有限步骤之内结束,但是至于需要多少步骤,就要仔细分析. 为了分析问题,需要假定图中所有边的容量都是整数.但是有个严重的问题,比如下图中,如果使用Ford-Fulkerson算法,需要迭代200次才能结束. 首先将所有边的容量都初始化为0. 第一次迭代和第二次迭代之后,两条边各增加了1. 到最后200次迭代之后整个算法才结束. 这还不算最坏的情况.因为整数最多

数据结构算法——算法复杂度分析

算法复杂度分为时间复杂度和空间复杂度 首先要清楚一点,大O表示法的时间复杂度高不代表程序运行时间长,空间复杂度高不代表占用空间多. 他们表示的是代码执行时间随着数据规模增长的变化趋势.和算法储存空间与数据规模之间的增长关系. 时间复杂度判断方法 1.只关注循环次数最多的一段代码 2.加法法则:总复杂度等于量级最大的那段代码的复杂度 3.乘法原则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积 常见的复杂度量级(按数量级递增) 常量阶:O(1) 对数阶:O(logn) 线性阶:O(n) 线性对数阶:

Java数据结构之算法时间度

1.度量一个程序(算法)执行时间的两种方法 1)事后统计的方法 这种方法可行, 但是有两个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行该程序:二是所得时间的统计量依赖于计算机的硬件.软件等环境因素, 这种方式,要在同一台计算机的相同状态下运行,才能比较那个算法速度更快. 2)事前估算的方法 通过分析某个算法的时间复杂度来判断哪个算法更优. 2.时间频度 基本介绍:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多.一个算法中的语句执行次数称为

算法复杂度分析方法以及算法概述

算法定义:解决特定问题的求解步骤的描述. 算法特性:有穷性.确定性.可行性.输入.输出 算法的设计要求:正确性.可读性.健壮性.高效率和低存储量需求 算法度量方法:事后统计方法(不科学).事前分析估算方法 函数的渐近增长:给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,那么我们说f(n)的增长渐近快于g(n). 于是,可以得出结论:判定一个算法好不好,可以对比算法的关键执行次数函数的渐近增长性,基本就可以分析出:某一个算法,随着n的变大,它会越

[算法]复杂度分析

时间复杂度 时间复杂度的分析 只关注循环执行次数最多的一段代码,因为使用大O表示法,其他执行次数较少的复杂度可以忽略 加法法则:总复杂度等于量级最大的那段代码的复杂度 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积 常见的时间复杂度示例 复杂度从低阶到高阶为:(复杂度越高阶,执行效率越低) O(1).O(logn).O(n).O(nlogn).O(n^2) O(1) 该时间复杂度表示代码的执行时间可以认为与输入n无关,是一个固定的值 O(logn)&O(nlogn) o(logn)复杂度