May 30, 2017
摘要: 1.介绍了算法性能分析的基本方法:渐进分析方法。 2.使用递归树的方法来求解递归问题的渐进复杂度。 3.介绍直接插入排序和归并排序两个例子。重点掌握归并排序算法的代码实现和时间复杂度分析。
在算法导论这门课中,最最重点的就是要学会分析算法。而算法的性能(performance)或者叫运行效率,是算法分析的主要内容。对于一个软件来说,有很多方面都是需要程序员考虑的,比如正确性、可维护性、可扩展性、健壮性、安全性、用户交互性等等。对于一个软件是否成功,这些因素都是必须考虑的。但是为什么我们在这里要这么重点地去分析算法的效率呢?因为上面提到的很多方面其实都离不开运行效率,就笑课上老师打的一个比方:认为钱重要还是水和饭重要?当然是水和饭,钱是不能保证人的生存的,但是钱却可以换来水和饭。而算法分析中的“效率”就相当于“钱”,你可以用“效率”来换取其他东西,比如安全性,稳定性等等。它只是一个交换物,但我们,却离不开它。[1]
一般情况下都是进行最坏情况分析,而最优情况分析实际上没有任何意义。
我们通常所说的运行时间,都会存在一个相对时间与绝对时间的区别。比如在一台巨型机和在一台微机上运行同一个程序,所用的时间显示是不同的。这是我们就需要引入一个更加宏观的概念:渐近分析
引入一个助记符号 $\Theta(n)$.
举一个例子:如果一个算法的运行时间为:$3n^3 + 2n^2 + 4n + 1$,那么忽略掉依赖机器的常量1,以及所有的低阶项 $2n^2$、$4n$,那么这个算法的时间复杂度就为$\Theta(n^3)$。
在这里,老师也进行了很形象的说明。如果算法A的渐近时间复杂度是$\Theta(n^3)$,算法B的是$\Theta(n^2)$,那么一定存在一个足够大的n,使得当数据规模大于n时,算法B的运行时间要小于A,不管算法A一开始的优势有多么大,不管算法B的渐近复杂度的系数和常数有多么大,都没有用。用这样一个助记符就可以将时间复杂度的分析独立于机器,独立于具体的常数,对我们分析算法将会十分有利。
# 插入排序
def insert_sort(A, n):
"""A 为一个序列,n 为序列的长度。这里所有的下标都是从 0 开始,而课堂上从 1 开始。"""
for j in xrange(1,n): # 按照顺序从第二个元素开始逐个插入
key = A[j]
i = j - 1
while (i >= 0) & (A[i] > key):
A[i+1] = A[i]
i = i - 1
A[i+1] = key
return A
# Eg:
A = [8, 2, 4, 9, 3, 6, 7]
n = len(A)
print 'Before sorted: ', A
A = insert_sort(A, n)
print 'After sorted: ', A
插入排序需要的辅助空间为 $O(1)$, 是一种稳定的排序算法。
归并排序是一种分治问题,通过递归的方式来解决问题。在每一层的递归中,应用下面三个步骤:
归并排序包括下面三个步骤。
关于求解递归式有三种方法:
在这节课上,主要介绍了使用递归树的方法来求解归并排序。
从上面归并排序的三个步骤分析,则有:
\begin{equation} T(n)= \left\{ \begin{array}{lr} \Theta(1), &if\ n = 1; \\ 2T(n/2) + \Theta(n), &if\ n> 1. \end{array} \right. \end{equation}现在引入递归树来求解 $T(n)=2T(n/2) + cn, 其中\ c > 0 为常数。$把上面的式子转换成一棵树。按照下面图 2~6 来推算。
所以归并排序的渐进时间复杂度为 $\Theta(nlgn)$,这比插入排序的 $\Theta(n^2)$ 增长要慢得多。实际上,当$n>30$的时候,归并排序优于插入排序。
下面是用 python 写的归并排序的代码。[2] 归并排序详解(python实现)
一共就两个函数。 "merge(a,b)" 函数将两个序列进行合并。"merge_sort(_list)" 函数,对序列 _list 从下(最小规模)往上进行合并排序。
# 归并排序
def merge(a,b):
"""a,b 是两个有序的序列,将a,b合并到c中返回。"""
c = list()
i = j = 0
while i < len(a) and j < len(b): # 若两个list中都还有元素
if a[i] < b[j]: # 从两者中选出较小的一个添加到 c 中
c.append(a[i])
i += 1
else:
c.append(b[j])
j += 1
if i == len(a): # 如果 a 已经全部添加到 c, 把 b 剩下的部分全部添加到 c 中
c.extend(b[j:])
else: # 否则 b 已经全部添加到 c 中,把 a 剩下的部分全部添加到 c 中
c.extend(a[i:])
return c
def merge_sort(_list):
"""用递归的方式来对整个序列 _list 进行合并排序。"""
if len(_list) <= 1: # 2.当只有一个元素的时候,直接求解。开始往上合并。
return _list
middle = len(_list) / 2 # 1.划分子序列。
left = merge_sort(_list[:middle])
right = merge_sort(_list[middle:])
return merge(left, right) # 3.合并子序列
# Eg:
A = [8, 2, 4, 9, 3, 6, 7]
n = len(A)
print 'Before sorted: ', A
A = merge_sort(A)
print 'After sorted: ', A
下面比较两个函数的实际运行效率:
import numpy as np
from time import time
size = 1000
list1 = np.random.randint(-100, 100, size)
time0 = time()
list_sorted = insert_sort(list1, size)
print 'Size: %d, Insert_sort costs %g seconds.'% (size, time() - time0)
time0 = time()
list_sorted = merge_sort(list1)
print 'Size: %d, Merge_sort costs %g seconds.'% (size, time() - time0)
size = 10000
list1 = np.random.randint(-100, 100, size)
time0 = time()
list_sorted = insert_sort(list1, size)
print 'Size: %d, Insert_sort costs %g seconds.'% (size, time() - time0)
time0 = time()
list_sorted = merge_sort(list1)
print 'Size: %d, Merge_sort costs %g seconds.'% (size, time() - time0)
从上面的结果来看,归并排序的运行时间要远远小于直接插入排序。
总结
参考
[1] MIT算法导论——第一讲.Analysis of algorithm
[2] 归并排序详解(python实现)