堆与堆排序(一)
堆與堆排序(一)
上一篇博文 淺談優先隊列 介紹了什么是優先隊列,文末提到了一種數據結構——“堆”,基于“堆”實現的優先隊列,出隊和入隊的時間復雜度都為 O(logN).
這篇博文我們就走進“堆”,看看它到底是什么結構。
此堆非彼堆
值得注意的是,這里的“堆”不是內存管理中提到的“堆棧”的“堆”。前者的“堆”——準確地說是二叉堆,是一種類似于完全二叉樹的數據結構;后者的“堆”是一種類似于鏈表的數據結構。
堆的結構性質
二叉堆在邏輯結構上是一棵完全二叉樹。什么是完全二叉樹呢?即樹的每一層都是滿的,除了最后一層最右邊的元素有可能缺位。
如下圖所示,打錯號的兩個不是完全二叉樹,其他都是。
對于一個有 N 個節點的完全二叉樹,我們可以為它的每個節點指定一個索引,方法是從上至下,從左到右,從1開始連續編號,如下圖黑色數字所示。了解二叉樹的朋友一定看出來了,這就是二叉樹的層序遍歷。
可以看出,對于一個有 N 個節點的完全二叉樹,索引值和元素是一一對應的。所以完全二叉樹可以用一個數組來表示而不需要指針:索引值就是數組的下標,元素的值就是節點的關鍵字。
如下圖,是一個完全二叉樹和數組的相互關系。
如果你繼續觀察,就會發現另一個規律:對于數組任一位置 i 上的元素,其左兒子在位置 2i 上,右兒子在2i+1上,它的父親則在位置 ?i/2??i/2?上。
以節點 D 為例,D 的下標是 4.
- B是它的父節點,B的下標是2(=4/2),如圖中黑色的線;
- H是它的左孩子,H的下標是8(=4*2),如圖中藍色的線;
- I是它的右孩子,I的下標是9(=4*2+1),如圖中紅色的線;
堆序性質
二叉堆一般分為兩種:最大堆和最小堆。
最大堆:也叫做大根堆。每一個節點的值(或者說關鍵字)都要大于或等于它孩子的值(對于任何葉子我們認為這個條件都是自動滿足的)。下圖就是一個最大堆。
最小堆:也叫做小根堆。每一個節點的值(或者說關鍵字)都要小于或等于它孩子的值(對于任何葉子我們認為這個條件都是自動滿足的)。下圖就是一個最小堆。
值得注意的是:以大根堆為例,在任何從根到某個葉子的路徑上,鍵值的序列是遞減的(如果允許相等的鍵存在,則是非遞增的)。然而,鍵值之間并不存在從左到右的次序。也就是說,在樹的同一層節點之間,不存在任何關系,更一般地來說,在同一節點的左右子樹之間也沒有任何關系。
堆的重要特性
以大根堆為例,把堆的重要特性總結如下。
只存在一棵 n 個節點的完全二叉樹。它的高度等于?log2n??log2?n?
堆的根總是包含了堆的最大元素
堆的一個節點以及該節點的子孫也是一個堆
可以用數組來實現堆,方法是用從上到下、從左到右的方式來記錄堆的元素。為了方便起見,可以在這種數組從 1 到 n 的位置上存放堆的元素,留下H[0],要么讓它空著,要么在其中放一個限位器,它的值大于堆中任何一個元素。
在 4 的表示法中:
1) 父母節點的鍵將會位于數組的前?n/2??n/2?個位置中,而葉子節點的鍵將會占據后?n/2??n/2?個位置。
2) 在數組中,對于一個位于父母位置 i 的鍵來說,它的子女將會位于2i和2i+1. 相應地,對于一個位于i的鍵來說,它的父母將會位于?i/2??i/2?
對于上面提到的父母節點的鍵將會位于數組的前?n/2??n/2?個位置中,而葉子節點的鍵將會占據后?n/2??n/2?個位置。這一點我覺得很有意思,咱們不嚴格證明,僅簡單分析一下為什么會這樣。
設一個堆共有N個元素。判斷一個索引為 i 的節點是不是父母節點,可以看它有沒有孩子。如果它有孩子,那么2i一定小于等于N,換句話說,如果2i大于N,則可以斷定它是葉子節點,在它位置之后的節點(如果有的話)也一定是葉子節點,因為從 2i > N 可以推出 2(i+1) > N,2(i+2) > N,…
所以,只要求解不等式 2i > N, 取i的最小值,就得到第一個葉子節點的位置。
經過演算,i 的最小值是 i=?N/2?+1i=?N/2?+1,所以,最后一個父母節點的位置是 ?n/2??n/2?
如何構造一個堆
針對給定的一列鍵值,如何構造一個堆呢?
方法一:自底向上堆構造
假設要構造一個大根堆,步驟如下:
如果該節點不滿足父母優勢,就把該節點的鍵 K 和它子女的最大鍵進行交換,然后再檢查在新的位置上,K 是否滿足父母優勢要求。這個過程一直繼續到對 K 的父母優勢要求滿足為止——這種策略叫做下濾(percolate down)。
假設有一列鍵(共10個):4,1,3,2,16,9,10,14,8,7
那么,按照上面給定的鍵值順序,對應的完全二叉樹如下圖。
最后一個父母節點是5(=10/2),我們從5號節點開始對這個二叉樹進行堆化。
看完這些圖,相信你已經知道如何構建大根堆了。下面就用C語言來實現。
遞歸解法
根據上文的算法描述,很容易想到用遞歸來實現。我們先設計一個函數——下濾函數。
先寫幾個宏。給定一個位置為 i 的節點,很容易算出它的左右孩子的位置和父母的位置。
#define LEFT(i) (2*i) // i 的左孩子 #define RIGHT(i) (2*i+1) // i 的右孩子 #define PARENT(i) (i/2) // i 的父節點假定以 LEFT(t) 和 RIGHT(t) 為根的子樹都已經是大根堆,下面的函數調整以 t 為根的子樹,使之成為大根堆。
// 下濾函數(遞歸解法) // 假定以 LEFT(t) 和 RIGHT(t) 為根的子樹都已經是大根堆 // 調整以 t 為根的子樹,使之成為大根堆。 // 節點位置為 1~n,a[0]不使用 void percolate_down_recursive(int a[], int n, int t) { #ifdef PRINT_PROCEDUREprintf("check %d\n", t); #endifint left = LEFT(t);int right = RIGHT(t); int max = t; //假設當前節點的鍵值最大if(left <= n) // 說明t有左孩子 {max = a[left] > a[max] ? left : max;}if(right <= n) // 說明t有右孩子 {max = a[right] > a[max] ? right : max;}if(max != t){ swap(a + max, a + t); // 交換t和它的某個孩子,即t下移一層 #ifdef PRINT_PROCEDUREprintf("%d NOT satisfied, swap it and %d \n",t, max); #endifpercolate_down_recursive(a, n, max); // 遞歸,繼續考察t} }//交換*a和*b, 內部函數 static void swap(int* a, int* b) {int temp = *a;*a = *b;*b = temp; }有了上面的函數,我們就可以從最后一個父母節點開始,到根為止,逐個進行“下濾”。
非遞歸解法
以上代碼是用“交換法”(第26行)對節點進行下濾。一次交換需要3條賦值語句,有沒有更好的寫法呢?有,就是“空穴法”(我自己起的名字)。我們先說明空穴法的原理,然后附上代碼。
以上圖中“檢查1號節點,不滿足”這個地方開始,對1號節點進行下濾。
// 非遞歸且不用交換 void percolate_down_no_swap(int a[], int n, int t) {int key = a[t]; // 用key記錄鍵值int max_idx;int heap_ok = 0; // 初始條件是父母優勢不滿足 #ifdef PRINT_PROCEDURE printf("check %d\n", t); #endif // LEFT(t) <= n 成立則說明 t 有孩子while(!heap_ok && (LEFT(t) <= n)){ max_idx = LEFT(t); // 假設左右孩子中,左孩子鍵值較大if(LEFT(t) < n) // 條件成立則說明有2個孩子{if(a[LEFT(t)] < a[RIGHT(t)])max_idx = RIGHT(t); //說明右孩子的鍵值比左孩子大}//此時max_idx指向鍵值較大的孩子if(key >= a[max_idx]){heap_ok = 1; //為 key 找到了合適的位置,跳出循環}else{ a[t] = a[max_idx]; //孩子上移一層,max_idx 被空出來,成為空穴 #ifdef PRINT_PROCEDURE printf("use %d fill %d \n", max_idx, t);printf("%d is empty\n", max_idx); #endif t = max_idx; //令 t 指向空穴 } }a[t] = key; // 把 key 填入空穴 #ifdef PRINT_PROCEDURE printf("use value %d fill %d \n", key, t);#endif return; }如果在編譯的時候定義宏PRINT_PROCEDURE,則可以看到堆化過程和上文的六張圖相符。假設源文件名是 max_heap.c,在編譯的時候用-D宏名稱可以定義宏。
gcc max_heap.c -DPRINT_PROCEDURE方法二:自頂向下堆構造
除了上面的算法,還有一種算法(效率較低)是通過把新的鍵連續插入預先構造好的堆,來構造一個新堆。有的人把它稱作自頂向下堆構造。
這種策略叫做上濾(percolate up)。
依然以4,1,3,2,16,9,10,14,8,7這列鍵為例,用圖說明上濾的過程。
細心的讀者應該已經看出來了:下濾法構造的堆,其對應的數組是
[16,14,10,8,7,9,3,2,4,1]而上濾法構造的堆,其數組是
[16,14,10,8,7,3,9,1,4,2]所以得出結論:對于同一列鍵,用下濾法和上濾法構造出來的堆,不一定完全相同。
囿于篇幅,“堆”就說到這里,上濾法的代碼,咱們下次說。
參考資料
https://blog.csdn.net/guoweimelon/article/details/50904346
總結
- 上一篇: xpath安装与下载
- 下一篇: by截取字段 group_深入理解 gr