第一章 算法在计算中的作用
练习
1.1 算法
1.1-1
排序的例子:一个医生要接诊的病人会按照挂号时间排序。
1.1-2
热效率,衡量发动机等热能转换装置能量利用的效率
1.1-3
数据结构:链表
优势:顺序访问效率高
局限:随机访问效率低
1.1-4
相似之处:都是点与点之间的移动;都是求最短的移动路线
不同:最短路径问题有明确的解法,而旅行商问题是NP完全问题,没有有效算法。
1.1-5
必须是最佳解的例子:一个数学定理的证明
可以使用近似解的例子:手术方案的建立
1.2 作为一种技术的算法
1.2-1
应用层的例子:一个压缩软件,在应用层就会使用压缩算法。
算法的功能:应该将输入的数据无损地转换为更小的数据
1.2-2
当n的取值为2-6时插入排序优于归并排序
1.2-3
n最小为15时在同一台机器上 100 n 2 100n^2 100n2的算法优于 2 n 2^n 2n的算法
思考题
1-1 运行时间的比较
思路:先将给定时间转换为毫秒,然后用 f ( n ) f(n) f(n)的逆函数求出对应的n值
1秒钟(1000ms) | 1分钟(60000ms) | 1小时(3600000ms) | 1天(86400000ms) | 1月(2592000000ms) | 1年(31536000000ms) | 1世纪(3153600000000ms) | |
---|---|---|---|---|---|---|---|
lg n \lg{n} lgn | 1 0 1000 10^{1000} 101000 | 1 0 60000 10^{60000} 1060000 | 1 0 3600000 10^{3600000} 103600000 | 1 0 86400000 10^{86400000} 1086400000 | 1 0 2592000000 10^{2592000000} 102592000000 | 1 0 31536000000 10^{31536000000} 1031536000000 | 1 0 3153600000000 10^{3153600000000} 103153600000000 |
n \sqrt{n} n | 100 0 2 1000^{2} 10002 | 6000 0 2 60000^{2} 600002 | 360000 0 2 3600000^{2} 36000002 | 8640000 0 2 86400000^{2} 864000002 | 259200000 0 2 2592000000^{2} 25920000002 | 3153600000 0 2 31536000000^{2} 315360000002 | 315360000000 0 2 3153600000000^{2} 31536000000002 |
n n n | 1000 1000 1000 | 60000 60000 60000 | 3600000 3600000 3600000 | 86400000 86400000 86400000 | 2592000000 2592000000 2592000000 | 31536000000 31536000000 31536000000 | 3153600000000 3153600000000 3153600000000 |
n 2 n^2 n2 | 31 31 31 | 244 244 244 | 1897 1897 1897 | 9295 9295 9295 | 50911 50911 50911 | 177583 177583 177583 | 1 , 775 , 837 1,775,837 1,775,837 |
n 3 n^3 n3 | 10 10 10 | 39 39 39 | 153 153 153 | 442 442 442 | 1373 1373 1373 | 3159 3159 3159 | 14 , 664 14,664 14,664 |
2 n 2^n 2n | 9 9 9 | 15 15 15 | 21 21 21 | 26 26 26 | 31 31 31 | 34 34 34 | 41 41 41 |
n ! n! n! | 6 6 6 | 8 8 8 | 9 9 9 | 11 11 11 | 12 12 12 | 13 13 13 | 15 15 15 |
第二章 算法基础
练习
2.1 插入排序
2.1-1
2.1-2
def insertionSortNonAscending(A):
for j in range(1,len(A)):
key = A[j]
i = j - 1
while i >=0 and A[i] < key:
A[i+1] = A[i]
i -= 1
A[i+1] = key
2.1-3
def linearFind(A, v):
for j in range(len(A)):
if A[j] == v:
return j
return None
循环不变式:已经搜索的子数组A[0…j-1]中不存在v
- 初始化:迭代开始之前(当0=1时)子数组A[0…j-1]中一个元素也没有,循环不变式成立
- 保持:如果A[j]等于v,循环终止。反之A[0…j]中不存在v,循环不变式成立
- 终止:当j等于A.length时循环终止,此时已经搜索的子数组A[0…j-1]即原数组A[0…n]。而如果在循环中A[j]等于v时循环终止,此时前一次迭代已经搜索的子数组A[0…j-1]中没有v,循环不变式成立
2.1-4
二进制加法问题:
输入:n个数的两个序列, A = < a 1 , a 2 , ⋯ , a n > A=<a_1,a_2,\cdots,a_n> A=<a1,a2,⋯,an>和 B = < b 1 , b 2 , ⋯ , b n > B=<b_1,b_2,\cdots,b_n> B=<b1,b2,⋯,bn>,A和B表示两个二进制数每个数位的依次排列
输出: n+1个数的序列 C = < c 1 , c 2 , ⋯ , c n + 1 > C=<c_1,c_2,\cdots,c_{n+1}> C=<c1,c2,⋯,cn+1>,C为二进制数A与B的和
def binarySum(A, B):
carry = 0
C = []
for i in range(len(A)-1,-1,-1):
sum = A[i] + B[i] + carry
C.insert(0, sum % 2)
carry = sum // 2
C.insert(0, carry)
return C
2.2 分析算法
2.2-1
Θ ( n 3 ) \Theta(n^3) Θ(n3)
2.2-2
def selectionSort(A):
for i in range(len(A)-1):
min = i
for j in range(i+1,len(A)):
if A[min] > A[j]:
min = j
A[min],A[i]=A[i],A[min]
循环不变式:A[0…i-1]为已排序好的序列
考虑最后一次循环,此时i=n-1,A[1…n-2]为已排序的序列且都小于A[n-1]和A[n],当将A[n]和A[n-1]中最小的放到A[n-1]处时,A[n]处的数据也回到了正确的位置,所以只循环n-1次就足够。
最好情况和最坏情况都是 Θ ( n 2 ) \Theta(n^2) Θ(n2)的运行时间
2.2-3
平均情况和最坏情况运行时间均为 Θ ( n ) \Theta(n) Θ(n)。
证明:
平均情况时,假设v有n+1种情况,v等可能的出现在A中任一位置或者不出现。
所以平均的运行时间为 1 + 2 + ⋯ + n n + 1 = 1 2 n ( n + 1 ) n + 1 = 1 2 n = Θ ( n ) \cfrac{1+2+\cdots+n}{n+1}=\cfrac{\frac{1}{2}n(n+1)}{n+1}=\frac{1}{2}n=\Theta(n) n+11+2+⋯+n=n+121n(n+1)=21n=Θ(n)
最坏情况时不出现,总共循环比较n次,运行时间为 Θ ( n ) \Theta(n) Θ(n)。
2.2-4
???不太确定题的意思
大概是保证最好情况时循环能够直接结束程序给出答案吧。
2.3 设计算法
2.3-1
2.3-2
def merge(arr, p, q, r):
n1 = q - p + 1
n2 = r - q
L = arr[p:q + 1]
R = arr[q + 1:r + 1]
i = j = 0
k = p
while i < n1 and j < n2:
if L[i] <= R[j]:
arr[k] = L[i]
i += 1
else:
arr[k] = R[j]
j += 1
k += 1
while i < n1:
arr[k] = L[i]
k += 1
i += 1
while j < n2:
arr[k] = R[j]
k += 1
j += 1
2.3-3
首先:当k=1时 n = 2 k = 2 n=2^k=2 n=2k=2, T ( n ) = T ( 2 ) = 2 , n lg n = 2 T(n)=T(2)=2,n\lg n=2 T(n)=T(2)=2,nlgn=2,所以 T ( n ) = n lg n T(n)=n\lg n T(n)=nlgn
假设:k=m时等式成立, n = 2 m , T ( n ) = n lg n = m 2 m n=2^m,T(n)=n\lg n=m2^m n=2m,T(n)=nlgn=m2m。因此k=m+1时将 n = 2 m + 1 n=2^{m+1} n=2m+1带入 T ( n ) = 2 T ( n 2 ) + n T(n)=2T(\cfrac{n}{2})+n T(n)=2T(2n)+n中,得 T ( n ) = 2 T ( 2 m ) + 2 m + 1 = m 2 m + 1 + 2 m + 1 = ( m + 1 ) 2 m + 1 T(n)=2T(2^m)+2^{m+1}=m2^{m+1}+2^{m+1}=(m+1)2^{m+1} T(n)=2T(2m)+2m+1=m2m+1+2m+1=(m+1)2m+1,而 n lg n = 2 m + 1 lg ( 2 m + 1 ) = ( m + 1 ) 2 m + 1 n\lg n= 2^{m+1}\lg(2^{m+1})=(m+1)2^{m+1} nlgn=2m+1lg(2m+1)=(m+1)2m+1,所以 T ( n ) = n lg n T(n)=n\lg n T(n)=nlgn成立
综上所述,当n刚好是2的幂时,本题递归式的解为 T ( n ) = n lg n T(n)=n\lg n T(n)=nlgn
2.3-4
T ( n ) = { Θ ( 1 ) , n = 1 T ( n − 1 ) + Θ ( n ) , n > 1 T(n)= \begin{cases} \Theta(1) ,&n=1\\ T(n-1) +\Theta(n),&n>1 \end{cases} T(n)={ Θ(1),T(n−1)+Θ(n),n=1n>1
2.3-5
def binaryFind(A, v, p, q):
if p <= q:
r = (p + q) // 2
if A[r] == v:
return r
elif A[r] < v:
return binaryFind(A, v, r + 1, q)
else:
return binaryFind(A, v, p, r - 1)
else:
return None
最坏情况下,有如下递归式:
T ( n ) = { Θ ( 1 ) , n = 1 T ( n 2 ) + Θ ( 1 ) , n > 1 T(n)= \begin{cases} \Theta(1) ,&n=1\\ T(\cfrac{n}{2}) +\Theta(1),&n>1 \end{cases} T(n)=⎩⎨⎧Θ(1),T(2n)+Θ(1),n=1n>1
显然有:
lg n { T ( n ) − T ( n 2 ) = Θ ( 1 ) T ( n 2 ) − T ( n 2 2 ) = Θ ( 1 ) T ( n 2 2 ) − T ( n 2 3 ) = Θ ( 1 ) ⋯ ⋯ T ( 2 ) − T ( 1 ) = Θ ( 1 ) \lg n \begin{cases} T(n)-T(\cfrac{n}{2})=\Theta(1)\\ T(\cfrac{n}{2})-T(\cfrac{n}{2^2})=\Theta(1)\\ T(\cfrac{n}{2^2})-T(\cfrac{n}{2^3})=\Theta(1)\\ \cdots\cdots\\ T(2)-T(1)=\Theta(1) \end{cases} lgn⎩⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎧T(n)−T(2n)=Θ(1)T(2n)−T(22n)=Θ(1)T(22n)−T(23n)=Θ(1)⋯⋯T(2)−T(1)=Θ(1)
所以最坏情况的运行时间为 Θ ( lg n ) \Theta(\lg n) Θ(lgn)
2.3-6
不能,2.1节中INSERTION-SORT的while循环同时起到两个作用:找到已排序的n个数中要插入的位置和将该位置之后的元素向后移动一位,运行时间是 Θ ( n ) \Theta(n) Θ(n)。最坏情况时,找到位置的需求可以使用二分查找来降低运行时间为 Θ ( lg n ) \Theta(\lg n) Θ(lgn),但移动元素的运行时间还是 Θ ( n ) \Theta(n) Θ(n),所以while循环的运行时间无法通过使用二分查找降低至 Θ ( lg n ) \Theta(\lg n) Θ(lgn),所以最坏情况运行时间也无法使用二分查找改进到 Θ ( n lg n ) \Theta(n\lg n) Θ(nlgn)的运行时间。
2.3-7
使用归并排序将集合的元素排序,时间复杂度为 Θ ( n lg n ) \Theta(n\lg n) Θ(nlgn),假设排好序的元素组成序列 A = < a 1 , a 2 , ⋯ , a n > A=<a_1,a_2,\cdots,a_n> A=<a1,a2,⋯,an>,其中 a 1 ⩽ a 2 ⩽ ⋯ ⩽ a n a_1\leqslant a_2\leqslant\cdots\leqslant a_n a1⩽a2⩽⋯⩽an。从数列两端遍历,让left= a 1 a_1 a1,right= a n a_n an,考虑left+right和x的关系。假设此时left= a l a_l al,right= a r a_r ar
如果left+right==x,说明正好找到了题目要求的两个数。
如果left+right>x,那么right右边的所有数 a r ′ a_{r'} ar′都有left + a r ′ > +a_{r'}> +ar′>left + a r > +a_{r}> +ar>x。此时将right左移一位,重新判断。
如果left+right<x,那么left左边的所有数 a l ′ a_{l'} al′都有 a l ′ + a_{l'}+ al′+right > a l >a_{l} >al+right > > >x。此时将left右移一位,重新判断。
当left和right重叠时说明已经找遍了可能产生x的两个数的组合。
可以看出,最好情况一次找到时间复杂度为 Θ ( 1 ) \Theta(1) Θ(1),最坏情况会遍历一遍排序完的序列,时间复杂度为 Θ ( 1 ) × n = Θ ( n ) \Theta(1)\times n=\Theta(n) Θ(1)×n=Θ(n),但无论什么情况显然远远小于 Θ ( n lg n ) \Theta(n\lg n) Θ(nlgn)的排序时间,所以该算法的时间复杂度为 Θ ( n lg n ) \Theta(n\lg n) Θ(nlgn)。
思考题
2-1
a.插入排序n个元素的最坏情况时间复杂度为 Θ ( n 2 ) \Theta(n^2) Θ(n2)。所以 n k \cfrac{n}{k} kn个子表插入排序的时间复杂度为 Θ ( k 2 ) × n k = Θ ( n k ) \Theta(k^2)\times\cfrac{n}{k}=\Theta(nk) Θ(k2)×kn=Θ(nk)
b.归并两个长度为k的子表时间复杂度为 Θ ( k ) \Theta(k) Θ(k),归并 n k \cfrac{n}{k} kn个子表时间复杂度为 Θ ( k ) × Θ ( n k ) = Θ ( n ) \Theta(k)\times\Theta(\cfrac{n}{k})=\Theta(n) Θ(k)×Θ(kn)=Θ(n),可知每轮归并时间复杂度为 Θ ( n ) \Theta(n) Θ(n), n k \cfrac{n}{k} kn个子表需要归并 Θ ( lg ( n k ) ) \Theta(\lg(\cfrac{n}{k})) Θ(lg(kn))轮,所以合并子表最坏情况时间复杂度为 Θ ( n ) × Θ ( lg ( n k ) ) = Θ ( n lg ( n k ) ) \Theta(n)\times\Theta(\lg(\cfrac{n}{k}))=\Theta(n\lg(\cfrac{n}{k})) Θ(n)×Θ(lg(kn))=Θ(nlg(kn))
c.有等式 Θ ( n k + n lg ( n k ) )