用数组实现两种堆

堆的实现

数据转换成堆的思路:

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

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

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

堆的基本信息:

typedef int HPDataType;
typedef struct Heap {
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;

堆的向上调整算法建堆

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

当插入数据80时,整体还是小堆,无需调整。
请添加图片描述

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

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

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

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

typedef int HPDataType;
void Swap(HPDataType* a, HPDataType* b) {
	HPDataType t = *a;
	*a = *b;
	*b = t;
}

//向上调整算法
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个根结点此时可以看成堆

操作示例:

typedef int HPDataType;
void Swap(HPDataType* a, HPDataType* b) {
	HPDataType t = *a;
	*a = *b;
	*b = t;
}

//向上调整算法
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;
}

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

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

堆的向下调整算法建堆

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

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

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

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

typedef int HPDataType;
void Swap(HPDataType* a, HPDataType* b) {
	HPDataType t = *a;
	*a = *b;
	*b = t;
}

//向下调整算法
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;
	}
}

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

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

typedef int HPDataType;
void Swap(HPDataType* a, HPDataType* b) {
	HPDataType t = *a;
	*a = *b;
	*b = t;
}

//向下调整算法
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;
	}
}

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

建堆时间复杂度分析

向上调整算法

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

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

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++2h1×(h1)

这是一个等差数列和等比数列交叉相乘再相加组成的数列,通过错位相减或公式

( k n + m ) q n − m (kn+m)q^n-m (kn+m)qnm ( k = a q − 1 m = b − k q − 1 ) (k=\frac{a}{q-1}m=\frac{b-k}{q-1}) (k=q1am=q1bk)(这个公式是数列 ( a n + b ) q n − 1 (an+b)q^{n-1} (an+b)qn1的前 n n n项和公式)

可得到

F ( h ) = 2 h × h − 2 × 2 h + 2 F(h)=2^h\times h-2\times2^h+2 F(h)=2h×h2×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)

向下调整算法

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

或从这个代码来推:

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)=2h2×1+2h3×2++21×(h2)+20×(h1)

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

我们同样用错位相减得到

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

因为树高 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)=nlog2(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 n n大于对数的底数时,对数是大于1的。所以同样的且有很多结点的完全二叉树,向上调整建堆在最坏情况下是比向下调整建堆耗时更长。

堆的其他操作

无论如何建堆,堆还是完全二叉树,还是数据结构。管理数据是数据结构的任务。

  1. 初始化(Init)

本次建堆用的是数组建立,很多功能参考顺序表的概念和实现-CSDN博客

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

typedef int HPDataType;
typedef struct Heap {
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;
//堆的初始化
void HeapInit(Heap* hp) {
	assert(hp);
	hp->_a = NULL;
	hp->_size = 0;
	hp->_capacity = 0;
}
  1. 销毁(Destroy)

参考顺序表。

typedef int HPDataType;
typedef struct Heap {
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;
// 堆的销毁
void HeapDestory(Heap* hp) {
	assert(hp);

	free(hp->_a);
	hp->_a = NULL;
	hp->_size = hp->_capacity = 0;
}
  1. 插入(Push)

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

typedef int HPDataType;
typedef struct Heap {
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;
// 堆的插入
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);
}

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

  1. 弹出堆顶元素(Pop)

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

typedef int HPDataType;
typedef struct Heap {
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;
// 弹出堆顶元素
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);
}
  1. 获取堆顶元素(Top)

返回根结点即可。

typedef int HPDataType;
typedef struct Heap {
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;
//获取堆顶元素
HPDataType HeapTop(Heap* hp) {
	assert(hp);
	assert(!HeapEmpty(hp));

	return hp->_a[0];
}
  1. 判断堆是否为空(Empty)

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

typedef int HPDataType;
typedef struct Heap {
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;
// 堆的判空
bool HeapEmpty(Heap* hp) {
	assert(hp);
	return hp->_size == 0;
}
  1. 获取堆的结点数(Size)

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

typedef int HPDataType;
typedef struct Heap {
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;
// 堆的数据个数
int HeapSize(Heap* hp) {
	assert(hp);
	return hp->_size;
}

参考程序汇总

小堆myMinHeap.h

#pragma once

#include<stdio.h>
#include<assert.h>
#include<stdbool.h>
#include<stdlib.h>
#include<time.h>

#ifndef MyMinHeap
#define MyMinHeap
typedef int HPDataType;//堆的定义
typedef struct Heap
{
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;
//小堆向上调整
void adjustUp(HPDataType* a, int child);
//小堆向下调整
void adjustDown(HPDataType* a, int n, int parent);
//交换数据
void Swap(HPDataType* a, HPDataType* b);
//堆的初始化
void HeapInit(Heap* php);
// 堆的销毁
void HeapDestory(Heap* hp);
// 小堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 小堆的弹出数据
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
bool HeapEmpty(Heap* hp);

#endif

小堆myMinHeap.c

#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS 1
#endif
#include"myMinHeap.h"

void Swap(HPDataType* a, HPDataType* b) {
	HPDataType t = *a;
	*a = *b;
	*b = t;
}

//向上调整算法
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;
}

//向下调整算法
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;
	}
}

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

// 堆的销毁
void HeapDestory(Heap* hp) {
	assert(hp);

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

// 堆的插入
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++;

	//adjustDown(hp->_a, hp->_size,(hp->_size-1-1)/2);
	//插入数据时不可以向下调整
	adjustUp(hp->_a,hp->_size-1);
}

// 弹出堆顶元素
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);
}

//获取堆顶元素
HPDataType HeapTop(Heap* hp) {
	assert(hp);
	assert(!HeapEmpty(hp));

	return hp->_a[0];
}

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

// 堆的数据个数
int HeapSize(Heap* hp) {
	assert(hp);
	return hp->_size;
}

大堆myMaxHeap.h

#pragma once

#include"myMinHeap.h"

//大堆插入数据
void HeapMaxPush(Heap* hp, HPDataType x);
//大堆弹出数据
void HeapMaxPop(Heap* hp);
//大堆向上调整
void adjustMaxUp(HPDataType* a, int child);
//大堆向下调整
void adjustMaxDown(HPDataType* a, int n, int parent);

大堆myMaxHeap.c

#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS 1
#endif
#include"myMaxHeap.h"


//大堆的向上调整
void adjustMaxDown(HPDataType* a, int n, int parent) {
	int child = parent * 2 + 1;
	while (child < n) {
		if (child + 1 < n)
			if (a[child] <= a[child + 1])
				++child;
		if (a[child] >= a[parent]) {
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else break;
	}
}

//大堆的向下调整
void adjustMaxUp(HPDataType* a, int child) {
	int parent = (child - 1) / 2;
	while (child > 0) {
		if (a[parent] <= a[child]) {
			Swap(&a[parent], &a[child]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else break;
	}
}

// 大堆的插入
void HeapMaxPush(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;
	adjustMaxUp(hp->_a, hp->_size - 1);
}

//大堆的弹出
void HeapMaxPop(Heap* hp) {
	assert(hp);
	Swap(&hp->_a[0], &hp->_a[hp->_size - 1]);
	hp->_size--;
	adjustMaxDown(hp->_a, hp->_size, 0);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Darkwanderor

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

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

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

打赏作者

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

抵扣说明:

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

余额充值