算法導論課后習題解析 第六章


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)



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM