在前面分析了二叉搜索樹
、紅黑樹
等衆多樹結構
,今天博主給大家換個口味,深入分析一下堆
的實現原理與維護規則。(堆
其實與二叉樹
有點相似)
一、堆
的概述
1、什麼是堆
堆
,在日常生活中是一個量詞,比如:一堆木頭
,下面這張圖就是一堆木頭
,大家注意它的擺放規則,成金字塔型
(上窄下寬)。
數據結構中的堆
,與上面的結構類似,不過藉助了二叉樹
的結構。肯定會有小夥伴來一句,woc,這不就是二叉樹
麼。。。其實你也可以暫時這麼理解,看完博客你就會發現還是有些區別。
2、堆
的劃分
根據堆
中元素擺放順序的不同,可分爲兩種堆結構,小頂堆
、大頂堆
。
小頂堆
:對於堆中的任意節點A
,如果它存在子節點B、C,則 節點A
的值 ≤ min{子節點B
的值, 子節點C
的值}
大頂堆
:對於堆中的任意節點A
,如果它存在子節點B、C,則 節點A
的值 ≥ max{子節點B
的值, 子節點C
的值}
堆只對父節點
的值與子節點
的值有大小限定,但是對於節點
的左、右子節點
的值相對關係沒有限定(二叉搜索樹
的特徵纔是 左子節點
< 父
< 右子節點
)。
3、堆
的作用
講了半天,也畫了好幾張圖,辣麼堆
有什麼作用呢?
根據前面小頂堆
、大頂堆
的定義,我們知道小頂堆
的堆頂
存放的是堆中最小
的元素,大頂堆
的堆頂
存放的堆中最大
的元素。而這正是堆
的作用,可能會有小夥伴一臉鄙夷,就這找最值的功能都要特意設計一個數據結構
來實現?
那來一道面試題,給你10億
個數,如何在最短的時間裏找出最大的10
個? (我覺得如果沒做出這道題,辣麼只能說了解堆的定義,但是不理解、不會運用堆
,邊看博客,邊思考吧,文末附答案)
二、堆
的底層實現
堆
的實現一般使用數組
,而不是二叉樹
什麼的。從形態來看,堆不就是棵二叉樹
麼,爲啥不用二叉樹
而用數組
呢?主要原因是爲了隨機訪問
(通過下標訪問元素
)。本篇博客將只討論使用數組
實現的堆
,如果你偏要用二叉樹
,可以自己實現一個。
以數組
實現堆
不僅帶來了隨機訪問
(通過下標訪問元素
),其實還有兩個很重要的規律
。
- 下標爲
index
的左、右孩子的下標分別是index * 2 + 1
、(index + 1) * 2
- 堆中有子節點的節點最大下標爲
size / 2 - 1
(注意:size
爲堆的大小,不是數組的大小)
三、堆
的維護
首先說明一下,堆
一般對外展示的只有堆頂
,也就是說堆
的插入
、移除
,在外界看來都是在堆頂
操作。(下面的維護都是基於小頂堆
,大頂堆
的維護操作是類似的。)
1、擴容
如果數組
沒有剩餘空位置,此時需要對數組
進行擴容
。由於基於數組
實現的堆
只有數組
這個結構,堆
只是通過數組下標
在邏輯
上存在,所以只要將數組
中的元素按原來的順序複製到一個更長的數組
即可。
2、插入
元素(上浮)
對於插入操作,我們首先將元素放到下一個空閒
的位置,然後對插入的元素進行上浮
操作。
插入元素上浮僞代碼如下:
// data是堆數組,size是堆的大小(並不是data數組的長度),element是待插入的節點
void insert(int data[], int &size, int element) {
// 假設data有空位置插入,將待插入元素直接放到堆尾的下一個空位置
int insertIndex = size++;
data[insertIndex] = element;
// 上浮插入節點
while (insertIndex > 0) {
// 求出父節點的下標(根據父節點下標能求出左右子節點下標,那麼根據子節點下標同樣也能求出父節點下標)
int parentIndex = (insertIndex - 1) / 2;
// 如果插入的元素大於父節點
if (data[insertIndex] > data[parentIndex]) {
// 上浮完畢
break;
}
// 否則插入的元素小於父節點元素的值,交換
swap(data[insertIndex], data[parentIndex]);
// 更新插入元素的下標,繼續上浮
insertIndex = parentIndex;
}
}
3、刪除
(堆頂)元素(下沉)
插入元素是將尾端
元素上浮
,而刪除
堆頂元素,是將堆尾
元素放入堆頂,然後將該元素下沉
。
刪除元素下沉僞代碼如下:
// data是堆數組,size是堆的大小(並不是data數組的長度)
void deleteTop(int data[], int &size) {
if (size == 0) {
// 堆爲空,無需進行刪除操作
return;
}
// 將堆尾替換堆頂
data[0] = data[--size];
// 記錄需要下沉節點的下標,尋找最大的有子節點的下標(前說過這是一條規律)
int index = 0, lastHaveChildIndex = size / 2 - 1;
// 只要當data[index]存在子節點纔有下沉的必要
while (index >= lastHaveChildIndex) {
// 右子節點必定有左子節點(左子節點的公式: 父節點下標 * 2 + 1)
// minChildIndex用於記錄左、右子節點更小的下標
int minChildIndex = index * 2 + 1, rightChildIndex;
if ((rightChildIndex = index * 2 + 2) < size && data[minChildIndex] > data[rightChildIndex]) {
// 如果index存在右子節點,並且右子節點的值比左子節點的值小,更新minChildIndex
minChildIndex = rightChildIndex;
}
// 如果下沉節點data[index]比左右子節點最小值都大,停止下沉
if (data[index] >= data[minChildIndex]){
break;
}
// 否則與左、右子節點較小則交換,並且更新index,繼續下沉
swap(data[index], data[minChildIndex]);
index = minChildIndex;
}
}
四、總結
堆中插入
元素,直接放入數組的下一個空位置
,然後上浮
插入元素,即可完成堆的調整;刪除堆頂
元素,將堆尾
的元素替換堆頂
,再對堆頂的元素進行下沉,即可完成堆的調整。(再次強調一下,基於數組
實現的堆
,只有數組
這個結構,堆
只是根據數組下標
的依賴關係在邏輯
上存在。如果你將堆的實現修改爲二叉樹
,那麼堆
纔是真正存在。)
現在提下前面提出的面試題的答案,給你10億
個數,如何在最短的時間裏找出最大的10
個?
我們只要構建一個大小爲10的小頂堆
,前期取10個數直接插入堆中,然後對剩下的10億-10
個數,每此取出一個都與堆頂
(堆中最小值)比較,如果比堆頂
大,則替換堆頂,接着調整堆(下沉堆頂元素
即可),最終堆中的元素就是10億
個數中最大的10
個數。(如果答案都沒看懂,再看一遍博客吧,有點走馬觀花哦)