1. 程式人生 > 其它 >堆以及堆排序

堆以及堆排序

技術標籤:演算法模板演算法堆排序樹堆java

堆是一棵完全二叉樹。

小根堆:每個父節點的值,都小於等於其子節點的值。因此,根節點的值為集合的最小值。

大根堆:每個父節點的值,都大於等於其子節點的值。因此,根節點的值為集合的最大值。

使用一維陣列來儲存堆。根節點的值存放在陣列中索引值為1的位置。由於完全二叉樹的特性,若父節點在陣列的索引為 x x x,則其左子節點的索引為 2 x 2x 2x,右子節點的索引為 2 x + 1 2x+1 2x+1。(凡是完全二叉樹,都是用一維陣列來儲存的)

堆最核心的是up操作down操作,使用這兩個操作可完成以下堆(小根堆)主要支援的函式:

1)插入一個數insert(int x)

heap[++ size] = x;
up(size);

2)求集合當中的最小值getMin()

heap[1];

3)刪除最小值deleteMin()

heap[1] = heap[size];
size --;
down(1);

4)刪除任意一個元素deleteMin(int k)

heap[k] = heap[size];
size --;
down(k);
up(k);

5)修改任意一個元素modify(int k, int x)

heap[k] = x;
down(k);
up(k);

up操作的程式碼:

public void up(int u){
    while(
u / 2 >= 1 && heap[u / 2] > heap[u]){ int tmp = heap[u / 2]; heap[u / 2] = heap[u]; heap[u] = tmp; u /= 2; } }

down操作的程式碼:

public void down(int u){
    int t = u;
    if(2 * u <= size && heap[2 * u] < heap[t]) t = 2 * u;
    if(2 * u + 1 <=
size && heap[2 * u + 1] < heap[t]) t = 2 * u + 1; if(t != u){ int tmp = heap[u]; heap[u] = heap[t]; heap[t] = tmp; down(t); } }

堆排序

可以使用堆來實現堆排序。堆排序的思路是:先根據待排序陣列建堆(小根堆),然後依次從堆中彈出最小值,也就是將堆的根節點刪除,彈出的元素即構成為從小到大排序的陣列。

1、建堆

建堆的直觀想法是,依次將陣列中的元素插入到堆中,這當然是沒啥問題的,但這種做法的時間複雜度是 O ( n l o g n ) O(nlogn) O(nlogn)推薦的做法是:將陣列元素放入到堆(heap陣列)裡後,對heap陣列中索引值從size/2直到0,進行down操作。注意,對heap陣列中索引值從0到size/2進行down操作是不正確的,這會導致排序出錯。

首先,上述的推薦做法是正確的,因為:①堆中只有一半的節點是存在子節點的,剩下的一半節點則沒有,對這些沒有子節點的節點不進行處理當然沒有問題;②按照size/2到0的順序進行down操作,可保證每個節點滿足小根堆的要求,遞迴的處理每個節點後,較大的節點值是”下沉‘的,留在上面的都是較小的節點值,如果down操作順序反過來,較大的節點值可能因為滿足區域性最小而被調整到根節點,且後面的down操作也不會再訪問該節點,從而導致錯誤。其次,推薦做法的時間複雜度為 O ( n ) O(n) O(n),證明如下:

如圖所示,建堆的時間複雜度可寫為: n 4 ∗ 1 + n 8 ∗ 2 + n 16 ∗ 3 + ⋯ + n 2 n ∗ ( n − 1 ) = n ∗ ( 1 2 2 + 2 2 3 + 3 2 4 + ⋯ + n − 1 2 n ) (1) \frac{n}{4}*1 + \frac{n}{8}*2 + \frac{n}{16}*3 + \cdots + \frac{n}{2^n}*(n - 1)=n*(\frac{1}{2^2}+\frac{2}{2^3}+\frac{3}{2^4}+\cdots+\frac{n-1}{2^n}) \tag{1} 4n1+8n2+16n3++2nn(n1)=n(221+232+243++2nn1)(1)

S = 1 2 2 + 2 2 3 + 3 2 4 + ⋯ + n − 2 2 n − 1 + n − 1 2 n (2) S=\frac{1}{2^2}+\frac{2}{2^3}+\frac{3}{2^4}+\cdots+\frac{n-2}{2^{n-1}}+\frac{n-1}{2^n}\tag{2} S=221+232+243++2n1n2+2nn1(2)

則有

2 ∗ S = 1 2 + 2 2 2 + 3 2 3 + 4 2 4 + ⋯ + n − 1 2 n − 1 (3) 2*S=\frac{1}{2}+\frac{2}{2^2}+\frac{3}{2^3}+\frac{4}{2^4}+\cdots+\frac{n-1}{2^{n-1}}\tag{3} 2S=21+222+233+244++2n1n1(3)

式(2)和式(3)錯位相減,可得到:

S = 1 2 − n − 1 2 n + ( 1 2 2 + 1 2 3 + ⋯ + 1 2 n − 1 ) (4) S=\frac{1}{2}-\frac{n-1}{2^n}+(\frac{1}{2^2}+\frac{1}{2^3}+\cdots+\frac{1}{2^{n-1}})\tag{4} S=212nn1+(221+231++2n11)(4)

簡化後,為: S = 1 − n + 1 2 n ≈ 1 S=1-\frac{n+1}{2^n}\approx1 S=12nn+11。因此,式(1)所示的時間複雜度為 O ( n ) O(n) O(n)

2、從堆中彈出最小值

此操作即對應上述的==deleteMin()==函式。

堆排序的整體程式碼為:

int[] heap;
int size;

private void down(int u){
    int t = u;
    if(2 * u <= size && heap[2 * u] < heap[t]) t = 2 * u;
    if(2 * u + 1 <= size && heap[2 * u + 1] < heap[t]) t = 2 * u + 1;
    if(t != u){
        int tmp = heap[u];
        heap[u] = heap[t];
        heap[t] = tmp;
        down(t);
    }
}

public static int deleteMin(){
    int min = heap[1];
    heap[1] = heap[size --];
    down(1);
    return min;
}

public void heap_sort(int[] a){
    int n = a.length;
    heap = new int[n + 10];
    size = n;
    
    // 建堆
    for(int i = 0; i < n; i ++) heap[i + 1] = a[i];
    for(int i = size >> 1; i >= 0; i --) down(i);
    
    // 彈出堆的最小值,並打印出來,打印出來的結果為陣列a從小到大的排序結果
    for(int i = 0; i < size; i ++) System.out.print(deleteMin() + " ");
}