《算法导论》 第二章 练习题 Exercise
2.2-1
Θ(n^3)
2.2-2
for j = 1 to A.length - 1 min = j // The minimum value of sequence A [j..A.length-1] is added to the end of sequence A [1..j-1] i = j + 1 while i < A.length if A[i] < A[min] min = i if A[j] != A[min] swap A[j] and A[min]
该算法维持的循环不变式:
I)初始化:循环第一次迭代前,j = 1,子序列 A[1..j-1] 为空。这个序列包含了 j-1 -1 +1 =0 个序列 A[j..A.length-1]中最小的元素。
II)保持:该算法从序列 A[j..A.length] 中选取一个最小值 A[min] 加入到序列 A[1..j-1] 的末尾,所以在每一次 swap 操作后,子序列 A[1..j] 将包含 j 个元素,且序列 A 的第 j 小的值位于 A[1..j] 第 j 位。随后 j++,为下次迭代重新建立起了循环不变式。
III)终止:循环结束时 j = A.length,此时在子序列 A[1..j-1] 中有 j-1 = A.length-1 个最小值且第 A.length-1 小的值位于第 A.length-1 位,而对于 A[1..A.length] 而言,剩下一个A[A.length],而这个第 A.length 小的值刚好位于第 A.length 位。故无需对第 A.length 位运行该算法,因为当循环结束时第 A.length 位自然就排序好了。
因为选择排序是对 A[j..A.length] 进行选取最小值的操作,所以对于最好和最坏的情况,运行时间是一样的。
由于增长量级最受 while i < A.length 的影响,而这一句的执行次数是
所以运行时间是 Θ(n^2)
2.2-3
平均情况的运行时间的求法: ,其中 i 是问题规模为 n 的实例, P(i) 是实例 i 的概率,而T(i) 是实例 i 的运行时间。
假设序列中的每一个元素都有固定 p 的概率是我们需要查找的元素v。一个序列有 k 个元素,若前 k-1 个元素都不是我们想要的元素,则第 k 个元素是我们想要的元素。这意味着当步数为 k 且它是我们想要的元素的概率是
但也有可能出现所有元素都不是我们想要的元素,它的概率是
通过将步数乘以它对应情况发生的概率,得到期望值关于步数的函数:
最坏情况很明显是遍历序列,需要查找 A.length 步,运行时间是 Θ(A.length)
接下来计算平均情况,我们先把上面的期望函数化简,化简过程利用了双求和、等差求和与等比求和:
因为 > 0,所以 E(steps) < ,又因为 A.Length ≥ 1,所以
因此,对于一个给定上下界的序列,它平均情况的运行时间的增长量级与 p 和 A.length 有关,因为我们在一开始就设了 p 是固定值即 p = Θ(1),所以现在只考虑序列长度对运行时间的影响,有 T(n) = Θ(A.length)
当然求平均时间还有另一种的办法,假设这个序列中一定存在你想要的元素,且每一个位置的元素是该元素的概率都是等可能性的,在这种情况下,最坏情况的查找次数与运行时间是不变的,而平均情况的查找次数是 步,运行时间即是 Θ(A.length)
2.2-4
算法刚开始时候先检测数据,如果数据满足特殊条件则直接输出相对应结果,比如一个排序算法,如果输入的数据是顺序的,那么不用再排序而是直接输出即可。
2.3-1
mergeSort (A, p , r) if p < r q = ⌊p+r/2⌋ //向下取整 mergeSort (A, p, q ) mergeSort (A, q+1, r) merge (A, p, q, r)
归并排序遵循分治法,在每层递归都有三个步骤:
I)分解:将数组A分解成2个子问题,每个问题的规模是原问题的一半,即 A1 = <3, 41, 52, 26>,A2 = <38, 58, 9, 49>
II)解决: 使用归并排序对两个子问题进行递归地排序,当待排序的长度为1时,递归“回升”,不做任何操作,因为此时长度为1的序列都是有序的。
III)合并:合并两个已经排好序的子序列。
base case :p == r 时序列仅有一个元素,可看作已经是排序好的了,终止递归
操作流程图如下:
2.3-2
merge (A, p, q, r) n1 = q - p + 1 n2 = r - q let L[1..n1], R[1..n2] be new arrays for i = 1 to n1 L[i] = A[p+i-1] for j = 1 to n2 R[j] = A[q+i] i = 1 j = 1 for k = p to r if i <= n1 and j <= n2 if L[i] < R[j] A[k] = L[i] i = i + 1 else A[k] = R[j] j = j + 1 else if i > n1 A[k] = R[j] j = j +1 else if j > n2 A[k] = R[i] i = i + 1
2.3-3
由题意,设 n = 2k 。当 k = 1 时,n = 2,T(2) = 2 lg2 = 2,T(n) = n lgn 成立。
假设当 k 时成立,即
当 k+1 时, 有如下推导:
所以对n = 2k,k是任意自然数,该递归式的解都是 T(n) = n lgn
2.3-4
递归版本的插入排序如下:
insertionSort (A, p, r) //base case if p == r return NIL //recursive case if p < r q = r - 1 insertionSort (A, p, q) //let A[1..i] be sorted key = A[r] i = q while i > 0 and A[i] > key A[i+1] = A[i] i = i - 1 A[i+1] = key return A
最坏运行情况是全部元素都是按逆序排序,需要移动 n-1 次,此时插入步骤所耗的时间将是 Ι(n-1) = Θ(n)
2.3-5
二分查找迭代版本
binarySearch(A, q, r, v) while q <= r mid = ⌊q+r⌋ / 2 if A[mid] == v reutrn mid if A[mid] < v q = mid + 1 else r = mid - 1 return NIL
二分查找递归版本
binarySearch (A, p, r, v) if p > r return NIL mid = ⌊p + r⌋ / 2
if A[mid] == v return mid
//recursive case if A[mid] < v return binarySearch (A, mid+1, r, v) if A[mid] > v return binarySearch (A, q, mid-1, v)
由于二分查找的递归树树高为 lgn,而每次查找的时间是 Θ(1),所以它的最坏情况运行时间的递归式是,时间总代价为 c lgn ∈ Θ(lgn)
2.3-6
不行。虽然我们可以在插入算法的基础上(插入算法令 A[1..i-1] 保持有序)利用二分查找去优化查询关键字的效率,优化的算法如下
biInsertionSort (A) for i = 2 to A.length key = A[i] low = 1 high = i - 1 // find a smaller value than A[i] of sorted sequence A[1..i-1] while (low <= high) mid = (low + high) / 2 if A[mid] < key low = mid + 1 if A[mid] > key high = mid - 1 // move A[mid..i-1] backwards such that A[i] insert A[mid] for j = mid to i-1 A[j+1] = A[j] A[mid] = key
但是可以发现,在查找的步骤后还需要将整个数组向后移动,而移动是线性的,每一轮都是 Θ(n),所以最坏情况的运行时间还是 Θ(n2)。
2.3-7
//先归并排序后二分查找 TwoNumberSum (S, x) mergeSort(S, 1, n) for j = 1 to n tar = x - S[j] j2 = BinarySearch(S, tar) if j2 != NIL break if j2 == NIL return false else return true
我采用的是先将S排序后,进行一轮循环查找 x-S[j],循环不变式的终止条件是找到 x-S[j] 的值。排序采用的是归并排序,最坏情况的时间代价为 Θ(n lgn),而查找采用的是二分查找,最坏情况的时间代价为 Θ(lgn),由于查找经历了 n 轮,所以查找的总代价 Θ(n lgn)。所以该算法最坏情况的时间代价为 Θ(n lgn)
还有一种办法,先归并排序,后利用两个指针分别指向头和尾,往中间扫描
TwoNumberSum (S, x) mergeSort (S, 1, n) i = 1 j = n while i < j if A[i] + A[j] == x return true if A[i] + A[j] < x i = i + 1 if A[i] + A[j] > x j = j - 1 return false
可以看得出来,查找的时间仅需 Θ(n),所以时间总代价受排序时间代价的影响,为Θ(n lgn)
扫描的原理很简单,先设 mi,j : A[i] + A[j] < S,Mi,j : A[i] + A[j] > S 。由于序列已排序,则 mi,j ⇒∀k < j 都有 mi,k 成立,并且 Mi,j ⇒ ∀k > i 都有 Mk,j 成立
其实这道题是LeetCode #1 原题,还可以利用哈希存储优化时间复杂度,相关的博客链接:http://www.cnblogs.com/Bw98blogs/p/8058931.html