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;
}

运行截图:
在这里插入图片描述


好了到了这里,堆的原理算法实现已经差不多讲完了,不知道大家有没有学到呢?
堆是很抽象的,所以请大家耐心慢慢看,慢慢理解。

由于一篇博文太长了,所以后续的企业级应用和作业我将留到后续的博文中讲解!

自此!

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cpp_learners

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值