圖說堆排序

這是【字節(jié)可視化系列】堆排序的第一篇文章。同時,文末會放上一張【速記卡】,方便快速回顧本文關(guān)鍵知識點。

本篇的主旨是理解二叉堆結(jié)構(gòu),所以具體實現(xiàn)代碼會留到第二篇講解。

提到堆,其實Java中的優(yōu)先隊列PriorityQueue就是基于堆結(jié)構(gòu)實現(xiàn)的;

而Java中的延遲隊列DelayQueue里面就用到了PriorityQueue;

再比如kafka中基于時間輪TimingWheel實現(xiàn)的延時定時器, 同樣離不開DelayQueue的配合。

這一切都和堆息息相關(guān)。

1 數(shù)組和樹

堆其實就是一個數(shù)組結(jié)構(gòu), 如圖

下面我們來點小魔法, 把數(shù)組變成樹!

這個數(shù)組對應(yīng)的樹結(jié)構(gòu)就是下面這樣:

數(shù)組結(jié)構(gòu)的下標(biāo)和樹結(jié)構(gòu)的節(jié)點對應(yīng)關(guān)系如下:

如果根節(jié)點對應(yīng)的數(shù)組下標(biāo)是0, 那么對于 i 節(jié)點,

左右子節(jié)點的位置為 2i +1和 2i+2;

i 節(jié)點的父節(jié)點位置為 ( i-1)/2;

簡單理解了數(shù)組和樹之后,下面我們進入正題。

2 最小堆結(jié)構(gòu)

首先我們來一個亂序的數(shù)組:

這個數(shù)組對應(yīng)的樹結(jié)構(gòu):

這只是一個簡單的樹結(jié)構(gòu), 還不是堆, 堆結(jié)構(gòu)是下面這樣子的:

這是一個最小堆, 可以看到根節(jié)點, 也就是堆頂, 是數(shù)組中最小的那個元素。

同時,整一個大的堆, 是由很多個小的堆組成的。

比如左邊這個小堆, 也是符合最小堆的定義, 堆頂?shù)?是三個數(shù)字中最小的:

右邊的小堆也是一個最小堆, 堆頂?shù)?是最小的數(shù)字:

那么怎么把一顆樹變成一個最小二叉堆呢?

3 插入和上浮

最簡單的辦法, 就是新建一個空的堆(一個空的數(shù)組), 把亂序數(shù)組中的所有元素依次插入到堆中, 在這個過程中堆會通過上浮來調(diào)整自身的結(jié)構(gòu)。

下面我們來解析下上面的動畫:

首先, 是往堆中?插入?元素, 比如往下面的這個最小堆中插入數(shù)字1:

當(dāng)1插入數(shù)組尾部之后, 就處于樹的最后一個葉子節(jié)點的位置:

然后通過?上浮?操作, 把A移到合適的位置。

簡單來說就是先和A的父節(jié)點比較, 如果比父節(jié)點小, 就和父節(jié)點交換位置, 這個過程一直持續(xù)到A的父節(jié)點比A小, 或者A已經(jīng)到達堆頂?shù)奈恢谩?/p>

下面繼續(xù)數(shù)字1這個例子, 當(dāng)1插入數(shù)組尾部之后, 會和他的父節(jié)點8作比較:

1比父節(jié)點8小, 交換位置:

1再和他的父節(jié)點4作比較:

1比父節(jié)點4小, 交換位置后1到達堆頂, 形成了一個最小堆結(jié)構(gòu)。

4 刪除和下沉

接下來講講堆節(jié)點的刪除, 這里的刪除是指取出堆頂?shù)墓?jié)點。

我們把堆頂?shù)脑厝〕龊? 堆會把剩下的數(shù)字中最小的那個數(shù)字再次推到堆頂?shù)奈恢谩?/p>

仔細看動畫,這里值得注意的是, 取出堆頂?shù)脑? 并不是直接把堆頂?shù)脑貏h除。什么意思呢?

在JavaScript中, 數(shù)組的pop操作就是把數(shù)組中最后一個元素刪除; 而Java中的堆棧Stack也有pop操作, 同樣是把最后一個元素移除。

而堆的?刪除?操作, 也是基于pop操作來實現(xiàn)。這樣做是為了維持完全二叉樹的結(jié)構(gòu)。

比如我們要把堆頂?shù)?刪除:

1. 先把根節(jié)點和最后一個節(jié)點交換位置, 也就是1和6交換位置;

此時堆頂不再是最小的數(shù)字,最小堆的特性被打破, 接下來我們要對堆頂?shù)脑剡M行?下沉?操作, 把樹結(jié)構(gòu)重新調(diào)整為堆結(jié)構(gòu)。

2. 先比較左右子節(jié)點大小,與最小節(jié)點交換位置 ;

3. 繼續(xù)步驟2;

繼續(xù)上面的例子,當(dāng)1節(jié)點移除后,此時會先對堆頂6的左右子節(jié)點進行大小比較, 找出比較小的那個子節(jié)點,也就是右節(jié)點3:

然后, 節(jié)點6再和右節(jié)點3比較:

節(jié)點6下沉, 此時6已經(jīng)是葉子節(jié)點了, 下沉操作結(jié)束, 可以看到此時整棵樹已經(jīng)重新變回了最小堆結(jié)構(gòu):

5 堆排序

有趣的是, 通過上面簡單的【插入并上浮】 和 【刪除并下沉】 , 我們就可以對亂序數(shù)組執(zhí)行堆排序了。

下面對亂序數(shù)組進行堆排序:

堆化,插入并上浮:

刪除并下沉:

排序完成后的數(shù)組:

最后對堆排序的總結(jié)就是:

1. 先把亂序數(shù)組堆化 (文中使用的是插入+上浮)

2. 再通過pop操作刪除堆頂元素, 通過下沉調(diào)整堆結(jié)構(gòu)

6 堆化 Heapify

平常的使用堆排序肯定不會再新建一個空的堆, 再把亂序數(shù)組的元素逐個插入堆中, 這樣太浪費空間, 下一章我們來講一下堆化。

7 色譜圖

簡單了解了堆排序后, 我們來看看16個元素進行堆排序后生成的色譜圖

也有網(wǎng)友說這是大腸圖, 感興趣的可以看看這篇文章了解下【譯】排序算法可視化之色譜圖, 后面本公眾號會開一個系列專門講解這種有趣的靜態(tài)可視化方法。

我們把這個圖拆成兩部分來看, 首先是左邊部分

這就是堆排序的第一步, 堆亂序數(shù)組堆化的過程

再來看看右邊部分

可以看到, 堆排序的第二步就是pop操作把堆頂元素移除, 圖中也體現(xiàn)得非常清晰,先把堆頂元素移到尾部, 被移除的元素就相當(dāng)于排好序了.

每次移除元素后, 都會對新的堆頂節(jié)點執(zhí)行下沉操作, 也就是圖中pop和pop之間的紅框部分, 我們可以清晰地看到堆在自我調(diào)節(jié)的過程

排序結(jié)束之后, 由淺到深的顏色依次呈現(xiàn)在我們眼前!

溫馨提示

文中的動畫是基于d3.js制作而成。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容