此次用近萬字的講解帶你幹掉堆!





0. 前言

你們好,我是多選參數的程序鍋,一個正在搗鼓操做系統、學數據結構和算法以及 Java 的失業人員。最近忙着搞論文,還有刷刷 LeetCode 上的題,推文的事被耽誤了一下,可是並無忘記要發推文,雖遲但到吧。node

整篇文章的主要知識提綱如圖所示,本篇相關的代碼均可以從 https://github.com/DawnGuoDev/algorithm 獲取,另外,該倉庫除了包含了基礎的數據結構和算法實現以外,還會有數據結構和算法的筆記、LeetCode 刷題記錄(多種解法、Java 實現) 、一些優質書籍整理。git

1. 基本概念

堆是一種特殊的樹。只要知足如下兩個條件就是一個堆。github

  • 堆是一個徹底二叉樹。既然是徹底二叉樹,那麼使用數組存儲的方式將會很方便。
  • 堆中的每一個節點的值都必須大於等於(或小於等於)其子節點的值。對於大於等於子節點的堆又被稱爲「大頂堆」;小於等於子節點的堆又被稱爲「小頂堆」。

以下圖所示,一、2 是大頂堆,3 是小頂堆,4 不是堆。同時,咱們能夠看出,對於同一組數據,能夠構建出不一樣形態的堆。web

2. 堆的存儲

知足堆的一個要求是」堆是一個徹底二叉樹「,而徹底二叉樹比較適合用數組存儲,一是節省空間,二是經過數組的下標就能夠找到父節點、左右子節點(數組下標最好從 1 開始)。本篇講的例子,及其代碼的實現都將從數組下標爲 1 的地方開始。算法

下面是一個用數組存儲堆的例子,假如從下標 1 開始存儲堆的數據,那麼下標爲 i 的節點的左子節點在數組中的下標爲 2*i,右子節點在數組中的下標爲 2*i+1,父節點在數組中的下標爲 i/2。假設從下標 0 開始存儲的話,下標爲 i 的節點的左子節點的下標爲 2*i+1,右子節點的下標爲 2*i+2,父節點的下標爲 (i-1)/2api

3. 堆的操做

堆最核心的操做是分別往堆中插入一個元素以及刪除堆頂元素。爲何這邊只提到刪除堆頂元素?由於刪除堆的其餘元素是毫無心義,堆頂元素通常是最大或者最小值,因此刪除堆頂元素纔有意思。數組

另外,在往堆中插入一個元素或者刪除堆頂元素以後,須要確保知足堆的兩個特性。而將不符合兩個特性的「堆」調整成符合兩個特性堆的過程被稱爲堆化(heapify)。堆化有兩種方式:從上往下堆化和從下往上堆化。二者的區別在於調整的順序,從上往下堆化是指從堆頂元素開始向下調整使其最終符合兩個特性,而從下往上則相反。在插入和刪除的操做中會具體看到這兩種方式。緩存

下面的闡述都將以大頂堆爲例,小頂堆同理。微信

3.1. 往堆中插入一個元素

插入元素時,將新插入的元素放到堆的最後比較方便。此時,採用從下往上的堆化方法比較合適,整個從下往上的堆化過程就是向上不斷跟父節點對比,而後交換。如圖所示,咱們往一棵現成的堆中插入了一個元素 22。那麼此時的「堆」不符合堆的兩個特性了。那麼新插入的節點先與父節點進行比較,不知足父子節點的應有的大小(大頂堆中,父節點的值應該大於子節點)則互換兩個節點。互換以後的父節點和它的父節點不知足大小關係的話就繼續交換。重複這個過程,直至調整過程當中的父子節點都知足大小關係爲止。數據結構

咱們將這個從下到上的堆化過程,實現爲以下代碼段所示

public void insert(int data) {
if (this.count >= this.capacity) {
return;
}
++this.count;
this.heap[this.count] = data;
heapifyFromBottom();
}

public void heapifyFromBottom() {
int i = this.count;
while (i/2 > 0 && this.heap[i] > this.heap[i/2]) {
swap(i, i/2);
i = i/2;
}
}

3.2. 刪除堆頂元素

從堆的第二點特性「堆中的每一個節點的值都必須大於等於(或小於等於)其子節點的值」能夠推出堆中最大(或最小)的值存儲在堆頂元素中(大頂堆堆頂則是最大值)。所以刪除堆頂元素是有意義的,而刪除堆中的其餘元素是沒有意義的。

那麼刪除堆頂元素以後,整個堆就不符合堆的兩個條件了。此時,正確的方式是把最後一個節點放到堆頂處,而後從上而下依次比較父子節點的大小,若是不知足大小關係則進行交換,直至父子節點之間知足大小關係爲止。這種方法就是從上往下的堆化方法

如圖所示,假如將堆頂的元素 33 刪除以後,那麼將最後一個節點的元素 12 放到堆頂處。而後 12 與 27 進行比較,由於不知足大頂堆的要求,12 與 27 進行交換。以此類推,最終調整後的堆知足了所要求的兩個特性。

咱們將上述從上而下的堆化過程實現爲以下圖所示

public void delete () {
if (this.count <= 0) {
return;
}

this.heap[1] = this.heap[this.count--];

heapifyFromTop(1);
}

public void heapifyFromTop(int begin) {
while (true) {
int i = begin; // i 是節點及其左右子節點中較大值的那個節點的下標

/* 就是在節點及其左右子節點中選擇一個最大的值,與節點所處的位置進行;
可是,須要注意的是假如這個值正好是節點自己,那麼直接退出循環;
不然須要進行交換,而後從交換以後的節點開始繼續堆化 */

if (begin * 2 <= this.count && this.heap[begin] < this.heap[2 * begin]) {
i = 2 * begin;
}

if ((2 * begin + 1) <= this.count && this.heap[i] < this.heap[2 * begin + 1]) {
i = 2 * begin + 1;
}

if (i == begin) {
break;
}

swap(begin, i);

begin = i;
}
}

3.3. 時間複雜度

堆化的過程是順着節點的路徑進行比較交換的,那麼比較交換這一組操做的次數是跟樹的層次有關的。對於一棵含 n 個節點的徹底二叉樹來講,樹的層次不會超過  。所以,整個堆化過程的時間複雜度是 O(logn)。那麼,插入一個元素和刪除堆頂元素的時間複雜度都是 O(logn)

4. 堆排序

藉助堆這種數據結構實現的排序被稱爲堆排序。堆排序是一種原地的、時間複雜度爲 O(nlogn) 且不穩定的排序算法。整個堆排序分爲建堆和排序兩個步驟。

4.1. 堆排序過程

4.1.1. 建堆

首先是將待排序數組創建成一個堆,秉着能不借助額外數組則不借助的原則,咱們能夠直接在原數組上直接操做。這樣,建堆有兩個方法:

  • 第一種方法相似於上述堆的操做中「往堆中插入一個元素」的思想。剛開始的時候假設堆中只有一個元素,也就是下標爲 1 的元素。而後,將下標爲 2 的元素插入堆中,並對堆進行調整。以此類推,將下標從 2 到 n 的元素依次插入到堆中。這種建堆方式至關於將待排序數組分紅「堆區」和「待插入堆區」。

    如圖所示,咱們將對待排序數據 七、五、1九、八、4 進行建堆(大頂堆)。能夠看到初始化堆就一個元素 7。以後將指針移到下標爲 2 的位置,將 5 這個元素添加到堆中並從下往上進行堆化。以後,再將指針移到下標爲 3 的位置,將 19 這個元素添加到堆中並從下往上進行堆化。依次類推。

  • 第二種方法是將整個待排序數組都當成一個「堆」,可是這個「堆」不必定知足堆的兩個條件,所以咱們須要對其進行總體調整。那麼,調整的時候是從數組的最後開始,依次往前調整。調整的時候,只須要調整該節點及其子樹知足堆的要求,而且是從上往下的方式進行調整。因爲,葉子節點沒有子樹,因此葉子節點不必調整,咱們只須要從第一個非葉子節點開始調整(這邊的第一是從後往前數的第一個)。那麼第一個非葉子節點的下標爲 n/2,所以咱們只須要對 n/2 到 1 的數組元素進行從上往下堆化便可(下標從 n/2 到 1 的數組元素所在節點都是非葉子節點,下標從 n/2+1 到 n 的數組元素所在節點都是葉子節點)。

    如圖所示,咱們將對待排序數據 七、五、1九、八、四、一、20、1三、16 進行建堆(大頂堆)。能夠看到整個過程是從 8 這個元素開始進行堆化。在對 8 進行堆化的時候,僅對 8 及其子樹進行堆化。在對 5 進行堆化的時候,僅對 5 及其子樹進行堆化。


咱們將第二種思路實現成以下代碼段所示:

public void buildHeap(int[] datas, int len) {
this.heap = datas;
this.capacity = len - 1;
this.count = len - 1;
for (int i = this.count/2; i >=1; i--) {
heapifyFromTop(i);
}
}

public void heapifyFromTop(int begin) {
while (true) {
int i = begin; // i 是節點及其左右子節點中較大值的那個節點的下標

/* 就是在節點及其左右子節點中選擇一個最大的值,與節點所處的位置進行;
可是,須要注意的是假如這個值正好是節點自己,那麼直接退出循環;
不然須要進行交換,而後從交換以後的節點開始繼續堆化 */

if (begin * 2 <= this.count && this.heap[begin] < this.heap[2 * begin]) {
i = 2 * begin;
}

if ((2 * begin + 1) <= this.count && this.heap[i] < this.heap[2 * begin + 1]) {
i = 2 * begin + 1;
}

if (i == begin) {
break;
}

swap(begin, i);

begin = i;
}
}

爲何下標從 n/2 到 1 的數組元素所在節點都是非葉子節點,下標從 n/2+1 到 n 的數組元素所在節點都是葉子節點?這個算是徹底二叉樹的一個特性。嚴格的證實暫時沒有,不嚴謹的證實仍是有的。這裏採用反證法,假如 n/2 + 1 不是葉子節點,那麼它的左子節點下標應該爲 n+2,可是整個徹底二叉樹最大的節點的下標爲 n。因此 n/2 + 1 不是葉子節點不成立,即 n/2 + 1 是葉子節點。那麼同理可證 n/2 + 1 到 n 也是如此。而對於下標爲 n/2 的節點來講,它的左子節點有的話下標應該爲 n,n 在數組中有元素,所以 n/2 有左子節點,即 n/2 不是葉子節點。同理可證 1 到 n/2 都不是葉子節點。

4.1.2. 排序

建完堆(大頂堆)以後,接下去的步驟是排序。那麼具體該怎麼實現排序呢?

此時,咱們能夠發現,堆頂的元素是最大的,即數組中的第一個元素是最大的。實現排序的過程大體以下:咱們把它跟最後一個元素交換,那最大元素就放到了下標爲 n 的位置。此時堆頂元素不是最大,所以須要進行堆化。採用從上而下的堆化方法(參考刪除堆頂元素的堆化方法),將剩下的 n-1 個數據構建成堆。最後一個數據由於已知是最大了,因此不用參與堆化了。n-1 個數據構建成堆以後,再將堆頂的元素(下標爲 1 的元素)和下標爲 n-1 的元素進行交換。一直重複這個過程,直至堆中只剩一個元素,此時排序工做完成。如圖所示,這是整個過程的示意圖。

下面將這個過程使用 Java 實現,以下圖所示

public void heapSort() {
while (this.count > 1) {
swap(this.count, 1);
this.count--;
heapifyFromTop(1);
}
}

4.2. 算法分析

4.2.1. 時間複雜度

堆排序的時間複雜度是由建堆和排序兩個步驟的時間複雜度疊加而成。

  • 建堆的時間複雜度

在採用第二方式建堆時,從粗略的角度來看,每一個節點進行堆化的時間複雜度是 O(logn),那麼 n/2 個節點堆化的總時間複雜度爲 O(nlogn)。可是這此時粗略的計算,更加精確的計算結果不是 O(nlogn),而是 O(n)

由於葉子節點不須要進行堆化,因此須要堆化的節點從倒數第二層開始。每一個節點須要堆化的過程當中,須要比較和交換的次數,跟這個節點的高度 k 成正比。那麼全部節點的高度之和,就是全部節點堆化的時間複雜度。假設堆頂節點對應的高度爲 h ,那麼整個節點對應的高度如圖所示(以滿二叉樹爲例,最後一層葉子節點不考慮)。

那麼將每一個非葉子節點的高度求和爲

求解這個公式可將兩邊同時乘以 2 獲得 S2,

而後再減去 S1,從而就獲得 S1

因爲

因此最終時間複雜度爲 O(2n-logn),也就是 O(n)。

  • 排序的時間複雜度

排序過程當中,咱們須要進行 (n-1) 次堆化,每次堆化的時間複雜度是 O(logn),那麼排序階段的時間複雜度爲 O(nlogn)。

  • 總的時間複雜度

那麼,整個總的時間複雜度爲 O(nlogn)

不對建堆過程的時間複雜度進行精確計算,也就是建堆以 O(nlogn) 的時間複雜度算的話,那麼總的時間複雜度仍是 O(nlogn)。

4.2.2. 穩定與否

堆排序不是穩定的排序算法。由於在排序階段,存在將堆的最後一個節點跟堆頂點進行互換的操做,因此有可能會改變相同數據的原始相對順序。好比下面這樣一組待排序 20、1六、1三、13 ,在排序時,第二個 13 會跟 20 交換,從而更換了兩個 13 的相對順序。

4.2.3. 是否原地

堆排序是原地排序算法,由於堆排序的過程當中的兩個步驟中都只須要極個別臨時存儲空間。

4.3. 堆排序總結

在實際開發中,爲何快速排序要比堆排序性能要好?

  1. 對於一樣的數據,在排序過程當中,堆排序算法的數據交換次數要多於快速排序

    對於基於比較的排序算法來講,整個排序過程就是由比較和交換這兩個操做組成。快速排序中,交換的次數不會比逆序度多。可是堆排序的過程,第一步是建堆,這個過程存在大量的比較交換操做,而且頗有可能會打亂數據原有的相對前後順序,致使原數據的有序度下降。好比,在對一組已經按從小到大的順序排列的數據進行堆排序時,那麼建堆過程會將這組數據構建成大頂堆,而這一操做將會讓數據變得更加無序。而採用快速排序的方法時,只須要比較而不須要交換。

    最直接的方式就是作個試驗看一下,對交換次數進行統計。

  2. 堆排序的訪問方式沒有快速排序友好

    快速排序來講,數據是順序訪問的。而堆排序,數據是跳着訪問的。訪問的數據量如何很大的話,那麼堆排序可能對 CPU 緩存不太友好。

5. 堆的其餘應用

上面介紹了堆的理論和堆排序,可是堆這種數據結構除了用在堆排序以外,還有其餘幾個很是重要的應用:優先級隊列、求 Top K 問題以及求中位數問題。

5.1. 優先級隊列

優先級隊列跟普通隊列全部不一樣。普通隊列最大的特性是先進先出,而優先級隊列則按照優先級來,優先級高的先出隊。咱們能夠發現,優先級隊列和堆有共通之處。堆頂元素都是最大或者最小的,那麼取堆頂元素就相似於優先級隊列的出隊操做。所以,優先級隊列最直接、最高效的實現方式是採用堆來實現。

不少語言也都有提供優先級隊列的實現,好比 Java 的 PriorityQueue,C++ 的 priority_queue 等。固然,優先級隊列也有不少應用場景,這邊舉兩個簡單的例子來看一下優先級隊列是怎麼用的。

5.1.1. 合併有序小文件

假設咱們須要將 100 個存儲着有序的單詞的文件合併成一個有序的大文件。按照以前講過的方法,咱們可使用數組的方式,也就是從這個 100 個文件中,各取第一個單詞。以後,根據字典序進行比較,將字典序最小的那個單詞放入合併後的大文件,並從數組中刪除。而後,從刪去單詞的那個小文件中再取一個單詞,將其放入數組。以後重複上述操做,直至 100 個文件的單詞都被合併到最終的有序的大文件中。使用數組這種方式,每次取字典序最小的那個單詞時都須要遍歷整個數組。顯然,數組的方式很不高效。

那麼咱們可使用優先級隊列,由於咱們每次須要的都是當前字典序最小的那個單詞,這跟優先級隊列的特性很類似。所以,咱們能夠維護一個容量爲 100 的小頂堆,每次取小頂堆的堆頂單詞,而後將其放入合入的大文件中,以後再從小文件中取新的單詞放入堆中,並進行堆化。重複這個過程,最終將 100 個文件的單詞數據全都合併到大文件中。那麼採用堆的方式以後,每次取字典序最小單詞的時間主要就是堆化的時間,而堆化的時間複雜度是 O(logn),這邊 n 是 100。顯然這個時間比使用數組的方式高效多了。

5.1.2. 高性能定時器

假設有一個定時器,定時器維護了不少任務。每一個任務都設定了一個要觸發執行的時間點。普通的方法可能就是採用輪詢的方式。每隔一段時間輪詢一次(好比 1 秒),查看是否有任務到達設定的執行時間。若是到達了,就拿出來執行。然而,這種作法比較低效。一是可能有些任務執行還要好久的時間,那麼前面不少次的掃描將是徒勞的;二是當任務列表很大的時候,每次這麼掃描是很費時的。

那麼咱們照樣可使用優先級隊列。咱們按照任務設定的執行時間構建小頂堆,堆頂元素是任務設定的執行時間距離如今最近的任務,也就是應該最早被執行的任務。當咱們拿到堆頂任務的執行時間點以後,與如今的時間進行相減,從而獲得一個時間間隔 T1 。接下去,咱們就能夠不用每隔 1 秒進行掃描,而是在 T1 秒以後,直接取堆頂的任務來執行。而且,對剩下的任務進行堆化,再計算新的堆頂任務距離如今的時間間隔 T2,將其做爲執行下一個任務須要等待的時間,以此類推。採用這種方式後,性能相比每隔 1 秒的輪詢方式提升了許多。

5.2. 求 Top K

求 Top K 的問題能夠分爲兩類,一類是針對靜態數據集合,也就至關於這個數據集合事先肯定了,不會再變了。另外一類是針對動態數據集合,也就是說數據事先並不徹底肯定,會有數據不斷的加入到集合中。下面針對這兩類分別進行闡述,求 Top K 大的問題爲例。

  • 針對靜態數據,使用堆來求 Top K 的方法以下。

    咱們須要維護一個大小爲 K 的小頂堆。一開始的時候,咱們能夠拿數組前 K 個元素先將這個堆填滿,也就至關於用前 K 個元素先構建一個小頂堆。以後,順序遍歷數組剩下的元素。若是,遍歷到的元素大於堆頂元素,則將原堆頂元素替換爲遍歷到的元素,而後叢上而下的方式進行堆化。若是比堆頂元素小,則不作處理,繼續遍歷。當遍歷完成以後,堆中的元素則是前 Top K 大的數據了。

    對前 k 個數據構建堆時,時間複雜度爲 O(k),那麼假設剩下的 n-k 個數據在遍歷以後都須要加入堆並進行堆化,那麼這個的時間複雜度爲 O((n-k)logk)。總的時間複雜度應該爲 O((n-k)logk + k)。簡單粗暴點來講,假如在對前 k 個數據構建堆時,是按照「往堆中插入一個元素」的方式的話,那麼時間複雜度也能夠算做 O(nlogk)。

  • 針對動態數據,使用堆來求 Top K 的方法以下。

    一樣,咱們須要維護一個大小爲 K 的小頂堆。一開始的時候,當堆中數據不滿 K 個的時候,每當插入一個數據以後,就將其放入堆中。當堆中數據滿 K 個以後,若是有新的數據被添加到集合中時,那麼先與堆頂元素進行比較。若是大於堆頂元素,則將堆頂元素替換爲新數據,而後進行堆化。若是小於堆頂元素,則不作處理。這樣,不管何時查詢時堆中的元素始終是 Top K 大的。

    固然還有另外一種方法是,在每次查詢的時候再針對新的數據集合求 Top K 問題,採用針對靜態數據的方式。

    我的以爲這種方式和另外一種方式,要具體問題具體分析。好比查詢時間比較短,前面第一種的實時方式比較合適。查詢時間比較長,而且新數據增長特別快,那麼第二種方式比較合適。

快速排序也能求 Top K 的問題,可是快速排序更加適合於靜態數據,若是數據集合一直在變更,那堆的方式會更適合一點。

另外在 Top K 的問題上,快排和堆這兩種方式,堆會更勝一籌。

5.3. 求中位數及各類百分位的數據

中位數是按順序排列的一組數據中居於中間位置的數。若是數據的個數是奇數,那麼中位數的位置爲 n/2+1(數據是從 1 開始編號,n 是最後一個數據的編號)。若是數據的個數是偶數,那麼中位數的位置爲 n/2 或者 n/2 +1(同上),通常狀況下是取這兩個數的平均值做爲中位數,固然也能夠取 n/2 位置的數據做爲中位數。下面咱們選擇這兩個數的平均值做爲中位數爲例。

  • 針對一組靜態數據,求中位樹的最好方式就是先排序,而後再取中位數。假若有查詢中位數的操做時,能夠將第一次求得中位數的值緩存下來或者中位數的位置緩存下來。

  • 針對動態數據集合時,集合不斷地變更,中位數也不斷的變更。那麼,再用先排序再求中位數的方式,效率就不高了。爲此,咱們可使用堆這種數據結構,而且維護兩個堆,一個大頂堆,一個小頂堆。大頂堆存儲前一半的數據,小頂堆存儲後一半的數據。大頂堆中的數據都比小頂堆中的數據小,也就是大頂堆的堆頂數據小於等於小頂堆的堆頂數據(有點相似沙漏)。

    如圖所示,假如 n 是奇數,那麼 n/2+1 個數據全都存儲在大頂堆中,剩下的 n/2個數據存儲在小頂堆中。這樣,大頂堆的頂堆數據就是中位數(固然 n/2 個數據全都存儲在大頂堆,剩下的 n/2 + 1 個數據存儲在小頂堆中也行,那麼小頂堆的頂堆數據就是中位數)。假如 n 是偶數,那麼前 n/2 個數據全都存儲在大頂堆中,剩下的 n/2 個數據存儲在小頂堆中。那麼大頂堆的堆頂數據和小頂堆的堆頂數據求平均就是中位數了。

    那麼當插入新的數據時,若是這個數據比大頂堆的數據小,那麼則將其插入大頂堆。反之,則將其插入小頂堆。同時,插入新的數據以後須要確保兩個堆的數據數量比例按照約定來,即插入以後若是數據數量爲偶數,那麼兩個堆的數量須要相等;若是數據數量爲奇數,那麼大頂堆要比小頂堆多一個數據。假如插入數據以後,兩個堆的數據數量不符合約定了,那麼則須要移動數據使其知足約定,移動的數據通常都是堆頂元素。

    使用這種方法,插入數據的時候會涉及到堆化,時間複雜度爲 O(logn),可是在求中位數的時候,只須要 O(1)。所以,我的以爲跟上述求 Top K 的相似。若是查詢中位數很頻繁,那麼動態方式的方式很 nice 了;假如查詢中位數不頻繁,那麼靜態的方式可能會更好。

另外,動態數據集合使用兩個堆來求中位數的方法也能夠很好地用於求其餘百分位的數據。求中位數其實就至關於求 50% 處的數據。好比,當求 90% 處的數據時。咱們能夠 90% 的數據保存在大頂堆中,10 % 的數據保存在小頂堆中。一樣的,插入數據以後也要維護兩個堆的比例,而且插入的時間複雜度爲 O(logn),而查詢的時間複雜度爲 O(1)。

這邊必定須要兩個堆嗎?我以爲是須要的,好比在查找 90% 的數據時,雖然咱們都只須要取大頂堆的堆頂元素。可是,假如咱們只維護一個大頂堆。某次插入數據時,這個數據沒被插入到大頂堆中,結果致使大頂堆和剩下的元素不符合比例了(好比大頂堆比應有的少了一個數據,而小頂堆比應有的多了一個數據)。那麼咱們須要從剩下的數據中找出在剩下的數據中最小的那個數據插入大頂堆。此時,假如剩下的數據直接使用小頂堆的方式來表示,那麼只須要取堆頂元素便可,很方便。這個使用堆的方式和數組的方式,在合併有序小文件的時候已經作過比較了。因此須要兩個堆!

6. 總結

  • 堆是一種很重要的數據結構。堆知足兩個特性,一是徹底二叉樹,二是任意節點都比其子節點大(或者小)。任意節點都比其子節點大的叫作大頂堆,不然叫作小頂堆。因爲堆是一個徹底二叉樹,因此堆使用數組的方式存儲會更加合適。所以,咱們對堆的操做通常都是在基於數組完成的。

  • 堆常見的操做是插入元素和刪除堆頂元素(注意:刪除堆中的其餘元素是沒有意義的)。另外,插入和刪除元素以後大部分狀況下都是須要進行堆化的,堆化的時間複雜度是 O(logn),所以這兩個操做的時間複雜度都是 O(logn)。

  • 堆最多見的應用是堆排序。使用堆排序對一組數據集合進行排序時,分爲兩步驟:①先是建堆。建堆就是將這組數據結合創建成一個堆,這裏能夠不用開闢新的內存空間而基於原數據集合直接創建堆。基於原數據集合直接創建堆能夠採用兩種方式,一種是從頭日後開始遍歷數組,這種方式相似於往堆中插入數據;另外一種則是從後往前,從最後一個非葉子節點開始對該非葉子及其子樹進行從上往下的堆化。②再是排序。因爲堆頂的元素是最值,所以先將堆頂元素和最後一個元素交換,而後將堆中的數量減一(至關於把位於最後位置的堆頂元素給刪除了)。以後再對剩下的數據進行堆化。堆化完成以後,再取堆頂元素,再交換,以此類推。最終實現排序。

    堆還有其餘幾種常見的應用。①優先級隊列。優先級隊列中,優先級最高的會先出隊,這跟堆的本質是同樣的。那麼優先級隊列可用於多個有序小文件的合併,同理也適合於多個有序數組的合併。②求 Tok K 問題。Top K 問題也可使用快速排序的方式實現求解。可是,整體來看堆更適合用於求 Top K 問題,尤爲針對動態數據集合。堆求 Top K 主要是維護一個容量爲 K 的小頂堆,當遍歷到的數據比堆頂元素大則加入堆,反之什麼也不作。③求中位數或者各類百分位的數據。此時,咱們應該維護兩個堆,一個大頂堆,一個小頂堆。大頂堆中的元素都比小頂堆中的元素小。兩個堆中的數據比例要符合所求的百分位狀況。好比中位數,就兩個堆中的數據比例要佔 50%。好比 90% 位的數據,那麼小頂堆中的數據佔 90%,大頂堆中的數據佔 10%。那麼,小頂堆的堆頂元素就是 90% 位的數據。另外,當兩個堆的數據佔比不符合約定時,須要進行數據的移動。

    對應用在求 Top K 和中位數時,發現針對動態數據集合效果會更顯著。由此,咱們能夠想到,堆這種數據結構可能更適合一些動態數據集合的場景。


不甘於「本該如此」,多選參數 值得關注




本文分享自微信公衆號 - 多選參數(zhouxintalk)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索