成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

搞懂二叉堆的那些事

開發(fā) 前端
我們?cè)谌粘I钪校ǔ?huì)說“一堆東西”或者“堆東西”,這里的“堆”,通常指重疊放置的許多東西。

1. 什么是二叉堆?

“二叉”自不必多說,本章主要介紹的樹都是二叉樹。那么啥是“堆”呢?

我們?cè)谌粘I钪校ǔ?huì)說“一堆東西”或者“堆東西”,這里的“堆”,通常指重疊放置的許多東西。

 [[397348]]

一堆東西

我們?cè)诙褨|西的時(shí)候,肯定都有一個(gè)經(jīng)驗(yàn),即:為了使這堆東西更穩(wěn)定,會(huì)將比較重的、大的東西放在下面,比較輕的、小的東西放在上面。

這個(gè)經(jīng)驗(yàn)放在數(shù)據(jù)結(jié)構(gòu)——二叉樹中,同樣適用。只不過“重”“大”是根據(jù)結(jié)點(diǎn)值的大小來判斷的,并且是在雙親結(jié)點(diǎn)和孩子結(jié)點(diǎn)之間進(jìn)行比較的

比如,結(jié)點(diǎn)值大的,作為孩子結(jié)點(diǎn);結(jié)點(diǎn)值小的,作為雙親結(jié)點(diǎn)。

下面舉一個(gè)例子,先看下面一顆普通二叉樹,也是一顆完全二叉樹:

再看下面一顆二叉堆:

最小堆

這個(gè)二叉堆的特點(diǎn)是:

  • 它是一顆完全二叉樹。事實(shí)上,該二叉堆就是由上圖的完全二叉樹經(jīng)過調(diào)整轉(zhuǎn)化而來;
  • 任何一個(gè)雙親結(jié)點(diǎn)的值,均小于或等于左孩子和右孩子的值;
  • 每條分支從根結(jié)點(diǎn)開始都是升序排序(如分支 1-2-3-4)。

這樣的二叉堆被稱為最小堆,它的堆頂,即根結(jié)點(diǎn) A,是整棵樹的最小值。

與最小堆相對(duì)應(yīng)的是最大堆:

  • 最大堆是一顆完全二叉樹;
  • 它的任何一個(gè)雙親結(jié)點(diǎn)的值,均大于或等于左孩子和右孩子的值;
  • 每條分支從根結(jié)點(diǎn)開始都是降序排序。

最大堆的堆頂,是整棵樹的最大值。

我們將上圖中的普通二叉樹轉(zhuǎn)化為最大堆,如下圖:

最大堆

2. 二叉堆的操作

2.1. 構(gòu)造二叉堆

給你一顆完全二叉樹,如何調(diào)整結(jié)點(diǎn),構(gòu)造出一個(gè)二叉堆?下面是一顆無序的完全二叉樹:

現(xiàn)在我們想要構(gòu)造出一個(gè)最小堆,首先找到這顆完全二叉樹中所有的非葉子結(jié)點(diǎn)(綠色標(biāo)記):

我們要做的事是:對(duì)每個(gè)非葉子結(jié)點(diǎn),做最小堆的“下沉”調(diào)整。

何謂最小堆的“下沉”調(diào)整?

對(duì)某個(gè)非葉子結(jié)點(diǎn),如果該結(jié)點(diǎn)大于其孩子結(jié)點(diǎn)中最小的那個(gè),則交換二者位置,否則不用交換。在圖上則表現(xiàn)出非葉子結(jié)點(diǎn)(即大值結(jié)點(diǎn))“下沉”一個(gè)層次。運(yùn)動(dòng)是相對(duì)的,大值結(jié)點(diǎn)“下沉”,就相當(dāng)于小值結(jié)點(diǎn)“上浮”。

需要注意的是,有時(shí)下沉一次是不夠的,我們需要下沉多次,確保該結(jié)點(diǎn)下沉到底(即它不再大于其孩子)。

所有非葉子結(jié)點(diǎn),從最后一個(gè)開始,按照從右到左,從下到上的順序進(jìn)行多次最小堆的下沉調(diào)整,即可構(gòu)造成最小堆。

比如對(duì)于值為 4 的非葉子結(jié)點(diǎn)而言,它下沉到第 3 層次后,仍然大于其孩子,這不算“下沉到底”,還需要繼續(xù)下沉到第 4 層次。至此,在分支 2-4-3-1 上,“大值”結(jié)點(diǎn) 4 算是下沉到底了。

下面進(jìn)行分步解釋:

1.對(duì)非葉子結(jié)點(diǎn) 7,它小于其孩子結(jié)點(diǎn) 10, 不用“下沉”;

2.對(duì)非葉子結(jié)點(diǎn) 3,它大于其孩子結(jié)點(diǎn)中較大的結(jié)點(diǎn) 1,結(jié)點(diǎn) 3 要“下沉”,和結(jié)點(diǎn) 1 交換。顯然,結(jié)點(diǎn) 3 沉到底了。

 

3.對(duì)非葉子結(jié)點(diǎn) 6,它大于其孩子結(jié)點(diǎn)中較小的結(jié)點(diǎn) 5,結(jié)點(diǎn) 6 要“下沉”, 和結(jié)點(diǎn) 5 交換位置。顯然,結(jié)點(diǎn) 6 沉到底了。

4.對(duì)非葉子結(jié)點(diǎn) 4,它大于其孩子結(jié)點(diǎn)中最小的結(jié)點(diǎn) 1,結(jié)點(diǎn) 4 要 “下沉”,和結(jié)點(diǎn) 1 交換位置。顯然,結(jié)點(diǎn) 4 并未沉到底。

5.仍對(duì)結(jié)點(diǎn) 4,它大于其孩子結(jié)點(diǎn)中最小的結(jié)點(diǎn) 3,結(jié)點(diǎn) 4 要“下沉”, 和結(jié)點(diǎn) 3 交換位置。此時(shí),結(jié)點(diǎn) 4 算是沉底了。

6.對(duì)非葉子結(jié)點(diǎn) 2,它大于其孩子結(jié)點(diǎn)中最小的結(jié)點(diǎn) 1,結(jié)點(diǎn) 2 要“下沉”,和結(jié)點(diǎn) 1 交換位置。顯然,結(jié)點(diǎn) 2 算是沉到底了。

至此,我們將一顆無序的完全二叉樹調(diào)整改造成了最小二叉堆,你可以檢查一下,最小堆中的所有結(jié)點(diǎn)皆滿足雙親的值小于孩子的值。并且,5 條分支上都是有序的。

構(gòu)造最大堆的步驟類似,不過最大堆的下沉調(diào)整是:如果某結(jié)點(diǎn)小于其孩子結(jié)點(diǎn)中最大的那個(gè),則交換二者位置,在圖上表現(xiàn)為非葉子結(jié)點(diǎn)(即小值結(jié)點(diǎn))“下沉”一個(gè)層次。通過多次下沉調(diào)整,使該結(jié)點(diǎn)不再小于其孩子。

下圖把一個(gè)無序完全二叉樹調(diào)成為最大堆:

2.2. 插入結(jié)點(diǎn)

二叉堆是一個(gè)完全二叉樹,要向其中插入結(jié)點(diǎn),插入到完全二叉樹的最后一個(gè)結(jié)點(diǎn)的下一個(gè)位置即可。

比如向下面的一個(gè)最大堆中插入結(jié)點(diǎn) 11,要插到最后一個(gè)結(jié)點(diǎn) 4 的下一個(gè)位置。當(dāng)最大堆新插入一個(gè)結(jié)點(diǎn) 11 時(shí),它就不再是最大堆了,因?yàn)榻Y(jié)點(diǎn) 11 破壞了原堆的結(jié)構(gòu)。所以,我們應(yīng)當(dāng)將其看作一個(gè)新的完全二叉樹,然后調(diào)整新完全二叉樹再次構(gòu)造出最大堆。(調(diào)整過程見上)

插入過程

2.3. 刪除結(jié)點(diǎn)

刪除操作與插入操作相反,是刪除第一個(gè)位置的元素,即刪除堆頂。

我們以刪除上圖最大堆的堆頂 11 為例。

當(dāng)刪除堆頂 11 后,二叉堆原結(jié)構(gòu)被破壞,甚至不是一顆二叉樹了(變成兩顆):

為了保持完全二叉樹的形態(tài),我們把最后一個(gè)結(jié)點(diǎn) 7 補(bǔ)到根結(jié)點(diǎn)去,頂替被刪除的根結(jié)點(diǎn) 11。如此一來,我們又得到了一個(gè)新完全二叉樹(不是二叉堆),然后我們根據(jù)這顆新完全二叉樹再次構(gòu)造出最大堆即可。

刪除過程

3. 二叉堆的存儲(chǔ)結(jié)構(gòu)

二叉堆的存儲(chǔ)結(jié)構(gòu)是順序存儲(chǔ),因?yàn)槎娑咽且活w完全二叉樹,在文章【二叉樹的存儲(chǔ)】中我們說過:完全二叉樹適合使用順序存儲(chǔ)結(jié)構(gòu)來實(shí)現(xiàn)。

下圖是一個(gè)最大堆,紅色方框是對(duì)結(jié)點(diǎn)的編號(hào),和數(shù)組下標(biāo)一一對(duì)應(yīng)。

 

二叉堆的順序存儲(chǔ)

鏈?zhǔn)酱鎯?chǔ)結(jié)構(gòu)能夠清晰而形象地為我們展現(xiàn)出二叉堆中雙親結(jié)點(diǎn)和左右孩子的關(guān)系。但是數(shù)組中沒有指針,只有數(shù)組下標(biāo),怎么表示雙親和孩子的關(guān)系呢?

其實(shí)對(duì)于完全二叉樹來說,數(shù)組下標(biāo)足矣!

現(xiàn)假設(shè)二叉堆中雙親結(jié)點(diǎn)的數(shù)組下標(biāo)為 parent_index,左孩子的數(shù)組下標(biāo)為 left_child_index,右孩子的數(shù)組下標(biāo)為 right_child_index,那么它們之間有如下關(guān)系:

(一)left_child_index = 2 × parent_index + 1

(二)right_child_index = 2 × parent_index + 2

(三)parent_index = (left_child_index - 1) ÷ 2

(四)parent_index = (right_child_index - 2) ÷ 2

(五)right_child_index = left_child_index + 1

比如:結(jié)點(diǎn) 3 的下標(biāo)為 3 ,則其左孩子 2 的下標(biāo)為 2 × 3 + 1 = 7、右孩子 1 的下標(biāo)為 2 × 3 + 2 = 8;

結(jié)點(diǎn) 3 的下標(biāo)為 3,作為左孩子,其雙親下標(biāo)為 (3 - 1) ÷ 2 = 1;結(jié)點(diǎn) 7 的下標(biāo)為 4,作為右孩子,其雙親下標(biāo)為 (4 - 2) ÷ 2 = 1;

假設(shè)某結(jié)點(diǎn)的數(shù)組下標(biāo)為 child_index,你不知道該結(jié)點(diǎn)是左孩子還是右孩子,要求其雙親的下標(biāo),有

(六)parent_index = (child_index - 1) ÷ 2

比如:你不知道結(jié)點(diǎn) 5(下標(biāo)為 5)、結(jié)點(diǎn) 6(下標(biāo)為 6)是左孩子還是右孩子,則結(jié)點(diǎn) 5 和結(jié)點(diǎn) 6 的雙親下標(biāo)分別為 (5 - 1) ÷ 2 = 2 、(6 - 1) ÷ 2 = 2。(注意,編程語言中的整型運(yùn)算,所以結(jié)果不是小數(shù))

這里,我們使用結(jié)構(gòu)體實(shí)現(xiàn)二叉堆:

  1. #define MAXSIZE 20 // 數(shù)組的最大存儲(chǔ)空間 
  2.  
  3. typedef struct { 
  4.     int array[MAXSIZE]; // 存儲(chǔ)數(shù)組 
  5.     int length; // 當(dāng)前堆長度(結(jié)點(diǎn)數(shù)) 
  6. } BinaryHeap; 

在進(jìn)行實(shí)際操作之前,需要初始化二叉堆,即對(duì)數(shù)組及堆長度賦值:

  1. /** 
  2.  * @description: 初始化二叉堆 
  3.  * @param {BinaryHeap} *heap 二叉堆 
  4.  * @param {int} *array 數(shù)組首地址,該數(shù)組是一個(gè)無序完全二叉樹 
  5.  * @param {int} arr_length 數(shù)組長度 
  6.  * @return {*} 無 
  7.  */ 
  8. void init_heap(BinaryHeap *heap, int *array, int arr_length) 
  9.     // array 拷貝到 heap 中 
  10.     memcpy(heap->array, array, arr_length * sizeof(int)); 
  11.     // 設(shè)置堆長度 
  12.     heap->length = arr_length; 

4. 二叉堆的具體實(shí)現(xiàn)

4.1. 調(diào)整和構(gòu)造

這里以構(gòu)造最小堆為例。

要構(gòu)造一個(gè)最小堆,就得調(diào)整所有的非葉子結(jié)點(diǎn)。而調(diào)整的依據(jù)就是比較非葉子結(jié)點(diǎn)和其孩子的大小。

我們約定 parent 為非葉子結(jié)點(diǎn), parent_index 為其下標(biāo)。child 為其孩子中較小的那個(gè),child_index為其下標(biāo)。

child 開始默認(rèn)標(biāo)識(shí)左孩子,那么右孩子的下標(biāo)即為 child_index + 1。當(dāng)左孩子小于等于右孩子時(shí),child 不需要改變;當(dāng)左孩子大于右孩子時(shí),就得更新 child_index ,使child 標(biāo)識(shí)右孩子。

下面結(jié)合下圖中值為 4 的非葉子結(jié)點(diǎn)為例,講述代碼如何實(shí)現(xiàn)。

先比較 parent 的左右孩子,左孩子較小,則 child 為左孩子,不需要更新 child_index。

parent 和 child 各就各位,發(fā)現(xiàn) parent 大于 child,可以交換位置。在交換之前,先保存一下 parent 的值,即 parent_value = 4:

交換位置:先把 child的值賦給 parent,從而達(dá)到 值1 上浮的效果:

 


然后更新 parent_index 和 child_index,二者都往下走一層次:

然后將之前保存的 value 賦給現(xiàn)在的 parent,從而達(dá)到 值4 下沉的效果:

一次調(diào)整完成,但對(duì)于 值4 來說,并沒有結(jié)束,因?yàn)?值4 還沒有沉到底。

比較此時(shí) parent 的左右孩子,發(fā)現(xiàn)右孩子較小,則 child 為右子樹,需要更新 child_index,使 child 標(biāo)識(shí)右孩子:

現(xiàn)在可以交換位置了,把 child 的值賦給 parent,達(dá)到 值3 的上浮:

 


然后,更新 parent_index 和 child_index 的值,二者向下走一個(gè)層次:

把 value 賦給 parent,達(dá)到 值4 的下沉:

 

此時(shí),child_index 已超過了二叉堆的長度,即 值4 已經(jīng)到底了。

調(diào)整代碼如下:

  1. /** 
  2.  * @description: 針對(duì)某個(gè)非葉子結(jié)點(diǎn)進(jìn)行到底的下沉調(diào)整 
  3.  * @param {BinaryHeap} *heap 二叉堆(無序) 
  4.  * @param {int} parent_index 某個(gè)非葉子結(jié)點(diǎn) 
  5.  * @return {*} 無 
  6.  */ 
  7. void adjust_for_min_heap(BinaryHeap *heap, int parent_index) 
  8.     // value 保存非葉子結(jié)點(diǎn)的值 
  9.     int value = heap->array[parent_index]; 
  10.     // child_index 標(biāo)識(shí)左孩子 
  11.     int child_index = parent_index * 2 + 1; 
  12.     // 最后一個(gè)結(jié)點(diǎn)的下標(biāo) 
  13.     int last_child_index = heap->length - 1; 
  14.  
  15.     // 雙親結(jié)點(diǎn) parent 至少有一個(gè)孩子 
  16.     while (child_index <= last_child_index) { 
  17.         // 如果雙親結(jié)點(diǎn) parent 有左孩子和右孩子 
  18.         if (child_index < last_child_index) { 
  19.             // 比較左孩子和右孩子誰小,如果右孩子小, 
  20.             if (heap->array[child_index] > heap->array[child_index + 1]) { 
  21.                 // 則 child_index 標(biāo)識(shí)右孩子 
  22.                 child_index = child_index + 1; 
  23.             } 
  24.         } 
  25.         // 如果雙親的值大于 child 的值 
  26.         if (value > heap->array[child_index]) { 
  27.             heap->array[parent_index] = heap->array[child_index]; // 小節(jié)點(diǎn)上浮 
  28.             parent_index = child_index; // 更新雙親下標(biāo) 
  29.             child_index = parent_index * 2 + 1; // 更新孩子下標(biāo) 
  30.         } else { // 不做操作,跳出循環(huán) 
  31.             break; 
  32.         } 
  33.         // 大節(jié)點(diǎn)下沉 
  34.         heap->array[parent_index] = value; 
  35.     } 

構(gòu)造代碼如下:

  1. /** 
  2.  * @description: 構(gòu)造最小堆 
  3.  * @param {BinaryHeap} *heap 二叉堆(無序) 
  4.  * @return {*} 無 
  5.  */ 
  6. void create_min_heap(BinaryHeap *heap) 
  7.     // 每個(gè)非葉子結(jié)點(diǎn)都調(diào)整 
  8.     for (int i = (heap->length - 2) / 2; i >= 0; i--) { 
  9.         adjust_for_min_heap(heap, i); 
  10.     } 

4.2. 插入結(jié)點(diǎn)

只需將新結(jié)點(diǎn)插入二叉堆最后一個(gè)結(jié)點(diǎn)的下一個(gè)位置,然后重新構(gòu)造二叉堆。

以最小堆為例,代碼如下:

  1. /** 
  2.  * @description: 向最小堆中插入一個(gè)元素 
  3.  * @param {BinaryHeap} *heap 最小堆指針 
  4.  * @param {int} elem 新元素 
  5.  * @return {*} 無 
  6.  */ 
  7. void insert_into_min_heap(BinaryHeap *heap, int elem) 
  8.     if (heap->length == MAXSIZE) { 
  9.         printf("二叉堆已滿,無法插入。\n"); 
  10.         return
  11.     } 
  12.     heap->array[heap->length] = elem; // 插入 
  13.     heap->length++; // 更新長度 
  14.     create_min_heap(heap); // 重新構(gòu)造 

4.3. 刪除結(jié)點(diǎn)

將最后一個(gè)結(jié)點(diǎn)移動(dòng)(賦值)到堆頂,然后重新構(gòu)造二叉堆。

以最小堆為例,代碼如下:

  1. /** 
  2.  * @description: 刪除最小堆的堆頂 
  3.  * @param {BinaryHeap} *heap 最小堆指針 
  4.  * @param {int} *elem 保存變量指針 
  5.  * @return {*} 無 
  6.  */ 
  7. void delete_from_min_heap(BinaryHeap *heap, int *elem) 
  8.     if (heap->length == 0) { 
  9.         printf("二叉堆空,無元素可刪。\n"); 
  10.         return
  11.     } 
  12.     *elem = heap->array[0]; 
  13.     heap->array[0] = heap->array[heap->length - 1]; // 移動(dòng)到堆頂 
  14.     heap->length--; // 更新長度 
  15.     create_min_heap(heap); //重新構(gòu)造 

5. 總結(jié)

構(gòu)造最大堆的本質(zhì)是:將每顆子樹的“大”結(jié)點(diǎn)上浮作為雙親,“小”結(jié)點(diǎn)下沉作為孩子。

構(gòu)造最小堆的本質(zhì)是:將每顆子樹的“小”結(jié)點(diǎn)上浮作為雙親,“大”結(jié)點(diǎn)下沉作為孩子。

插入結(jié)點(diǎn)的本質(zhì)是:插入新結(jié)點(diǎn)至二叉堆末尾,破壞了原二叉堆的結(jié)構(gòu),然后調(diào)整新得到的完全二叉樹,重新構(gòu)造二叉堆。

刪除結(jié)點(diǎn)的本質(zhì)是:刪除堆頂,破壞了原完全二叉樹的結(jié)構(gòu),然后使用最后一個(gè)結(jié)點(diǎn),重新構(gòu)造完全二叉樹,再調(diào)整新得到的完全二叉樹,重新構(gòu)造二叉堆。

用四個(gè)字概括就是——破而后立。

至于代碼實(shí)現(xiàn),關(guān)鍵在于結(jié)點(diǎn)的調(diào)整,把這個(gè)搞明白,剩下的就簡單了。

以上就是二叉堆的原理和相關(guān)操作。

完整代碼請(qǐng)移步至 GitHub[1] | Gitee[2] 獲取。

參考資料
[1]GitHub: https://github.com/xingrenguanxue/Simple-DS-and-Easy-Algo

[2]Gitee: https://gitee.com/xingrenguanxue/Simple-DS-and-Easy-Algo

 

責(zé)任編輯:姜華 來源: 二十二畫程序員
相關(guān)推薦

2020-08-31 07:43:58

二叉堆大頂堆存儲(chǔ)

2020-11-23 08:53:34

堆Heap

2021-03-02 10:57:39

二叉樹二叉堆節(jié)點(diǎn)

2023-04-06 07:39:48

2021-04-06 08:20:24

二叉搜索樹數(shù)據(jù)結(jié)構(gòu)算法

2020-04-27 07:05:58

二叉樹左子樹右子樹

2018-03-05 22:45:34

2020-12-11 09:49:29

二叉樹搜索樹數(shù)據(jù)

2021-04-19 07:47:42

數(shù)據(jù)結(jié)構(gòu)二叉樹Tree

2021-04-20 08:37:14

數(shù)據(jù)結(jié)構(gòu)二叉樹

2022-10-26 23:58:02

二叉樹數(shù)組算法

2021-08-27 11:36:44

二叉樹回溯節(jié)點(diǎn)

2021-08-31 11:35:24

二叉搜索樹迭代法公共祖先

2021-04-28 20:12:27

數(shù)據(jù)結(jié)構(gòu)創(chuàng)建

2013-07-15 16:35:55

二叉樹迭代器

2021-05-09 20:22:41

順序查找二叉查找數(shù)據(jù)結(jié)構(gòu)

2021-03-17 08:19:22

二叉樹LeetCode

2011-09-19 15:40:35

2020-07-29 08:14:59

云計(jì)算云遷移IT

2014-06-06 16:08:17

初志科技
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 免费h视频 | 神马久久香蕉 | 最新国产视频 | 国产成人综合在线 | 九九综合 | 久久国产精品网站 | 91视频久久 | 四虎永久免费影院 | 成人在线中文字幕 | 亚州精品天堂中文字幕 | 91精品国产91久久久久久吃药 | 午夜亚洲 | 久久久亚洲一区 | 日韩av三区| 精品视频国产 | 久久一| 亚洲欧洲精品一区 | 国产精品日日做人人爱 | 精品日韩在线 | 欧美成人一区二区三区 | 免费观看一级毛片 | 国产精品一区二区在线播放 | 综合久久99 | 国产精品久久久久久久久久久免费看 | 99精品国产一区二区三区 | 亚洲男人天堂av | 美女黄频 | 九九av| 国产精品视频中文字幕 | 国产午夜视频 | 日韩一区二区三区视频 | 超碰在线免费公开 | 黄色a三级 | 国产在线一区二区 | 国产色播av在线 | 97人澡人人添人人爽欧美 | 国产免费一区二区 | 精品无码久久久久国产 | 国产999精品久久久影片官网 | 国产精品久久国产精品久久 | 欧美成人a|