算法导论课后习题解析 第六章
6.1-1
元素最少的情况是最底层只有一个叶子,即$2^h$;元素最多的情况是整棵树是满的,即$2^{h+1}-1$。(这里按照叶子到根的最大边数来定义高度)
6.1-2
设高度为h,那么可知
$$2^h \le n \le 2^{h+1}-1$$
$$\Rightarrow h \le \lg n \lt h+1$$
$$\Rightarrow \lg n - 1 \lt h \le \lg n$$
即$h = \lfloor \lg n \rfloor$。
6.1-3
令p(i)为第i个元素的父亲下标,那么对任意$i > 1$,都有$A[i] \le A[p(i)] \le A[p(p(i))] \le \cdots \le A[1]$,即都小于根元素。
6.1-4
最小的元素处于最底层或次底层的叶子结点,即该结点没有子结点。这时因为大根堆(max heap)中父结点一定不小于子结点,所以最小的元素必须出现在叶子结点上。
6.1-5
对于有序的数列肯定有$\forall i \lt j, a_i \lt a_j$,而$Parent(i) = i/2 \lt i$,故有$a_{Parent(i)} \lt a_i$,满足小根堆的性质。
6.1-6
树结构如下,元素7和6不满足大根堆的性质,故不是大根堆。
23 17 14 6 13 10 1 5 7 12
6.1-7
设堆的高度为h,那么除去最底层的结点的数量为$2^h = 2^{h+1}/2$个,所以底层结点的下标为$2^{h+1}/2 + 1, 2^{h+1}/2 + 2 \cdots ,n$,即$\lfloor n/2 \rfloor + 1, \lfloor n/2 \rfloor + 2, \cdots , n$
6.2-1
简单起见,我们只列出收影响的部分子树
3 10 1 8 9 0 --------- 10 3 1 8 9 0 --------- 10 9 1 8 3 0
6.2-2
只要把相应的最小元素移到父亲的位置即可
Min-Heapify(A, i) l = Left(A, i) r = Right(A, i) if l <= A.heap-size and A[l] < A[i] smallest = l else smallest = i if r <= A.heap-size and A[r] < A[smallest] smallest = r if smallest != i swap A[i] with A[smallest] Min-Heapify(A, smallest)
由于只改变了比较部分,该算法的时间复杂度与Max-Heapify一致。
6.2-3
不会有任何改变。
6.2-4
当$i \gt A.heap-size/2$时,结点为叶子结点没有孩子,所以不会有任何改变。
6.2-5
Max-Heapify(A, i) while true l = Left(A, i) r = Right(A, i) if l <= A.heap-size and A[l] > A[i] largest = l else largest = i if r <= A.heap-size and A[r] > A[largest] largest = r if largest != i swap A[i] with A[largest] i = largest else break
6.2-6
Max-Heapify最坏的情况下对从根结点到最深的叶子结点中的所有结点都调用一遍自身,例如叶子结点大于到根结点路径上所有结点的值,如果结点数为n,那么高度为$\lg n$,如果每次比较和交换的时间为$\Theta (1)$,那么总时间为$\Theta (\lg n)$。
6.3-1
5 3 17 10 84 19 6 22 9 ------------------ 5 3 17 22 84 19 6 10 9 ------------------ 5 3 19 22 84 17 6 10 9 ------------------ 5 84 19 22 3 17 6 10 9 ------------------ 84 22 19 10 3 17 6 5 9
6.3-2
因为调用Max-Heapify都假定当前结点的左右子树都是堆,只有从下往上调用才能满足这样的前提。
6.3-3
首先考虑h=0,即叶子结点的情况。假设堆的高度为H,当该堆是一个满二叉树时,显然有叶子结点的数量为$2^{H+1}-2^H=2^H=n/2=n/2^{h+1}$。当堆不是满二叉树时,叶子结点分布在深度为H和H-1的结点中。我们令x表示深度为H的结点数,那么n-x即为高度为H-1的满二叉树的结点数,所以n-x必为奇数。由此可见x和n中有一个奇数一个偶数。
- 当n是奇数x是偶数时,说明所有的非叶结点都有两个孩子,那么$n_{leaf} + n_{internal} = 2n_{internal} + 1$可得$n_{leaf} = n_{internal} + 1$,所以$n = n_{leaf} + n_{internal} = 2 n_{leaf} - 1 \Rightarrow n_{leaf} = (n+1)/2 = \lceil n/2 \rceil$
- 当n是偶数x是奇数时,我们为最右下的叶子结点添加一个兄弟使其变为第一种情况,这时可得$n_{leaf} + 1 = \lfloor (n+1)/2 \rfloor = \lceil n/2 \rceil + 1 \Rightarrow n_{leaf} = \lceil n/2 \rceil$
接下来我们假设当高度为h-1时命题成立,证明当高度为h时也成立。令$n_h$表示高度为h的结点的个数,$n_h'$表示去掉叶子结点后高度为h的结点的个数,由于去掉叶子结点后所有结点的高度减1,所以有$n_h = n_{h-1}'$。又由于叶子结点的数量前面已经求出为$\lceil n/2 \rceil$所以去掉后结点的数量变为$\lfloor n/2 \rfloor$,代入上式可得: $$n_h = n_{h-1}' \le \lceil \frac {n'}{2^h} \rceil = \lceil \frac {\lfloor n/2 \rfloor}{2^h} \rceil \le \lceil \frac {n/2}{2^h} \rceil = \lceil \frac n{2^{h+1}} \rceil$$ 于是我们利用数学归纳法得证。
6.4-1
竖线表示堆的末尾
5 13 2 25 7 17 20 8 4 建堆------------------ 25 13 20 8 7 17 2 5 4 | --------------------- 20 13 17 8 7 4 2 5 | 25 --------------------- 17 13 5 8 7 4 2 | 20 25 --------------------- 13 8 5 2 7 4 | 17 20 25 --------------------- 8 7 5 2 4 | 13 17 20 25 --------------------- 7 4 5 2 | 8 13 17 20 25 --------------------- 5 4 2 | 7 8 13 17 20 25 --------------------- 4 2 | 5 7 8 13 17 20 25 --------------------- 2 | 4 5 7 8 13 17 20 25
6.4-2
- Initialization: 开始时由于使用Build-Max-Heap把A[1..n]变成一个堆,而A[n+1,n]即空集显然包含0个A种的最大值,所以满足循环不变条件;
- Maintenance: 一遍循环开始前有A[1]为A[1..i]中的最大值,同时$A[1] \le A[i+1] \le A[i+2] \le \cdots A[n]$,这时交换A[1]和A[i]使得$A[i] \le A[i+1] \le A[i+2] \le \cdots A[n]$。之后将heap-size减1调用Max-Heapify,使得A[1..i-1]重新成为一个堆,这在i增1之后满足了下一遍循环的前提条件;
- Termination: 当循环终止时i=1,此时有$A[1] \le A[1+1] \le A[1+2] \le \cdots A[n]$,即序列有序。
6.4-3
- 当序列已经有序时,建堆的时候每次调用Max-Heapify都要进行最大次数的交换,根据6.3节所求的上界,时间为O(n)。之后排序的时间为$O(n \lg n)$,所以总时间为$O(n \lg n)$
- 当序列为递减时,建堆的时候每次调用Max-Heapify只进行一次交换,共n/2次,所以时间为O(n)。之后排序的时间为$O(n \lg n)$,所以总时间为$O(n \lg n)$
6.4-4
当排序阶段每次调用Max-Heapify时都进行最大次数的交换时,总的交换次数为:
$$T \ge \sum_{i=2}^{n} {\lfloor \lg n \rfloor} = \Theta(n \lg n) = \Omega (n \lg n)$$
6.4-5
堆排序的过程可以理解为堆中每个元素都不断地向堆顶移动,直到所有的元素都到达过堆顶的过程,该题可参见Sedgewick的论文The analysis of heapsort
6.5-1
15 13 9 5 12 8 7 4 0 6 2 1 --------------------- 1 13 9 5 12 8 7 4 0 6 2 --------------------- 13 1 9 5 12 8 7 4 0 6 2 --------------------- 13 12 9 5 1 8 7 4 0 6 2 --------------------- 13 12 9 5 6 8 7 4 0 1 2
6.5-2
15 13 9 5 12 8 7 4 0 6 2 1 10 --------------------- 15 13 9 5 12 10 7 4 0 6 2 1 8 --------------------- 15 13 10 5 12 9 7 4 0 6 2 1 8 --------------------- 15 13 10 5 12 9 7 4 0 6 2 1 8
6.5-3
Heap-Minimum(A) return A[1] Heap-Min(A) if A.heap-size == 0 error "heap underflow" min = A[1] A[1] = A[A.heap-size] A.heap-size = A.heap-size - 1 Min-Heapify(A, 1) return min Heap-Decrease-Key(A, i, key) if A[i] < key error "new key is bigger than original" A[i] = key while i > 1 and A[i] < A[Parent(i)] exchange A[i] A[Parent(i)] i = Parent(i) Heap-Insert(A, key) A.heap-size = A.heap-size + 1 A[A.heap-size] = INFINITE Heap-Decrease-Key(A, A.heap-size, key)
6.5-4
因为Heap-Increase-Key的前提条件是原来的key小于新的key,将原来的key设为负无穷就可以保证Heap-Increase-Key调用成功。
6.5-5
- Initialization: 在还没有增加之前A[1..A.heap-size]是一个大根堆,所以在A[i]增加为key之后只有可能A[i]和A[Parent(i)]之间不满足堆的性质;
- Maintenance: 假如一遍循环开始前满足循环条件,那么将A[i]和A[Parent(i)]之中大的那个放到A[Parent(i)]使得他们之间满足堆的性质,而有可能使得A[Parent(i)]和A[Parent(Parent(i))]违背堆的性质,当i变为Parent(i)之后满足了下一次循环的前提条件;
- Termination: 当循环终止时i=1或者A[i]<A[Parent(i)],当i=1时A[Parent(i)]不存在,而A[i]<A[Parent(i)]时不破坏堆的性质,所以整个A[1..A.heap-size]都满足堆的性质。
6.5-6
可以先向下移动小于他的祖先,直到没有小于他的祖先后放在空出的位置上
Heap-Increase-Key(A, i, key) if A[i] < key error "new key is smaller than original" while i > 1 and A[Parent(i)] < key A[i] = A[Parent(i)] i = Parent(i) A[i] = key
6.5-7
要使得队列先进先出就要使先进入元素的key大于后进入的key即可,最简单的方式就是第一个进入的key值最大,后面进入的key值比前面的少1。
反过来第一个进入的key值为0,之后进入的key值递增就能实现先进后出的栈。
6.5-8
相当于对A[i]为根的堆进行Extract-Max操作
Heap-Delete(A, i) A[i] = A[A.heap-size] A.heap-size = A.heap-size - 1 Heapify(A, i)
6.5-9
建一个大小为k的堆,堆中的每个元素代表一个List,元素的key为List当前最小元素的值,每次取出堆顶的元素,然后插入相应List中下一个元素的值,如果该List没有下一个元素就不插入,直到n个元素归并完毕。这里用类C++代码描述。
struct Node { int key; int list; }; class MinHeap { public: Node extract(); void insert(Node node); }; class List { public: int at(int index); void set(int index, int value); int length(); void reserve(int size); }; void merge(List* lists, int k, List* result) { MinHeap heap; int n = 0; int* A = new int[k]; for (int i = 0; i < k; i++) { Node node = { lists[i].at(0), i }; heap.insert(node); n += lists[i].length(); A[i] = 1; } result->reserve(n); for (int i = 0; i < n; i++) { Node node = heap.extract(); result->set(i, node.key); int j = node.list; if (A[j] < lists[j].length()) { node.key = lists[j].get(A[j]); A[j]++; heap.insert(node); } } delete[] A; }
6-1
a) 不一定能产生相同的堆,例如A={1,2,3,4,5},利用heapify产生的堆为
5 4 3 1 2
利用heap-insert产生的堆为
5 4 2 1 3
b) 当A初始时元素递增排列时,每次调用heap-insert都要将该元素移到堆顶,所以移动的次数 $$T(n)=\sum_{i=2}^{n}{\lfloor \lg i \rfloor} \le \sum_{i=2}^{n}{\lg i} = O(n \lg n)$$
6-2
a) 当用A[1]表示堆顶的时候,他的孩子下标范围是[2, d+1],A[2]的孩子的下标范围是[d+2, 2d+1],A[3]孩子的下标范围是[2d+2, 3d+1],由此可得A[i]孩子下标范围是[d(i-1)+2, di+1]。同理可求元素父亲的下标。用Parent(i)来求下标为i的元素的父亲,用Child(i,j)用来求下标为i的元素的第j个孩子
Parent(i) return (i - 2) / d + 1 Child(i, j) return d * (i - 1) + j + 1
b) 高度为H的堆节点数量为 $$\begin {align} n & = \sum_{h=0}^{H-1}{d^h} + j \qquad (1 \le j \le d^H) \\ & = \frac{d^H-1}{d-1} + j \\ \Rightarrow d^H & = (n-j)(d-1)+1 \\ \Rightarrow H & = O(\log_d n)) = O(\lg n) \end {align} $$
c)
Max-Heapify(A, i) max = i for j = 1 to d c = Child(i, j) if (c <= A.heap-size && A[c] > A[max]) max = c if i != max exchange A[i] with A[max] Max-Heapify(A, max) Extract-Max(A) max = A[1] A[1] = A[A.heap-size] A.heap-size = A.heap-size - 1 Max-Heapify(A, 1) return max
最坏情况下,换到A[1]位置的叶子元素需要移到堆底,共需$O(\log_d n)$次交换,每次交换需要d次比较,共$O(d \log_d n)$次比较。
d)
Increase(A, i, key) if A[i] > key error "new key should be lagger than original" A[i] = key while i > 1 && A[i] > A[Parent(i)] exchange A[i] with A[Parent(i)] i = Parent(i) Max-Insert(A, key) A.heap-size = A.heap-size + 1 A[A.heap-size] = -INFINITE Increase(A, A.heap-size, key)
最坏情况下,增大的元素需要换到堆顶,而且该元素处于堆底,共需$O(\log_d n)$次交换。
e) 见d)
6-3
a) 最简单的方法就是按照顺序从左到右从上到下依次填充数字即可,剩余的位置填上无穷。 $$\begin {pmatrix} 2 & 3 & 4 & 5 \\ 8 & 9 & 12 & 14 \\ 16 & \infty & \infty & \infty \\ \infty & \infty & \infty & \infty \end {pmatrix}$$
b) 如果Y[1,1]为无穷则其右边和下边的数都不能小于无穷,只有所有的元素都是无穷(即Y为空)才能满足;同理,如果Y[m,n]小于无穷则其左上元素都小于无穷,即Y全满。
c) 类似heapify的过程,只不过每次与右边和下边的元素进行比较。
Youngify(Y, i, j) i2 = i j2 = j if i + 1 <= Y.m && Y[i + 1, j] < Y[i, j] i2 = i + 1 if j + 1 <= Y.n && Y[i2, j + 1] < Y[i2, j] j2 = j + 1 if i != i2 or j != j2 exchange Y[i, j] with Y[i2, j2] Youngify(Y, i2, j2) Extract-Min(Y) min = Y[1, 1] Y[1, 1] = Y[Y.m, Y.n] Y[Y.m, Y.n] = INFINITE Youngify(Y, 1, 1) return min
最坏情况下左上的元素要移到右下,共m+n-2次交换,所以时间复杂度为O(m+n)
d) 类似于堆的insert,每次与上方和左方的元素比较
Decrease(Y, i, j, key) Y[i, j] = key i2 = i j2 = j if i > 1 && Y[i - 1, j] > Y[i, j] i2 = i - 1 if j > 1 && Y[i, j - 1] > Y[i2, j] j2 = j - 1 if i != i2 or j != j2 key = Y[i, j] Y[i, j] = Y[i2, j2] Decrease(Y, i2, j2, key) Insert(Y, key) Decrease(Y, Y.m, Y.n, key)
最坏情况下右下的元素要移到左上,共m+n-2次交换,所以时间复杂度为O(m+n)
e)
Sort(A) n = Sqrt(A.length) Y.m = n Y.n = n for i = 1 to n for j = 1 to n Y[i, j] = INFINITE for i = 1 to A.length Insert(Y, A[i]) for i = 1 to A.length A[i] = Extract-Min(Y)
第7行共执行$n^2$次,复杂度为$O(n^2)$,第9行共执行$n^2$次,复杂度为$O(n+n)O(n^2)=O(n^3)$,第11行共执行$n^2$次,复杂度为$O(n+n)O(n^2)=O(n^3)$,所以总时间复杂度为$O(n^3)$
f) 特别感谢@逍遥承影的更正
Contains(Y, key, i, j) if i > Y.m || j < 1 return false if Y[i, j] == key return true ii = i jj = j if i == Y.m || Y[i + 1, j] > key jj = jj + 1 if j == 1 || Y[i, j - 1] < key ii == ii + 1 return Contains(Y, key, ii, jj) Contains(Y, key) return Contains(Y, key, 1, Y.n)
最坏情况下需要从右上的元素检查到左下角的元素,每次向右或向下移动1,所以共需检查m+n次,时间复杂度为O(m+n)