C/C++ 入門核心算法大局觀:堆

觀看本系列博文提醒:

  1. 你將學會堆的原理算法實現
  2. 一個企業級應用:堆實現優先隊列
  3. 還有堆排序
  4. 最後還有一道檢測是否掌握堆算法作業

堆的原理精講

堆是算法中一種特別的樹狀數據結構,堆是一棵完全二叉樹。但他又和二叉樹有區別。

(不懂二叉樹?沒關係,下面我也會有簡單的介紹,在後續的博文中會有詳細講解,敬請期待!)

什麼是二叉樹呢?
在這裏插入圖片描述
如上圖就是一個簡單的二叉樹。也是一個堆。對就是基於二叉樹實現的。

上圖中,95就是二叉樹的,而93,87,82,78等,是二叉樹的,而最後一行灰色的數字是二叉樹的

何爲二叉樹,即他的根或者莖最多只能有兩個子節點。且只有一個根

好了,二叉樹就先瞭解到這裏,下面我們開始講一下堆的原理。


堆分爲最大堆最小堆

最大堆的特點:
1. 每個節點最多可以有兩個節點;
2. 根節點的鍵值是所有堆節點鍵值中最大者,且每個節點的值都比起孩子的值要大;
3. 除了根節點沒有兄弟節點,最後一個左子節點可以沒有兄弟節點,其他節點必須有兄弟節點。
在這裏插入圖片描述
如上圖中,圖一和圖二是最大堆,而圖三和圖四不是最大堆。因爲他不符合上面三條最大堆的原理。

最小堆和最大堆的原理是相反的,所以我們這裏都是以最大堆爲例解說。

看圖識堆:
在這裏插入圖片描述


堆是你見過的最有個性的樹!它是用數組表示的樹。

i的左子節點:2 * i + 1
i的右子節點:2 * i + 2
i的父節點:(i - 1) / 2

請記住這三條公式,再堆的算法實現中用經常用到!

爲什麼說堆是最後個性的數?請看下圖。
在這裏插入圖片描述
堆的存儲方式完全都可以使用數組的方式。
這裏更有利於我們操作堆,既可以得到最大的數目,也可以再最大數目出堆時,更快速的找到第二大的數目。
這也是最大堆爲什麼要迎合上面那三條原理的原因,如果最大堆不符合上面那三條原理,那麼我們那些公式就無法使用了。(如果不信,大家可以使用公式計算一下,i 是下標)


到這裏興許大家已經對堆有了一定的瞭解,那麼該如何在一堆混亂的數據中建堆呢?如下圖。
在這裏插入圖片描述
如何從灰色的二叉樹中變成堆呢?

  1. 首先我們需要找到最後一個結點的父結點如圖(a),我們找到的結點是 87,然後找出該結點的最大子節點與自己比較,若該子節點比自身大,則將兩個結點交換. 圖(a)中,87 比左子節點 95 小,則交換之.如圖(b)所示
    在這裏插入圖片描述
    這裏呢95沒有兄弟節點,所以可以直接與87進行比較,如果有的話,得先和兄弟節點比較,選出最大的節點再與父節點比較。

  2. .我們移動到前一個父結點 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 插入到上面大頂堆中的過程如下:

  1. 原始的堆,如圖 a
    在這裏插入圖片描述
    對應的數組:{95, 93, 87, 92, 86, 82}

  2. 將新進的元素插入到大頂堆的尾部,如下圖 b 所示:
    在這裏插入圖片描述
    對應的數組:{95, 93, 87, 92, 86, 82, 99}

  3. 此時最大堆已經被破壞,需要重新調整, 因加入的節點比父節點大,則新節點跟父節點調換即可,如圖 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;
}

運行截圖:
在這裏插入圖片描述


好了到了這裏,堆的原理算法實現已經差不多講完了,不知道大家有沒有學到呢?
堆是很抽象的,所以請大家耐心慢慢看,慢慢理解。

由於一篇博文太長了,所以後續的企業級應用和作業我將留到後續的博文中講解!

自此!

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