C语言数据结构——二叉树顺序结构的实现(堆的实现)


前言

本篇文章主要讲解堆的实现以及堆的应用。

 


一、二叉树的顺序结构

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

 二、堆的概念及结构

如果有一个关键码的集合K = {k0 ,k1 ,k2 ,…,k(n-1) },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:父节点小于子节点(父节点大于子节点),则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

 三、堆的实现

我们的堆要实现以下功能(下列功能都按照大堆实现,想要实现小堆只需改变调整函数即可):

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

// 堆的构建
void HeapCreate(HP* php, HPDataType* a, int n);
//堆的打印
void HeapPrint(HP* php);
//初始化
void HeapInit(HP* php);
//堆的销毁
void HeapDestroy(HP* php);
// 插入数据,保持他继续是一个堆 O(logN)
void HeapPush(HP* php, HPDataType x);
// 删除堆顶的数据,并且保持他继续是一个堆 O(logN)
void HeapPop(HP* php);
//读取堆顶数据
HPDataType HeapTop(HP* php);
//堆中数据的数量
int HeapSize(HP* php);
// 堆的判空
bool HeapEmpty(HP* php);
//向下调整
void Adjustdown(HPDataType* a, int n, int father);
//交换函数
void Swap(HPDataType* a, HPDataType* b);
//向上调整函数
void Adjustup(HPDataType* a, int child);

想实现上面的功能,最不可少的就是向上调整和向下调整

3.1 向上调整

思路:给出一个数组,逻辑上看是完全二叉树,如果是大堆的话,数组尾部插入一个数后,和他的父节点比大小,如果比父节点大则和父节点交换位置,然后继续和其新的父节点比较,如果比父节点小或者成为根节点则停止比较。

 

//交换函数
void Swap(HPDataType* a, HPDataType* b)
{
	int p = *a;
	*a = *b;
	*b = p;
}
//向上调整函数
void Adjustup(HPDataType* a, int child)
{
	assert(a);
	int father = (child - 1) / 2;
	while (child>0)
	{
		if (a[child] > a[father])
		{
			//交换函数
			Swap(&a[child], &a[father]);
			//迭代
			child = father;
			father = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

3.2 向下调整

思路:给出一个数组,逻辑上是完全二叉树,以大堆为例,数组首部插入一个数,然后和其左右孩子的较大值作对比,小于孩子则和孩子交换,然后和新孩子作对比,当其大于孩子或者成为叶节点时停止交换。

 

//交换函数
void Swap(HPDataType* a, HPDataType* b)
{
	int p = *a;
	*a = *b;
	*b = p;
}
//向下调整函数
void Adjustdown(HPDataType* a, int n, int father)
{
	assert(a);
	//假设child为最大值,此时child为左child
	int child = (father * 2) + 1;
	while (child<n)
	{
		//child与右child比较,将大值赋给child
		if (a[child + 1] > a[child]&&child+1<n)
		{
			child = child + 1;
		}
		//交换
		if (a[child] > a[father])
		{
			Swap(&a[child], &a[father]);
			//迭代
			father = child;
			child = (father * 2) + 1;
		}
		else
		{
			break;
		}
	}
}

3.3 堆的初始化

//初始化
void HeapInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->capacity = php->size = 0;
}

3.4 堆的构建

思路:首先我们拿到一个数组,我们可以将数组看成一个完全二叉树,但不是堆,那么我们需要将其调整成堆。我们从最后一个非叶节点的节点开始向下调整,直到调整到根。

 

// 堆的构建
void HeapCreate(HP* php, HPDataType* a, int n)
{
	assert(php && a);
	php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (php->a == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	php->capacity = n;
	//复制数组
	memcpy(php->a, a, sizeof(HPDataType) * n);
	php->size += n;
	//调整
	int father = (n -1 - 1) / 2;
	while (father>=0)
	{
		Adjustdown(php->a, n, father);
		father--;
	}
}

3.5 堆的打印

//堆的打印
void HeapPrint(HP* php)
{
	assert(php);
	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}

3.6 插入数据

思路:尾部插入数据,然后进行向上调整

// 插入数据,保持他继续是一个堆 O(logN)
void HeapPush(HP* php, HPDataType x)
{
	assert(php);
	//扩容
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDataType* ptr = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
		if (ptr == NULL)
		{
			perror("realloc");
			exit(-1);
		}
		php->a = ptr;
		php->capacity = newcapacity;
	}
	//插入
	php->a[php->size] = x;
	php->size++;
	//调整
	Adjustup(php->a,php->size-1);
}

3.7 删除堆顶的数据

思路:将堆顶的数据和堆尾的数据交换,也就是数组的第一个数据和最后一个数据进行交换,然后尾删,然后对堆顶的数据进行向下调整

// 删除堆顶的数据,并且保持他继续是一个堆 O(logN)
void HeapPop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));
	//堆顶数据和最后一个数据进行交换,然后尾删
	//在对堆顶数据进行由上到下的调整
	//交换
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;
	//调整
	Adjustdown(php->a, php->size, 0);

}

3.8 读取堆顶数据/堆中的数量/堆的判空

读取堆顶数据

//读取堆顶数据
HPDataType HeapTop(HP* php)
{
	assert(php);
	return php->a[0];
}

堆中的数量

//堆中数据的数量
int HeapSize(HP* php)
{
	assert(php);
	return php->size;
}

堆的判空

// 堆的判空
bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

四、堆的应用

4.1 堆排序

思路:1.拿到一个数据先建堆

           2.利用堆删除的思想,进行排序

 

// 对数组进行堆排序O(N*logN)
void HeapSort(int* a, int n)
{
	int father = (n - 1 - 1) / 2;
	while (father >= 0)
	{
		Adjustdown(a, n, father);
		father--;
	}
	int end = n-1;
	while (end)
	{
		Swap(&a[0], &a[end]);
		Adjustdown(a, end, 0);
		end--;
	}
}

4.2 Top-K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

思路:建立一个数量为k的小堆,然后数组里的数大于堆顶则入堆顶,然后向下调整沉底,当数组遍历完后,最大的前k个数就在堆里

//用堆求topk
void test2()
{
	int n=1000;
	int k=5;
	//向文件写入一千个随机值
	srand(time(0));
	FILE* data = fopen("data.txt", "w");
	if (data == NULL)
	{
		perror("fopen");
	}
	for (int i = 0; i < n; i++)
	{
		int redom = rand();
		fprintf(data, "%d\n", redom);
	}
	fclose(data);
	//读取文件最大的k个数
	data = fopen("data.txt", "r");
	if (data == NULL)
	{
		perror("fopen");
	}
	//创建一个大小为k的小堆
	HP heap;
	HeapInit(&heap);
	int ret;
	for (int i = 0; i < k; i++)
	{
		fscanf(data, "%d", &ret);
		HeapPush(&heap, ret);
	}
	//HeapPrint(&heap);
	//遍历文件
	while (fscanf(data, "%d", &ret) != EOF)
	{
		if (ret > heap.a[0])
		{
			heap.a[0] = ret;
			Adjustdown(heap.a, k, 0);
		}

	}
	HeapPrint(&heap);
	fclose(data);
	data = NULL;

}

总结

以上就是堆的全部内容,希望铁子们能有所收货

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值