【堆】数据结构-堆的实现【超详细的数据结构教学】

导航小助手

  • 博主给大家的话
  • 什么是二叉树-堆
  • 堆的实现
    • 堆的结构
    • 要实现的接口
    • 堆的初始化接口
    • 堆的销毁接口
    • 堆的打印接口
    • 取堆顶数据,判空,返回堆大小接口
    • 插入元素接口(重点核心)
      • 双亲节点和孩子节点之间的关系
      • 向上调整算法
      • 插入接口完整代码
    • 删除元素接口(重点核心)
      • 双亲节点和孩子节点之间的关系
      • 向下调整算法
      • 删除接口完整代码
    • 向下调整和向上调整的时间复杂度
  • 尾声

博主给大家的话

【堆】数据结构-堆的实现【超详细的数据结构教学】_第1张图片

那么这里博主先安利一下一些干货满满的专栏啦!

数据结构专栏:手撕数据结构 这里包含了博主很多的数据结构学习上的总结,每一篇都是超级用心编写的,有兴趣的伙伴们都支持一下吧!
算法专栏:算法 这里可以说是博主的刷题历程,里面总结了一些经典的力扣上的题目,和算法实现的总结,对考试和竞赛都是很有帮助的!
力扣刷题专栏:跟着博主刷Leetcode 想要冲击ACM、蓝桥杯或者大学生程序设计竞赛的伙伴,这里面都是博主的刷题记录,希望对你们有帮助!
C的深度解剖专栏:C语言的深度解剖 想要深度学习C语言里面所蕴含的各种智慧,各种功能的底层实现的初学者们,相信这个专栏对你们会有帮助的!

什么是二叉树-堆

是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:

  • 堆中某个结点的值总是不大于或不小于其父结点的值;
  • 堆总是一棵完全二叉树。

将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。

当然,这些都是二叉树的相关概念,二叉树的表示常常有链式或者用数组模拟。因为堆是完全二叉树,可以用数组模拟堆,即(逻辑结构上是二叉树,物理结构上是一个数组)
这里博主给大家提供二叉树相关概念顺序表的基本操作的传送门供大家食用!

  • 【二叉树】树是什么?-树和二叉树的基本必会概念讲解
  • 【顺序表】数据结构顺序表基本操作(增删查改)【保姆级别详解】

堆的实现

堆的结构

因为,我们这里的堆用顺序表模拟,所以这里的底层结构其实就是顺序表结构。

typedef int HPDataType;
typedef struct Heap {
	HPDataType* a;
	int size;
	int capacity;
}Heap;

要实现的接口

  • 打印堆
  • 初始化堆
  • 销毁堆
  • push
  • pop
  • 取堆顶元素
  • 判空
  • 返回堆的大小
  • 向上调整算法(核心)
  • 向下调整算法(核心)

堆的初始化接口

其实就是一个顺序表的初始化,很简单。

void _HeapInit(Heap* php) {
	assert(php);
	php->a = nullptr;//一开始先置成空指针
	php->size = php->capacity = 0;//容量和大小都是0
}

堆的销毁接口

注意:释放之后,一定要记得处理野指针!

void _HeapDestroy(Heap* php) {
	assert(php);
	free(php->a);
	php->a = nullptr;
	php->size = php->capacity = 0;
}

堆的打印接口

其实就是一个数组的遍历,我们用for循环控制即可。

void _HeapPrint(Heap* php) {
	assert(php);
	for (int i = 0; i < php->size; i++) {
		cout<<php->a[i]<<" ";
	}
	cout << endl;
}

取堆顶数据,判空,返回堆大小接口

//返回堆顶的元素
HPDataType _HeapTop(Heap* php) {
	assert(php);
	assert(php->size > 0);
	return php->a[0];//堆顶就是数组的第一个位置元素
}
//判断堆是否为空
bool _HeapEmpty(Heap* php) {
	assert(php);
	return php->size == 0;//看size是不是0即可
}
//返回堆的大小
int _HeapSize(Heap* php) {
	assert(php);
	return php->size;
}

插入元素接口(重点核心)

其实上面那些接口,对于学了线性数据结构的我们来说,其实是非常简单的,其实只要弄清什么是堆,堆的实现思路,自己写出上面那些接口应该是小菜一碟的。

现在,让我们来实现堆的一个重要接口-插入元素。

我们插入元素,一般是直接往数组的最后一个位置插入元素,当然,如果这样插入元素,我们是不能保证堆的性质的。
这时,有些伙伴们就会说,我们可以找到合适的插入位置插入元素,但是我们知道,顺序表的中间位置插入和删除,效率都是很低的,这样插入,无法利用到堆的优势。

那么,堆到底如何插入元素呢?

为了效率,我们直接往最后一个位置插入元素,这样插入动作是O(1)的,但是,我们还要对这个完全二叉树进行调整,将其变成堆。
这里用到一个操作堆的核心算法:向上调整算法,也可以成为节点上浮。

它的主要思路是,如果插入的元素需要上浮,它只会影响它的一些祖先节点,其它节点是不会影响的。
因此,我们让这个节点和它的双亲节点进行对比,如果这个节点更小(小堆为例),交换这两个节点的值,循环往复,直到双亲节点大于本节点为止。

双亲节点和孩子节点之间的关系

   leftchild=parent*2+1
   rightchild=parent*2+2
   parent=(child-1)/2

向上调整算法

void _AdjustUp(HPDataType* a, int child) {
	int parent = (child - 1) / 2;
	//这个循环条件的终止条件要注意
	while (child > 0) {
		if (a[child] < a[parent]) {
			swap(a[child], a[parent]);
			//把父亲的下标给孩子
			child = parent;
			parent = (child - 1) / 2;
		}
		//此时直接break
		else {
			break;
		}
	}
}

插入接口完整代码

//向上调整算法
void _AdjustUp(HPDataType* a, int child) {
	int parent = (child - 1) / 2;
	//这个循环条件的终止条件要注意
	while (child > 0) {
		if (a[child] < a[parent]) {
			swap(a[child], a[parent]);
			//把双亲的下标给孩子
			child = parent;
			parent = (child - 1) / 2;
		}
		//此时直接break
		else {
			break;
		}
	}
}
void _HeapPush(Heap* php, HPDataType x) {
	assert(php);
	//扩容
	if (php->size == php->capacity) {
		int _newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, _newCapacity * sizeof(HPDataType));
		if (tmp == nullptr) {
			perror("_HeapPush::realloc");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = _newCapacity;
	}
	php->a[php->size] = x;
	php->size++;
	//向上调整算法
	_AdjustUp(php->a, php->size - 1);
}

删除元素接口(重点核心)

删除元素,我们pop掉的是数组第一个元素,但是此时,我们如果还要维持堆的性质,我们将采用一种很巧妙的方式pop掉堆顶的元素:将其与数组最后一个元素进行交换,size--,这不就相当于pop掉最上面的元素了吗?

当然,此时,交换之后,这棵完全二叉树也不符合堆的性质了,但是,我们发现,这棵树的根节点的左子树和右子树都没变,都是堆。
所以,我们将根节点元素向下调整。
核心算法:向下调整算法,也就是元素下沉。

  • 思路:找到两个孩子节点中小的那个(小堆为例),比较,如果当前节点较大,下沉即可

双亲节点和孩子节点之间的关系

   leftchild=parent*2+1
   rightchild=parent*2+2
   parent=(child-1)/2

向下调整算法

//向下调整算法
void _AdjustDown(HPDataType* a, int size, int parent) {
	int child = parent * 2 + 1;
	while (child < size) {
		//选出小的那个孩子节点
		if (child + 1 < size && a[child + 1] < a[child]) {
			++child;
		}
		if (a[child] < a[parent]) {
			//向下调整
			swap(a[child], a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else {
			//无需调整
			break;
		}
	}
}

删除接口完整代码

//向下调整算法
void _AdjustDown(HPDataType* a, int size, int parent) {
	int child = parent * 2 + 1;
	while (child < size) {
		//选出小的那个孩子节点
		if (child + 1 < size && a[child + 1] < a[child]) {
			++child;
		}
		if (a[child] < a[parent]) {
			//向下调整
			swap(a[child], a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else {
			//无需调整
			break;
		}
	}
}
void _HeapPop(Heap* php) {
	//1.把头和尾交换一下
	//2.向下调整
	assert(php);
	assert(php->size > 0);
	swap(php->a[0], php->a[php->size - 1]);
	php->size--;
	//向下调整算法
	_AdjustDown(php->a, php->size, 0);
}

向下调整和向上调整的时间复杂度

向调整算法 向下调整算法
O(Nlog(N)) O(n)

这也是为什么,堆排序中是用向下调整算法的,因为快!

有关堆排序heapSort的相关内容,博主提供传送门了,供大家食用!
【排序】万字九大排序宝藏汇总 轻松拿下九大排序算法【带动画】 (包含超详细的解释和注释)

尾声

看到这里,相信大家对堆这个数据结构有了了解了。
如果你感觉这篇博客对你有帮助,不要忘了一键三连哦!

你可能感兴趣的