數據結構 — 堆

目錄

堆可以是一個完全二叉樹,這樣實現的堆也被稱爲二叉堆。完全二叉樹,它的葉子節點都在最後一層,並且這些葉子節點都是靠左排序的。

堆中節點的值都 >=(或 <=)其子節點的值,堆中如果節點的值都 >= 其子節點的值,我們把它稱爲大頂堆,如果都 <= 其子節點的值,我們將其稱爲小頂堆。

從堆的特點可知,下圖中 1、2 是大頂堆,3 是小頂堆, 4 不是堆(不是完全二叉樹)。

在這裏插入圖片描述

從上圖也可以看到,一組數據如果表示成大頂堆或小頂堆,可以有不同的表示方式,因爲它只要求節點值 >=(或 <=)子節點值,並未規定左右子節點的排列方式。

堆的底層是如何表示的呢,從以上堆的介紹中我們知道堆是一顆完全二叉樹,而完全二叉樹可以用數組表示:

在這裏插入圖片描述

如上圖示,給完全二叉樹按從上到下、從左到右編號,則對於任意一個節點來說,很容易得知如果它在數組中的位置爲 i,則它的左右子節點在數組中的位置爲 2i、2i + 1,通過這種方式可以定位到樹中的每一個節點,從而串起整顆樹。

一般對於二叉樹來說每個節點是要存儲左右子節點的指針,而由於完全二叉樹的特點(葉子節點都在最後一層,並且這些葉子節點都是靠左排序的),用數組來表示它再合適不過,用數組來存儲的好處在於不需要存指向左右節點的指針,在這顆樹很大的情況下能省下很多空間。

堆的應用

  1. 優先級隊列:我們知道隊列都是先進先出的,而在優先級隊列中,元素被賦予了權重的概念,權重高的元素優先執行,執行完之後下次再執行權重第二高的元素。顯然用堆來實現優先級隊列再合適不過了,只要用一個大頂堆來實現優先級隊列即可,當權重最高的隊列執行完畢,將其移除(相當於刪除堆頂),再選出優先級第二高的元素(堆化讓其符合大頂堆的條件)。

  2. 求 TopK 問題:求出 n 個元素中前 K 個最大/最小的元素。

  3. 求 TP99 問題:TP99 指的是在一個時間段內(如5分鐘),統計某個接口(或方法)每次調用所消耗的時間,並將這些時間按從小到大的順序進行排序,取第99%的那個值作爲 TP99 值,舉個例子, 假設這個方法在 5 分鐘內調用消耗時間爲從 1 s 到 100 s 共 100 個數,則其 TP99 爲 99,這個值爲啥重要呢,對於某個接口來說,這個值越低,代表 99% 的請求都是非常快的,說明這個接口性能很好,反之,就說明這個接口需要改進。

有人可能會說以上的這些應用貌似用快排或其他排序也能實現,沒錯,確實能實現,但是我們需要注意到,在靜態數據下用快排確實沒問題,但在動態數據上,如果每插入/刪除一個元素對所有的元素進行快排,其實效率不是很高,由於要快排要全量排序,時間複雜度是 O(nlog n),而堆排序就非常適合這種對於動態數據的排序,對於每個新添加的動態數據,將其插入到堆中,然後進行堆化,時間複雜度只有 O(logK)

綜上,堆是一種非常重要的數據結構,在對動態數據進行排序時性能很高,優先級隊列底層也是普遍採用堆來管理。

堆的基本操作

堆有兩個基本的操作:

  1. 構建堆(往堆中插入元素)
  2. 刪除堆頂元素

往堆中插入元素

往堆中插入元素後(如下圖示),我們需要繼續滿足堆的特性,所以需要不斷調整元素的位置直到滿足堆的特點爲止(堆中節點的值都 >= 或 <= 其子節點的值),我們把這種調整元素以讓其滿足堆特點的過程稱爲堆化(Heapify)。

在這裏插入圖片描述

由於上圖中的堆是個大頂堆,所以我們需要調整節點以讓其符合大頂堆的特點:不斷的比較子節點與父節點,如果子節點大於父節點,則交換,不斷重複此過程,直到子節點小於其父節點。來看下上圖插入節點 11 後的堆化過程:

在這裏插入圖片描述
這種調整方式是先把元素插到堆的最後,然後自下而上不斷比較子節點與父節點的值,我們稱之爲由下而上的堆化。時間複雜度就是樹的高度,所以爲 O(logn)。

刪除堆頂元素

由於堆的特點,所以其根節點(堆項)要麼是所有節點中最大,要麼是所有節點中最小的,當刪除堆頂元素後,也需要調整子節點,以讓其滿足堆(大頂堆或小頂堆)的條件。

假設我們要操作的堆是大頂堆,則刪除堆頂元素後,要找到原堆中第二大的元素以填補堆頂元素,而第二大的元素無疑是在根節點的左右子節點上,假設是左節點,則用左節點填補堆頂元素之後,左節點空了,此時需要從左節點的左右節點中找到兩者的較大值填補左節點。不斷迭代此過程,直到調整完畢,調整過程如下圖示:

在這裏插入圖片描述

但是這麼調整後,問題來了,如上圖所示,在最終調整後的堆中,出現了數組空洞,對應的數組如下:

在這裏插入圖片描述
我們可使用最後一個元素覆蓋堆頂元素,然後再自上而下地調整堆,讓其滿足大頂堆的要求,這樣即可解決數組空洞的問題。

在這裏插入圖片描述

時間複雜度和插入堆中元素一樣,也是樹的高度,所以爲 O(logn)。

堆的排序

用堆怎麼實現排序?我們知道在大頂堆中,根節點是所有節點中最大的,於是我們有如下思路:

假設待排序元素個數爲 n(假設其存在數組中),對這組數據構建一個大頂堆,刪除大頂堆的元素(將其與數組的最後一個元素進行交換),再對剩餘的 n-1 個元素構建大頂堆,再將堆頂元素刪除(將其與數組的倒數第二個元素交換),再對剩餘的 n-2 個元素構建大頂堆。不斷重複此過程,這樣最終得到的排序一定是從小到大排列的,堆排序過程如下圖所示:

在這裏插入圖片描述

從以上的步驟中可以看到,重要的步驟就兩步,建堆(堆化,構建大頂堆)與排序。

參考文章

https://mp.weixin.qq.com/s/lk_lQJw8QSVQDqYLvRkfrQ

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章