观看本系列博文提醒:
- 你将学会堆的原理 和 算法实现;
- 一个企业级应用:堆实现优先队列;
- 还有堆排序;
- 最后还有一道检测是否掌握堆算法的作业。
堆的原理精讲
堆是算法中一种特别的树状数据结构,堆是一棵完全二叉树。但他又和二叉树有区别。
(不懂二叉树?没关系,下面我也会有简单的介绍,在后续的博文中会有详细讲解,敬请期待!)
什么是二叉树呢?
如上图就是一个简单的二叉树。也是一个堆。对就是基于二叉树实现的。
上图中,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;
}
运行截图:
好了到了这里,堆的原理 和 算法实现已经差不多讲完了,不知道大家有没有学到呢?
堆是很抽象的,所以请大家耐心慢慢看,慢慢理解。
由于一篇博文太长了,所以后续的企业级应用和作业我将留到后续的博文中讲解!
自此!