開心一刻
一天,一個演講老師正在演講正確的愛情觀
情到深處,老師激動的說道:你一個月掙三千,憑什麼讓一個月掙三萬的人喜歡你?
結果底下站起來一個女孩,說道:因為我騷呀
堆結構
定義:堆就是用陣列實現的完全二叉樹,並且根據堆屬性來排序,決定節點在樹中的順序
資訊量是不是有點大?
欸,有這些疑問就對了,我們慢慢往下看
堆屬性
堆分為兩種:大頂堆和小頂堆,也稱最大堆和最小堆
在大頂堆中,父節點的值大於等於左右孩子節點的值。在小頂堆中,父節點的值小於等於左右孩子的值。這就是所謂的 堆屬性 ,並且這個屬性對堆中的每一個節點都成立
注意:堆屬性只限制了父節點與其左右孩子的大小關係,並沒有限制左右孩子之間的大小關係
我們看個例子
上圖中父節點有兩個:9 和 5,9 比 5 和 7 都大,5 比 3 和 2 都大,滿足大頂堆的屬性,所以它是一個大頂堆
上圖中父節點有兩個:2 和 5,2 比 5 和 3 都小,5 比 7 和 9 都小,滿足小頂堆的屬性,所以它是一個小頂堆
由此我們可以得出:大頂堆的根節點存放的肯定是最大值,小頂堆的根節點存放的肯定是最小值
大頂堆能夠快速得到最大值、小頂堆能夠快速得到最小值,但也就僅此而已了。堆中其他節點的順序是未知的,大頂堆中不能確定最小值,小頂堆中不能確定最大值
陣列如何實現完全二叉樹
用陣列來實現完全二叉樹,是不是感覺很怪?常規的樹的節點由 資料+指向孩子節點的指標 組成,陣列如何表現 指向孩子節點的指標?
怪不代表不能,不僅能實現,而且在時間和空間上還很高效
我們以前面的大頂堆示例為例,通過陣列這樣儲存: [9, 5, 7, 3, 2] ,僅此而已,不需要任何額外的空間!
那麼關鍵問題來了,既然沒有使用指標,那麼如何確定某個節點的父節點以及子節點了?答案就是: 索引對映
假設某個節點的索引是 i,那麼它的父節點和子節點在陣列中的位置可以通過如下公式獲取
注意看左右孩子的公式,不難得出:某個節點的左右孩子處於相鄰位置
我們將公式放到大頂堆示例中驗證一下
完美契合,只是需要注意下索引的有效性
堆與二叉搜尋樹的區別
從定義上來講,堆和二叉搜尋樹還是有區別的,所以堆並不能取代二叉搜尋樹
相似點就不梳理了,我們重點來看下它們的區別
節點順序。二叉搜尋樹中,左孩子必須比父節點小,右孩子必須比父節點大。但是堆中並非如此,堆中只需要保證父節點比左右孩子都大(小)
記憶體佔用。二叉搜尋樹除了需要儲存資料,還需要儲存指向左右孩子的的指標。但堆僅用一個陣列來儲存資料,而不使用指標
平衡。二叉搜尋樹在平衡的情況下,其大部分操作的時間複雜度是 O(log N) ,非平衡的極端情況下,二叉搜尋樹退化成一個連結串列,大部分操作的時間複雜度是 O(N)
堆就是陣列實現的完全二叉樹,完全二叉樹就是平衡二叉樹,所以堆肯定是平衡的
搜尋。二叉搜尋樹本身就是為搜尋而生,所以其搜尋很快。而堆的目的是快速找到最大(小)節點,所以其搜尋會很慢
堆操作
有兩個原始操作: shiftUp 和 shiftDown 用於保證插入或刪除節點後,堆仍然是一個有效的大頂堆或者小頂堆
上移 → shiftUp
在位置 k 處插入元素 x,將 x 逐層往根上移動,直至滿足堆屬性(仍是大頂堆或小頂堆)
假設初始大頂堆如下:
我們以它為例,來看兩種情況
1、新插入元素:6,插入位置索引:5
索引 5 的父位置索引是 2,那麼元素 6 的父元素是 7,7 比 6 大,仍是大頂堆,滿足堆屬性,操作完成
此時大頂堆如下
2、新插入元素:10,插入位置索引:5
索引 5 的父位置索引是 2,那麼元素 10 的父元素是 7,7 比 10 小,不滿足堆屬性,元素 10 逐層往上移動,如下圖
小頂堆是一樣的處理方式,只是比較方式不一樣而已,就不具體演示了
我們再來看下具體的程式碼實現
實現相容了 自然比較器 和 自定義比較器 兩種情況, 自然比較器 預設是升序排序
比較器 升序對應的是小頂堆,降序對應的是大頂堆
下移 → shiftDown
在位置 k 處插入元素 x,將 x 逐層往葉子上移動(下移),直至滿足堆屬性(仍然是大頂堆或小頂堆)。整個操作也稱作 堆化(heapify)
假設大頂堆如下:
我們以它為例,來看看一個例子
假設我們需要將根節點 9 替換成 1,操作步驟是怎樣的?
將 9 替換成 1 後,不滿足大頂堆屬性,需要調整,將節點 1 逐層向下移動,直至滿足堆屬性,如下所示
1、節點 1 在根節點的時候,取它的孩子節點中的大者(7) 與自身交換
2、節點 1 在索引為 1 的位置的時候,取它的孩子節點中的大者(3) 與自身交換
3、節點 1 來到葉子節點,操作完成
我們再來看看程式碼實現
基於 shiftUp 和 shiftDown ,還有很多其他的操作,我們慢慢往下看
insert
在堆的末尾新增一個新的元素,然後用 shiftUp 修復堆;程式碼如下
peek
獲取根元素;如果是大頂堆則是獲取最大值,如果是小頂堆,則是獲取最小值
indexOf
查詢元素的位置索引
因為堆不是為了快速查詢而建立的,所以其時間複雜度是 O(N)
remove & removeAt
remove 是刪除元素。為了將這個節點刪除後的空位填補上,需要將最後一個元素移到根節點的位置,然後使用 shiftDown 方法來修復堆
removeAt 是刪除指定位置的節點。將最後一個元素移到此位置,當它與子節點比較發現無序使用 shiftDown ,如果與父節點比較發現無序則使用 shiftUp
replace
將指定位置的元素替換成目標元素;當它與子節點比較發現無序使用 shiftDown ,如果與父節點比較發現無序則使用 shiftUp
buildHeap
構建初始堆,迴圈呼叫 insert 即可
使用場景
堆排序
這個可以說是大家最容易想到的堆的使用場景
過程如下:
1、以 0 ~ arr.length-1 元素進行堆化,那麼 arr[0] 就是最大值(大頂堆)或最小值(小頂堆),然後將 arr[length-1] 與 arr[0] 進行交換
2、以 0 ~ arr.length-2 元素進行堆化,那麼 arr[0] 就是最大值(大頂堆)或最小值(小頂堆),然後將 arr[length-2] 與 arr[0] 進行交換
3、以此類推,直至整個陣列有序
如果是大頂堆,那麼則是升序;如果是小頂堆,則是降序
以降序為例,我們來看下程式碼實現
優先佇列
優先佇列的底層實現就是:堆,有興趣的小夥伴可以去看看你們的開發語言中優先佇列的底層實現
Java 中是 PriorityQueue ,只要你們去看它的原始碼,你們就會發現我上述 堆操作 的程式碼實現和 PriorityQueue 的基本一致,你們懂的: 拿來主義
獲取極值
快速得到最大值或最小值;這是由堆屬性決定的,我們就不重複講了
處理大資料量的 topN 問題,比如磁碟資料檔案 10G,記憶體卻只有 1G,如何統計出前 100 大的資料?
可以利用小頂堆:每次讀取一個數與堆頂進行比較,若比堆頂大,則把堆頂彈出,把當前資料壓入堆頂,然後調整小頂堆( shiftDown ),最終得到的小頂堆即為最大的100條資料
提升逼格
雖然很虛,也很飄,但真的提升逼格,面試的時候還真有用!
總結
堆屬性
只強調了父節點與左右孩子節點的大小關係,並未要求左右孩子節點的大小關係
所以堆不是有序的,查詢的時間複雜度 O(N)
堆操作
重點是上移操作 shiftUp 與下移操作 shiftDown ,其他操作都是基於這兩個操作
使用場景
堆排序
優先佇列
獲取極值