最坏情况与平均复杂度
上篇讨论的都是算法的运行时间直接取决于问题规模的大小N,但是在某些情况下,算法的运行时间不仅取决于问题的大小,还取决于代码的特定特性,看下面一段代码:
int linearSearch(int key, Vector<int> & vec) {
int n = vec.size();
for (int i = 0; i < n; i++) {
if (key == vec[i]) return i;
}
return -1;
}
其作用是在一个vector中,查找某个数值。它返回出现key的vec中的第一个下标位置,如果key不显示在向量中的任何位置,则返回-1。因为实现中的for循环执行了n次,所以你可能第一时间想到的LinearSearch的性能,就是O(N)。但是换个角度想,对linearSearch的调用有时候可以非常快地执行。例如,假设正在搜索的key元素恰好位于向量中的第一个位置。在这种情况下,for循环的其实只运行一次。如果你足够幸运地搜索始终位于向量开始处的值,linearSearch将始终在确定的时间内完成。 反之,当key不在vector中时,当然会发生linearSearch函数的最坏情况。因为当key不存在时,该函数必须完成for循环的所有n个循环,这意味着其性能是O(N)((因为O(n/2)忽略常数也是O(n)))。
当分析程序的计算复杂性时,我们通常对最小可能的时间不感兴趣。一般来说,我们往往会关注以下两种复杂性分析:
- 最坏情况下的复杂度。(Worst-case complexity.) 最常见的复杂性分析类型包括在最坏的情况下确定算法的性能。它允许你设置计算复杂度的上限。如果我们分析最坏的情况,那么之事可以保证算法的性能至少与你的分析表明一样好。因为没有什么情况比这个更糟糕了。
- 平均情况下的复杂度。(Average-case complexity) 从实际的角度来看,如果你在所有可能的输入数据集中平均计算其行为,那么考虑算法的平均执行情况通常是有用的。特别是当你没有办法确定你的问题的具体输入是非典型的输入,平均案例分析可提供实际绩效的最佳统计估计。
我们稍后介绍的“Quicksort算法”中,算法的平均情况和最差情况下的性能有时会以定性的方式存在差异,这意味着在实践中,要综合考虑到这两个情况。
Big-O的公式化定义(题外话,考研不考)
因为理解大O符号对现代计算机科学至关重要,因此提供更正式的定义以帮助我们了解大O的直观模型的原理以及为什么建议的大O公式的简化。在数学中,大O表示法用于表达两个函数之间的关系,在这样的表达式中:
该表达式的形式意义在于,f(N)是具有以下特征的t(N)的近似值:存在常数N0和正常数C,使得对于N≤N0的每个值, 以下条件成立:
换句话说,只要N“足够大”,函数t(N)总是被函数ƒ(N)的常数倍数界定。
当用于表达计算复杂度时,函数t(N)表示算法的实际运行时间,这通常很难计算。函数ƒ(N)是一个简单得多的公式,尽管如此,对于运行时间如何作为N的函数而言,提供了合理的定性估计,因为在大O的数学定义中表达的条件确保实际的运行时间不能增长比ƒ(N)快。 (即不会比Fn更大)、
让我们返回到选择排序示例。分析选择排序的循环结构表明,执行了最内循环中的操作,执行次数为
并且运行时间与这个公式成正比。当这种复杂性用大O表示法表示时,常数和低阶项被消除,只留下执行时间为O(N^2)的断言,这实际上是断言:
为了表明这个表达式在大O的正式定义下确实是真的,所有你需要做的是找到常量C和N0,使得:
对于N≥N0的所有值。这个特殊的例子非常简单。将常数C和N0都设置为1. 毕竟,只要N不小于1,就有(N^2)≥N。因此,放缩一下就变成这样:
所以,对于任意的N >= 1,都满足要求。
更一般的证明
可以使用类似的参数来表示任何k阶多项式,可以用一般式子来表示:
其big-O为
为了证明这个结论,我们的目标是寻找常数C和N0使得 对于任意的 N >= N0:
都成立。如前面的例子所示,首先使N0为1.对于N≥1的所有值,N的每个连续阶数至少与其前身一样大,因此
再次进行放缩(放大):
将右边的公因子提出来:
显然,左边的一堆绝对值相加是一个常数,用C来替换这个常数,有:
于是证明了上述整个多项式的算法复杂度就是
如果这些繁重的数学式子吓到了你,不用担心,不懂无所谓,更重要的是明白Big-O的含义,而非它的公式化过程,反正考研不考哈哈哈