算法导论课后习题解析 第六章
6.1-1
元素最少的情况是最底层只有一个叶子,即
6.1-2
设高度为h,那么可知
6.1-3
令p(i)为第i个元素的父亲下标,那么对任意
6.1-4
最小的元素处于最底层或次底层的叶子结点,即该结点没有子结点。这时因为大根堆(max heap)中父结点一定不小于子结点,所以最小的元素必须出现在叶子结点上。
6.1-5
对于有序的数列肯定有
6.1-6
树结构如下,元素7和6不满足大根堆的性质,故不是大根堆。
23 17 14 6 13 10 1 5 7 12
6.1-7
设堆的高度为h,那么除去最底层的结点的数量为
6.2-1
简单起见,我们只列出收影响的部分子树
3 10 1 8 9 0 --------- 10 3 1 8 9 0 --------- 10 9 1 8 3 0
6.2-2
只要把相应的最小元素移到父亲的位置即可
1
2
3
4
5
6
7
8
9
10
11
12
|
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
当
6.2-5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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,那么高度为
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,当该堆是一个满二叉树时,显然有叶子结点的数量为
- 当n是奇数x是偶数时,说明所有的非叶结点都有两个孩子,那么
nleaf+ninternal=2ninternal+1 nleaf+ninternal=2ninternal+1可得nleaf=ninternal+1 nleaf=ninternal+1,所以n=nleaf+ninternal=2nleaf−1⇒nleaf=(n+1)/2=⌈n/2⌉ n=nleaf+ninternal=2nleaf−1⇒nleaf=(n+1)/2=⌈n/2⌉ - 当n是偶数x是奇数时,我们为最右下的叶子结点添加一个兄弟使其变为第一种情况,这时可得
nleaf+1=⌊(n+1)/2⌋=⌈n/2⌉+1⇒nleaf=⌈n/2⌉ nleaf+1=⌊(n+1)/2⌋=⌈n/2⌉+1⇒nleaf=⌈n/2⌉
接下来我们假设当高度为h-1时命题成立,证明当高度为h时也成立。令
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]≤A[i+1]≤A[i+2]≤⋯A[n] A[1]≤A[i+1]≤A[i+2]≤⋯A[n],这时交换A[1]和A[i]使得A[i]≤A[i+1]≤A[i+2]≤⋯A[n] A[i]≤A[i+1]≤A[i+2]≤⋯A[n]。之后将heap-size减1调用Max-Heapify,使得A[1..i-1]重新成为一个堆,这在i增1之后满足了下一遍循环的前提条件; - Termination: 当循环终止时i=1,此时有
A[1]≤A[1+1]≤A[1+2]≤⋯A[n] A[1]≤A[1+1]≤A[1+2]≤⋯A[n],即序列有序。
6.4-3
- 当序列已经有序时,建堆的时候每次调用Max-Heapify都要进行最大次数的交换,根据6.3节所求的上界,时间为O(n)。之后排序的时间为
O(nlgn) O(nlgn),所以总时间为O(nlgn) O(nlgn) - 当序列为递减时,建堆的时候每次调用Max-Heapify只进行一次交换,共n/2次,所以时间为O(n)。之后排序的时间为
O(nlgn) O(nlgn),所以总时间为O(nlgn) O(nlgn)
6.4-4
当排序阶段每次调用Max-Heapify时都进行最大次数的交换时,总的交换次数为:
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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
可以先向下移动小于他的祖先,直到没有小于他的祖先后放在空出的位置上
1
2
3
4
5
6
7
|
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操作
1
2
3
4
|
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++代码描述。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
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都要将该元素移到堆顶,所以移动的次数
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个孩子
1
2
3
4
5
|
Parent(i)
return
(i - 2) / d + 1
Child(i, j)
return
d * (i - 1) + j + 1
|
b) 高度为H的堆节点数量为
c)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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]位置的叶子元素需要移到堆底,共需
d)
1
2
3
4
5
6
7
8
9
10
11
12
|
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)
|
最坏情况下,增大的元素需要换到堆顶,而且该元素处于堆底,共需
e) 见d)
6-3
a) 最简单的方法就是按照顺序从左到右从上到下依次填充数字即可,剩余的位置填上无穷。
b) 如果Y[1,1]为无穷则其右边和下边的数都不能小于无穷,只有所有的元素都是无穷(即Y为空)才能满足;同理,如果Y[m,n]小于无穷则其左上元素都小于无穷,即Y全满。
c) 类似heapify的过程,只不过每次与右边和下边的元素进行比较。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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,每次与上方和左方的元素比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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)
1
2
3
4
5
6
7
8
9
10
11
|
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行共执行
f) 特别感谢@逍遥承影的更正
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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)