觀看本系列博文提醒:
- 你將學會堆的原理 和 算法實現;
- 一個企業級應用:堆實現優先隊列;
- 還有堆排序;
- 最後還有一道檢測是否掌握堆算法的作業。
堆的原理精講
堆是算法中一種特別的樹狀數據結構,堆是一棵完全二叉樹。但他又和二叉樹有區別。
(不懂二叉樹?沒關係,下面我也會有簡單的介紹,在後續的博文中會有詳細講解,敬請期待!)
什麼是二叉樹呢?
如上圖就是一個簡單的二叉樹。也是一個堆。對就是基於二叉樹實現的。
上圖中,95就是二叉樹的根,而93,87,82,78等,是二叉樹的莖,而最後一行灰色的數字是二叉樹的葉。
何爲二叉樹,即他的根或者莖最多只能有兩個子節點。且只有一個根。
好了,二叉樹就先瞭解到這裏,下面我們開始講一下堆的原理。
堆分爲最大堆和最小堆。
最大堆的特點:
1. 每個節點最多可以有兩個節點;
2. 根節點的鍵值是所有堆節點鍵值中最大者,且每個節點的值都比起孩子的值要大;
3. 除了根節點沒有兄弟節點,最後一個左子節點可以沒有兄弟節點,其他節點必須有兄弟節點。
如上圖中,圖一和圖二是最大堆,而圖三和圖四不是最大堆。因爲他不符合上面三條最大堆的原理。
最小堆和最大堆的原理是相反的,所以我們這裏都是以最大堆爲例解說。
看圖識堆:
堆是你見過的最有個性的樹!它是用數組表示的樹。
i的左子節點:2 * i + 1
i的右子節點:2 * i + 2
i的父節點:(i - 1) / 2
請記住這三條公式,再堆的算法實現中用經常用到!
爲什麼說堆是最後個性的數?請看下圖。
堆的存儲方式完全都可以使用數組的方式。
這裏更有利於我們操作堆,既可以得到最大的數目,也可以再最大數目出堆時,更快速的找到第二大的數目。
這也是最大堆爲什麼要迎合上面那三條原理的原因,如果最大堆不符合上面那三條原理,那麼我們那些公式就無法使用了。(如果不信,大家可以使用公式計算一下,i 是下標)
到這裏興許大家已經對堆有了一定的瞭解,那麼該如何在一堆混亂的數據中建堆呢?如下圖。
如何從灰色的二叉樹中變成堆呢?
-
首先我們需要找到最後一個結點的父結點如圖(a),我們找到的結點是 87,然後找出該結點的最大子節點與自己比較,若該子節點比自身大,則將兩個結點交換. 圖(a)中,87 比左子節點 95 小,則交換之.如圖(b)所示
這裏呢95沒有兄弟節點,所以可以直接與87進行比較,如果有的話,得先和兄弟節點比較,選出最大的節點再與父節點比較。 -
.我們移動到前一個父結點 93,如圖©所示.同理做第一步的比較操作,結果不需要交換。
3.繼續移動結點到前一個父結點 82,如圖(d)所示,82 小於右子節點 95,則 82 與 95 交換,如圖(e)所示,82 交換後,其值小於左子節點,不符合最大堆的特點,故需要繼續向下調整,如圖(f)所示
4.所有節點交換完畢,最大堆構建完成。
好了建堆的原理講完了,我們來看一下代碼是怎麼樣實現的吧。
首先我們得先定義堆:
涉及到堆得長度,我們還得定義一個宏
#define DEFAULT_CAPCITY 128
typedef struct _Heap {
int* arr; // 存儲堆元素的數組
int size; // 當前已存儲的元素個數
int capacity; // 當前以存儲的容量
}Heap;
之後呢?我們還需要定義幾個函數來完成堆的初始化和建堆的工作。
bool initHeap(Heap &heap, int *orginal, int size); // 初始化堆
static void buildHeap(Heap &heap); // 建堆
static void adjustDown(Heap &heap, int index); // 父莖的下移
函數的具體實現:
bool initHeap(Heap &heap, int *orginal, int size) { // 參數二:待成堆的數組;參數三:數組的個數
int capacity = DEFAULT_CAPCITY > size ? DEFAULT_CAPCITY : size; // 確定堆元素的存儲容量
heap.arr = new int[capacity]; // 分配內存
if (!heap.arr) return false; // 如果內存分配失敗
heap.capacity = capacity; // 數據賦值
heap.size = 0;
if (size > 0) { // 判斷數組是否有數據
memcpy(heap.arr, orginal, size * sizeof(int)); // 內存拷貝
heap.size = size;
buildHeap(heap); // 建堆
}
return true;
}
/* 從最後一個父節點(size/2-1的位置)逐個往前調整所有父節點(直到根節點),
確保每一個父節點都是要給最大堆,最後整體上形成一個最大堆 */
void buildHeap(Heap &heap) {
for (int i = heap.size / 2 - 1; i >= 0; i--) {
adjustDown(heap, i); // 堆裏面的數據排序
}
}
// 將當前的節點和子節點調整成最大堆
void adjustDown(Heap &heap, int index) { // 參數二:待調整的父節點下標
int cur = heap.arr[index]; // 將待調整節點賦值給cur
int parent, child; // parent:充當父節點的下標;child:充當左或右子節點的下標
/* 判斷是否存在大於當前節點的子節點,如果不存在,則堆本身是平衡的,不需要調整;
如果存在,則將最大的子節點與之交換,交換後,如果這個子節點還有子節點,則需要繼續
按照同樣的步驟對這個子節點進行調整。*/
for (parent = index; (parent * 2 + 1) < heap.size; parent = child) {
child = parent * 2 + 1; // 計算出當前節點的左子節點的下標
// 取兩個子節點中的最大的節點
if ((child + 1) < heap.size && (heap.arr[child] < heap.arr[child + 1])) {
child += 1; // 不管有沒有右子節點,都可以計算出最大的子節點下標位置
}
// 判斷最大的子節點是否大於當前的父節點
if (cur > heap.arr[child]) {
break;
} else {
// 子節點和父節點交換值
heap.arr[parent] = heap.arr[child];
heap.arr[child] = cur;
}
}
}
代碼中都有詳細的註釋解析。
堆建好後,我們得試試插入元素了。
將數字 99 插入到上面大頂堆中的過程如下:
-
原始的堆,如圖 a
對應的數組:{95, 93, 87, 92, 86, 82} -
將新進的元素插入到大頂堆的尾部,如下圖 b 所示:
對應的數組:{95, 93, 87, 92, 86, 82, 99} -
此時最大堆已經被破壞,需要重新調整, 因加入的節點比父節點大,則新節點跟父節點調換即可,如圖 c所示;調整後,新節點如果比新的父節點小,則已經調整到位,如果比新的父節點大,則需要和父節點重新進
行交換,如圖 d, 至此,最大堆調整完成。
這樣就完成了堆元素的插入了。
原理也講完了,我們來看一下代碼實現:
同樣,需要建立幾個實現的函數。
bool insert(Heap& heap, int value); // 插入元素
static void adjustUp(Heap& heap, int index); // 父莖的上移
具體函數實現:
// 將當前的節點和父節點調整成最大堆
void adjustUp(Heap& heap, int index) {
if (index < 0 || index >= heap.size) {
cout << "index參數不合法!" << endl;
return;
}
while (index > 0) {
int temp = heap.arr[index];
int parent = (index - 1) / 2; // 計算出父節點下標
if (parent >= 0) { // 如果索引沒有出界就執行想要的操作
if (temp > heap.arr[parent]) {
heap.arr[index] = heap.arr[parent];
heap.arr[parent] = temp;
index = parent; // 交換後,index下標被更新
} else { // 如果已經比父親小,直接結束循環
break;
}
} else {
break;
}
}
}
// 最大堆尾部插入節點,同時保證最大堆的特性
bool insert(Heap& heap, int value) { // 參數二:待插入的值
if (heap.size == heap.capacity) {
cout << "堆以滿!" << endl;
return false;
}
int index = heap.size;
heap.arr[heap.size] = value; // 將元素插入堆的最後一個位置
heap.size += 1;
adjustUp(heap, index); // 實現插入元素的上移
}
代碼中都有詳細註釋,請耐心觀看。
我們已經知道該怎麼插入元素了,那麼該如何刪除堆頂的元素呢?
如果我們將堆頂的元素刪除,那麼頂部有一個空的節點,怎麼處理?
處理方法很簡單:
當插入節點的時候,我們將新的值插入數組的尾部。現在我們來做相反的事情:我們取出數組中的最後一個元素,將它放到堆的頂部,然後再修復堆屬性。
替換後,我們只需要使用初始化堆時用過的函數來將堆重新排序一遍就行了。
我們需要使用到的新函數:
bool popHeap(Heap &heap, int &value); // 刪除最大的元素
還有前面已經定義好的函數:
static void adjustDown(Heap &heap, int index); // 父莖的下移
函數實現:
// 堆中最大元素出堆
bool popHeap(Heap &heap, int &value) {
if (heap.size < 1) {
cout << "堆爲空!" << endl;
return false;
}
value = heap.arr[0];
heap.arr[0] = heap.arr[heap.size - 1]; // 將最後一個值覆蓋第一個值
heap.size -= 1; // 長度減一
adjustDown(heap, 0); // 重新調整最大堆
return true;
}
調整完成後,又是一個全新的最大堆。
全部代碼:
#include <iostream>
#include <Windows.h>
using namespace std;
#define DEFAULT_CAPCITY 128
typedef struct _Heap {
int* arr; // 存儲堆元素的數組
int size; // 當前已存儲的元素個數
int capacity; // 當前以存儲的容量
}Heap;
bool initHeap(Heap &heap, int *orginal, int size); // 初始化堆
static void buildHeap(Heap &heap); // 建堆
static void adjustDown(Heap &heap, int index); // 父莖的下移
bool insert(Heap& heap, int value); // 插入元素
static void adjustUp(Heap& heap, int index); // 父莖的上移
bool popHeap(Heap &heap, int &value); // 刪除最大的元素
bool initHeap(Heap &heap, int *orginal, int size) { // 參數二:待成堆的數組;參數三:數組的個數
int capacity = DEFAULT_CAPCITY > size ? DEFAULT_CAPCITY : size; // 確定堆元素的存儲容量
heap.arr = new int[capacity]; // 分配內存
if (!heap.arr) return false; // 如果內存分配失敗
heap.capacity = capacity; // 數據賦值
heap.size = 0;
// 方式二
if (size > 0) { // 判斷數組是否有數據
memcpy(heap.arr, orginal, size * sizeof(int)); // 內存拷貝
heap.size = size;
buildHeap(heap); // 建堆
}
// 方式一
/*for (int i = 0; i < size; i++) {
insert(heap, orginal[i]);
}*/
return true;
}
/* 從最後一個父節點(size/2-1的位置)逐個往前調整所有父節點(直到根節點),
確保每一個父節點都是要給最大堆,最後整體上形成一個最大堆 */
void buildHeap(Heap &heap) {
for (int i = heap.size / 2 - 1; i >= 0; i--) {
adjustDown(heap, i); // 堆裏面的數據排序
}
}
// 將當前的節點和子節點調整成最大堆
void adjustDown(Heap &heap, int index) { // 參數二:待調整的父節點下標
int cur = heap.arr[index]; // 將待調整節點賦值給cur
int parent, child; // parent:充當父節點的下標;child:充當左或右子節點的下標
/* 判斷是否存在大於當前節點的子節點,如果不存在,則堆本身是平衡的,不需要調整;
如果存在,則將最大的子節點與之交換,交換後,如果這個子節點還有子節點,則需要繼續
按照同樣的步驟對這個子節點進行調整。*/
for (parent = index; (parent * 2 + 1) < heap.size; parent = child) {
child = parent * 2 + 1; // 計算出當前節點的左子節點的下標
// 取兩個子節點中的最大的節點
if ((child + 1) < heap.size && (heap.arr[child] < heap.arr[child + 1])) {
child += 1; // 不管有沒有右子節點,都可以計算出最大的子節點下標位置
}
// 判斷最大的子節點是否大於當前的父節點
if (cur > heap.arr[child]) {
break;
} else {
// 子節點和父節點交換值
heap.arr[parent] = heap.arr[child];
heap.arr[child] = cur;
}
}
}
// 將當前的節點和父節點調整成最大堆
void adjustUp(Heap& heap, int index) {
if (index < 0 || index >= heap.size) {
cout << "index參數不合法!" << endl;
return;
}
while (index > 0) {
int temp = heap.arr[index];
int parent = (index - 1) / 2; // 計算出父節點下標
if (parent >= 0) { // 如果索引沒有出界就執行想要的操作
if (temp > heap.arr[parent]) {
heap.arr[index] = heap.arr[parent];
heap.arr[parent] = temp;
index = parent; // 交換後,index下標被更新
} else { // 如果已經比父親小,直接結束循環
break;
}
} else {
break;
}
}
}
// 最大堆尾部插入節點,同時保證最大堆的特性
bool insert(Heap& heap, int value) { // 參數二:待插入的值
if (heap.size == heap.capacity) {
cout << "堆以滿!" << endl;
return false;
}
int index = heap.size;
heap.arr[heap.size] = value; // 將元素插入堆的最後一個位置
heap.size += 1;
adjustUp(heap, index); // 實現插入元素的上移
}
// 堆中最大元素出堆
bool popHeap(Heap &heap, int &value) {
if (heap.size < 1) {
cout << "堆爲空!" << endl;
return false;
}
value = heap.arr[0];
heap.arr[0] = heap.arr[heap.size - 1]; // 將最後一個值覆蓋第一個值
heap.size -= 1; // 長度減一
adjustDown(heap, 0); // 重新調整最大堆
return true;
}
int main(void) {
Heap hp;
int origVals[] = { 1, 2, 3, 87, 93, 82, 92, 86, 95 };
if (!initHeap(hp, origVals, sizeof(origVals) / sizeof(origVals[0]))) {
cout << "初始化堆失敗!" << endl;
exit(-1);
}
cout << "初始化堆後:" << endl;
for (int i = 0; i < hp.size; i++) {
cout << hp.arr[i] << " ";
}
cout << endl << endl;
cout << "插入新的元素99後:" << endl;
insert(hp, 99);
for (int i = 0; i < hp.size; i++) {
cout << hp.arr[i] << " ";
}
cout << endl << endl;
// 出堆
cout << "全部元素出堆:" << endl;
while (1) {
int value = 0;
if (popHeap(hp, value)) {
cout << value << ",";
} else {
break;
}
}
system("pause");
return 0;
}
運行截圖:
好了到了這裏,堆的原理 和 算法實現已經差不多講完了,不知道大家有沒有學到呢?
堆是很抽象的,所以請大家耐心慢慢看,慢慢理解。
由於一篇博文太長了,所以後續的企業級應用和作業我將留到後續的博文中講解!
自此!