堆以及堆排序
堆
堆是一棵完全二叉樹。
小根堆:每個父節點的值,都小於等於其子節點的值。因此,根節點的值為集合的最小值。
大根堆:每個父節點的值,都大於等於其子節點的值。因此,根節點的值為集合的最大值。
使用一維陣列來儲存堆。根節點的值存放在陣列中索引值為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} 4n∗1+8n∗2+16n∗3+⋯+2nn∗(n−1)=n∗(221+232+243+⋯+2nn−1)(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+⋯+2n−1n−2+2nn−1(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} 2∗S=21+222+233+244+⋯+2n−1n−1(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=21−2nn−1+(221+231+⋯+2n−11)(4)
簡化後,為: S = 1 − n + 1 2 n ≈ 1 S=1-\frac{n+1}{2^n}\approx1 S=1−2nn+1≈1。因此,式(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() + " ");
}