日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

堆与堆排序(一)

發布時間:2025/3/15 编程问答 13 豆豆
生活随笔 收集整理的這篇文章主要介紹了 堆与堆排序(一) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

堆與堆排序(一)

上一篇博文 淺談優先隊列 介紹了什么是優先隊列,文末提到了一種數據結構——“堆”,基于“堆”實現的優先隊列,出隊和入隊的時間復雜度都為 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?

    如何構造一個堆

    針對給定的一列鍵值,如何構造一個堆呢?

    方法一:自底向上堆構造

    假設要構造一個大根堆,步驟如下:

  • 在初始化一棵包含 n 個節點的完全二叉樹時,按照給定的順序來放置鍵;
  • 按照下面的方法,對樹進行“堆化”
  • 從最后一個父母節點開始,到根為止,該算法檢查這些節點的鍵是否滿足父母優勢的要求。如果該節點不滿足,就把該節點的鍵 K 和它子女的最大鍵進行交換,然后再檢查在新的位置上,K 是否滿足父母優勢要求。這個過程一直繼續到對 K 的父母優勢要求滿足為止(最終它必須滿足,因為對每個葉子中的鍵來說,這條件是自動滿足的)。
  • 對于以當前父母節點為根的子樹,在完成它的“堆化”以后,對該節點的直接前趨(數組中此節點的前一個節點)進行同樣的操作。在對樹的根完成這種操作以后,該算法就停止了。
  • 如果該節點不滿足父母優勢,就把該節點的鍵 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

    方法二:自頂向下堆構造

    除了上面的算法,還有一種算法(效率較低)是通過把新的鍵連續插入預先構造好的堆,來構造一個新堆。有的人把它稱作自頂向下堆構造。

  • 首先,把一個鍵值為 K 的新節點附加在當前堆的最后一個葉子后面;
  • 然后,拿 K 和它父母的鍵做比較。如果 K 小于等于它的父母,那么算法停止;否則交換這兩個鍵,并把 K 和它的新父母做比較。
  • 重復2,一直持續到 K 不大于它的父母,或者 K 成為樹根為止。
  • 這種策略叫做上濾(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

    總結

    以上是生活随笔為你收集整理的堆与堆排序(一)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。