为什么要进行算法分析?
- 预测算法所需的资源
- 计算时间(CPU 消耗)
- 内存空间(RAM 消耗)
- 通信时间(带宽消耗)
- 预测算法的运行时间
- 在给定输入规模时,所执行的基本操作数量。
- 或者称为算法复杂度(Algorithm Complexity)
如何衡量算法复杂度?
- 内存(Memory)
- 时间(Time)
- 指令的数量(Number of Steps)
- 特定操作的数量
- 磁盘访问数量
- 网络包数量
- 渐进复杂度(Asymptotic Complexity)
算法的运行时间与什么相关?
- 取决于输入的数据。(例如:如果数据已经是排好序的,时间消耗可能会减少。)
- 取决于输入数据的规模。(例如:6 和 6 * 109)
- 取决于运行时间的上限。(因为运行时间的上限是对使用者的承诺。)
算法分析的种类:
- 最坏情况(Worst Case):任意输入规模的最大运行时间。(Usually)
- 平均情况(Average Case):任意输入规模的期待运行时间。(Sometimes)
- 最佳情况(Best Case):通常最佳情况不会出现。(Bogus)
例如,在一个长度为 n 的列表中顺序搜索指定的值,则
- 最坏情况:n 次比较
- 平均情况:n/2 次比较
- 最佳情况:1 次比较
而实际中,我们一般仅考量算法在最坏情况下的运行情况,也就是对于规模为 n 的任何输入,算法的最长运行时间。这样做的理由是:
- 一个算法的最坏情况运行时间是在任何输入下运行时间的一个上界(Upper Bound)。
- 对于某些算法,最坏情况出现的较为频繁。
- 大体上看,平均情况通常与最坏情况一样差。
算法分析要保持大局观(Big Idea),其基本思路:
- 忽略掉那些依赖于机器的常量。
- 关注运行时间的增长趋势。
比如:T(n) = 73n3 + 29n3 + 8888 的趋势就相当于 T(n) = Θ(n3)。
渐近记号(Asymptotic Notation)通常有 O、 Θ 和 Ω 记号法。Θ 记号渐进地给出了一个函数的上界和下界,当只有渐近上界时使用 O 记号,当只有渐近下界时使用 Ω 记号。尽管技术上 Θ 记号较为准确,但通常仍然使用 O 记号表示。
使用 O 记号法(Big O Notation)表示最坏运行情况的上界。例如,
- 线性复杂度 O(n) 表示每个元素都要被处理一次。
- 平方复杂度 O(n2) 表示每个元素都要被处理 n 次。
计算代码块的渐进运行时间的方法有如下步骤:
- 确定决定算法运行时间的组成步骤。
- 找到执行该步骤的代码,标记为 1。
- 查看标记为 1 的代码的下一行代码。如果下一行代码是一个循环,则将标记 1 修改为 1 倍于循环的次数 1 * n。如果包含多个嵌套的循环,则将继续计算倍数,例如 1 * n * m。
- 找到标记到的最大的值,就是运行时间的最大值,即算法复杂度描述的上界。
案例1:
阶乘(factorial),给定规模 n,算法基本步骤执行的数量为 n,所以算法复杂度为 O(n)。
decimal Factorial(int n) { if (n == 0) return 1; else return n * Factorial(n - 1); }
案例2:
n 为数组 array 的大小,则最坏情况下需要比较 n 次以得到最大值,所以算法复杂度为 O(n)。
1 int FindMaxElement(int[] array) 2 { 3 int max = array[0]; 4 for (int i = 0; i < array.Length; i++) 5 { 6 if (array[i] > max) 7 { 8 max = array[i]; 9 } 10 } 11 return max; 12 }
案例3:n 为数组 array 的大小,则基本步骤的执行数量约为 n*(n-1)/2,所以算法复杂度为 O(n2)。
long FindInversions(int[] array) { long inversions = 0; for (int i = 0; i < array.Length; i++) for (int j = i + 1; j < array.Length; j++) if (array[i] > array[j]) inversions++; return inversions; }
案例4:给定规模 n 和 m,则基本步骤的执行数量为 n*m,所以算法复杂度为 O(n2)。
long SumMN(int n, int m) { long sum = 0; for (int x = 0; x < n; x++) for (int y = 0; y < m; y++) sum += x * y; return sum; }
案例5:给定规模 n,则基本步骤的执行数量约为 n*n*n ,所以算法复杂度为 O(n3)。
decimal Sum3(int n) { decimal sum = 0; for (int a = 0; a < n; a++) for (int b = 0; b < n; b++) for (int c = 0; c < n; c++) sum += a * b * c; return sum; }
案例6:插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的有序数据。算法适用于少量数据的排序,时间复杂度为 O(n2)。
private static void InsertionSortInPlace(int[] unsorted) { for (int i = 1; i < unsorted.Length; i++) { if (unsorted[i - 1] > unsorted[i]) { int key = unsorted[i]; int j = i; while (j > 0 && unsorted[j - 1] > key) { unsorted[j] = unsorted[j - 1]; j--; } unsorted[j] = key; } } }
参考:http://www.cnblogs.com/gaochundong/p/complexity_of_algorithms.html