用数组实现小根堆

用数组实现小根堆

  • [6.4.3 堆的实现](#6.4.3 堆的实现)
    • [6.4.3.1 堆的向上调整算法建堆](#6.4.3.1 堆的向上调整算法建堆)
    • [6.4.3.2 堆的向下调整算法建堆](#6.4.3.2 堆的向下调整算法建堆)
    • [6.4.3.3 建堆时间复杂度分析](#6.4.3.3 建堆时间复杂度分析)
    • [6.4.4 堆的其他操作](#6.4.4 堆的其他操作)
      • [1. 初始化(Init)](#1. 初始化(Init))
      • [2. 销毁(Destroy)](#2. 销毁(Destroy))
      • [3. 插入(Push)](#3. 插入(Push))
      • [4. 弹出堆顶元素(Pop)](#4. 弹出堆顶元素(Pop))
      • [5. 获取堆顶元素(Top)](#5. 获取堆顶元素(Top))
      • [6. 判断堆是否为空(Empty)](#6. 判断堆是否为空(Empty))
      • [7. 获取堆的结点数(Size)](#7. 获取堆的结点数(Size))

6.4.3 堆的实现

数据转换成堆的思路:

  1. 新开辟一个新数组。
  2. 将数组作为完全二叉树,通过调整算法改造成堆,再交换对顶和最后一个元素,并逐步缩小可操作的堆结点数(或范围)。改造过程其实是堆的插入过程。

调整算法有两种:向上调整和向下调整。

堆的实现过程都可以在数组中完成。

堆的基本信息:

c 复制代码
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;

6.4.3.1 堆的向上调整算法建堆

这是一个小堆,分别插入数据80,40和5时,如何将这个数组重新调整使数组依旧是小堆?

当插入数据80时,整体还是小堆,无需调整。

当插入数据40或5时均需做调整。

可以看到,新增的结点在参与调整时都是和父结点做比较 ,当新结点比自己的父结点的数据小时,和父结点做置换 ,自己来充当父结点。

这种调整在自己成了新的根结点时即可停止 。或者说它一直充当父结点,而父结点最终也只会为0,因为(child-1)/2== − 1 2 -\frac{1}{2} −21,因为c语言的整型会舍去小数部分,所以还是0,当child为根结点时生效。所以调整的结束条件child>0

所以我们便有了这样一个向上调整的算法:

c 复制代码
//向上调整算法
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;
		}
		else break;
}

需要注意的是,在进行向上调整时需要保证该结点插入之前所有结点都已经成了堆 。所以在使用这个算法进行调整时可以从二叉树的第二层开始调整 ,此时第二层上方只有1个根结点此时可以看成堆

c 复制代码
void f1() {//测试向上调整算法
	srand((size_t)time(0));//随机数的种子
	int a[10] = { 0 };
	int i = 0;
	for (i = 0; i < 10; i++)
		a[i] = rand() % 100 + 1;//生成完全二叉树
	for (i = 0; i < 10; i++)
		printf("%d ", a[i]);
	printf("\n");
	for (i = 1; i <10; i++)//从第二层开始调整
		adjustUp(a, i);
	for (i = 0; i < 10; i++)
		printf("%d ", a[i]);
}

当我们将除了根结点外的所有结点都进行了向上调整时,会发现这个树变成了我们想要的小堆。

6.4.3.2 堆的向下调整算法建堆

们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整

当被调整的结点成为新的叶结点或结点来到它应该在的位置时,调整完成。

每个结点都至少有2个子结点,选择哪个子结点作为配合调整的对象,取决于哪个结点更符合条件。例如上图是个小堆,所以从15和19两个结点中选择最小的那个结点15。在开始时我们先默认左结点更符合条件,之后再看右结点。

根据上文的大概描述,我们有了向下调整算法:

c 复制代码
//向下调整算法
void adjustDown(HPDataType* a, int n, int parent) {
	int child = 2 * parent + 1;//默认左结点更符合条件
	while (child < n) {
		if (child + 1 < n)//考虑数组容量,或者说右孩子不存在的情况
			if (a[child + 1] < a[child])//选择更合适的结点
				++child;
		if (a[child] < a[parent]) {
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else break;
	}
}

我们看到完全二叉树的倒数第二层,可以看到他们的左、右结点要么为空,要么有一个或两个子结点。这些子结都是叶结点,我们可以看成是堆。

但既然是向下调整,则向下调整的结点必须有子结点。所以我们从倒数第一个父结点开始向下调整

c 复制代码
void f2() {//测试向下调整算法
	srand((size_t)time(0));//随机数的种子
	int a[10] = { 0 };
	int i = 0;
	for (i = 0; i < 10; i++)
		a[i] = rand() % 100 + 1;//生成完全二叉树
	for (i = 0; i < 10; i++)
		printf("%d ", a[i]);
	printf("\n");
	for (i = (10-1-1)/2; i >=0; i--)//从倒数第一个父结点开始调整
		adjustDown(a, 10,i);
	for (i = 0; i < 10; i++)
		printf("%d ", a[i]);
}

6.4.3.3 建堆时间复杂度分析

向上调整算法

完全二叉树的最后一层拥有树快一半的结点,最坏的情况是最后一层要调整的结点刚好是全部,每个结点调整h-1次,于是调整次数为 2 h − 1 ( h − 1 ) 2^{h-1}(h-1) 2h−1(h−1)次(也就是满二叉树并且每个叶结点都要调整),

于是总的调整次数和高度的关系为

F ( h ) = 2 1 × 1 + 2 2 × 2 + ⋯ + 2 h − 1 × ( h − 1 ) F(h)=2^1\times1+2^2\times 2+\cdots+2^{h-1}\times(h-1) F(h)=21×1+22×2+⋯+2h−1×(h−1),

这是一个等差数列和等比数列交叉相乘再相加组成的数列,通过错位相减或公式 ( k n + m ) q n − m ( k = a q − 1 , m = b − k q − 1 ,这个公式是数列 ( a n + b ) q n − 1 的前 n 项和公式 ) (kn+m)q^n-m(k=\frac{a}{q-1},m=\frac{b-k}{q-1},这个公式是数列(an+b)q^{n-1}的前n项和公式) (kn+m)qn−m(k=q−1a,m=q−1b−k,这个公式是数列(an+b)qn−1的前n项和公式)

可得到

F ( h ) = 2 h × h − 2 × 2 h + 2 F(h)=2^h\times h-2\times2^h+2 F(h)=2h×h−2×2h+2,

因为树高 h = l o g 2 ( n + 1 ) h=log_{2}(n+1) h=log2(n+1),

所以 F ( h ) = ( n + 1 ) l o g 2 ( n + 1 ) − 2 n F(h)=(n+1)log_{2}(n+1)-2n F(h)=(n+1)log2(n+1)−2n。

它可以看成树高为h的完全二叉树进行向上调整建堆的调整次数。

所以向上调整算法的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。

向下调整算法

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):

或从这个代码来推:

c 复制代码
void f(){
    for(int i=(n-1-1)/2;i>=0;i--)
        adjustDown(a,n,i);
}

可得调整次数:

F ( h ) = 2 h − 2 × 1 + 2 h − 3 × 2 + ⋯ + 2 1 × ( h − 2 ) + 2 0 × ( h − 1 ) F(h)=2^{h-2}\times 1+2^{h-3}\times2+\cdots+2^1\times(h-2)+2^0\times(h-1) F(h)=2h−2×1+2h−3×2+⋯+21×(h−2)+20×(h−1),

2 h − 2 × 1 2^{h-2}\times 1 2h−2×1表示倒数第二层的结点,每个结点最多调整1次,总的调整次数。

我们同样用错位相减得到

F ( h ) = 2 h − 1 − h F(h)=2^h-1-h F(h)=2h−1−h,

因为树高 h = l o g 2 ( n + 1 ) h=log_2(n+1) h=log2(n+1),所以表达式变成了

F ( h ) = n − l o g 2 ( n + 1 ) ≈ n F(h)=n-log_2{(n+1)}\approx n F(h)=n−log2(n+1)≈n,

于是向下调整建堆的时间复杂度为 O ( n ) O(n) O(n)。

对比向下调整建堆(时间复杂度 O ( n ) O(n) O(n))和向上调整建堆(时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)),我们知道当n大于对数的底数时,对数是大于1的。所以同样且有很多结点的完全二叉树,向上调整建堆在最坏情况下是比向下调整建堆耗时更长。

6.4.4 堆的其他操作

不管建堆怎么样,堆还是完全二叉树,还是数据结构。管理数据是数据结构的任务。

1. 初始化(Init)

本次建堆用的是数组建立,很多功能参考顺序表笔记------目标是随时手搓顺序表-CSDN博客

很多功能以前都有记过笔记,就不过多赘述,直接上菜。

c 复制代码
//堆的初始化
void HeapInit(Heap* hp) {
	assert(hp);
	hp->_a = NULL;
	hp->_size = 0;
	hp->_capacity = 0;
}

2. 销毁(Destroy)

参考顺序表。

c 复制代码
// 堆的销毁
void HeapDestory(Heap* hp) {
	assert(hp);

	free(hp->_a);
	hp->_a = NULL;
	hp->_size = hp->_capacity = 0;
}

3. 插入(Push)

每次插入数据都要进行调整使被新数据打乱结构的树重新成为堆。

c 复制代码
// 堆的插入
void HeapPush(Heap* hp, HPDataType x) {
	assert(hp);

	if (hp->_size == hp->_capacity)//容量检测
	{
		int newCapacity = hp->_capacity == 0 ? 4 : hp->_capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(hp->_a, newCapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}

		hp->_a = tmp;
		hp->_capacity = newCapacity;
	}

	hp->_a[hp->_size] = x;
	hp->_size++;
	
	adjustUp(hp->_a,hp->_size-1);
}

这里不能使用向下调整算法进行堆的调整,因为向下调整算法是调整从当前结点到叶结点这一路径,而我们希望的是新加入的叶结点能快速融入到堆中,自然需要将新增的叶结点和各个父结点进行对比。

4. 弹出堆顶元素(Pop)

根结点和最后一个结点交换位置,然后删除最后一个结点,并进行调整使被打乱结构的树重新成为堆。

c 复制代码
// 弹出堆顶元素
void HeapPop(Heap* hp) {
	assert(hp);
	assert(!HeapEmpty(hp));

	Swap(&hp->_a[0], &hp->_a[hp->_size - 1]);
	hp->_size--;

	adjustDown(hp->_a, hp->_size, 0);
}

5. 获取堆顶元素(Top)

返回根结点即可。

c 复制代码
//获取堆顶元素
HPDataType HeapTop(Heap* hp) {
	assert(hp);
	assert(!HeapEmpty(hp));

	return hp->_a[0];
}

6. 判断堆是否为空(Empty)

我们设计的堆有专门的计数变量,可以通过计数变量来判断。

c 复制代码
// 堆的判空
bool HeapEmpty(Heap* hp) {
	assert(hp);
	return hp->_size == 0;
}

7. 获取堆的结点数(Size)

我们设计的堆有专门的计数变量,可以通过计数变量来判断。

c 复制代码
// 堆的数据个数
int HeapSize(Heap* hp) {
	assert(hp);
	return hp->_size;
}
相关推荐
搬砖的小码农_Sky8 分钟前
C语言:数组
c语言·数据结构
先鱼鲨生2 小时前
数据结构——栈、队列
数据结构
一念之坤2 小时前
零基础学Python之数据结构 -- 01篇
数据结构·python
IT 青年2 小时前
数据结构 (1)基本概念和术语
数据结构·算法
熬夜学编程的小王2 小时前
【初阶数据结构篇】双向链表的实现(赋源码)
数据结构·c++·链表·双向链表
liujjjiyun3 小时前
小R的随机播放顺序
数据结构·c++·算法
ahadee4 小时前
蓝桥杯每日真题 - 第19天
c语言·vscode·算法·蓝桥杯
Theliars4 小时前
C语言之字符串
c语言·开发语言
Reese_Cool4 小时前
【数据结构与算法】排序
java·c语言·开发语言·数据结构·c++·算法·排序算法
djk88885 小时前
.net将List<实体1>的数据转到List<实体2>
数据结构·list·.net