C语言【数据结构】二叉树实现堆及堆排序

目录

一.二叉树的顺序结构及堆的概念

1.二叉树的顺序结构

2.堆的概念及结构

(1)堆的概念 

(2)堆的性质

二.堆的实现

1.初始化堆

2.销毁堆

3.打印堆

4.插入数据,并保持依旧为堆

5.删除堆顶的数据

6.判断堆是否为空

7.堆中数据个数

8.返回堆顶数据

三.堆总代码 

1.Heap.h

2.Heap.c

3.Test.c

四.堆排序

堆排序HeapSort函数

五.TOP-K问题


前言:这个是二叉树的第二篇,主要用二叉树来实现堆和堆排序,后面还有一篇实现二叉树。

最后一篇->C语言【数据结构】二叉树实现_糖果雨滴a的博客-CSDN博客

一.二叉树的顺序结构及堆的概念

1.二叉树的顺序结构

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

2.堆的概念及结构

(1)堆的概念 

如果有一个集合K = { k0,k1 ,k2 ,…,kN-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:K <= K2*i+1且 Ki<= K2*i+2(K >= K2*i+1且 Ki >= K2*i+2) (i = 0,1, 2…),则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

(2)堆的性质

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

二.堆的实现

1.初始化堆

在初始化之前,我们要先创建一个结构体,因为是用数组存的,所以这里类似于动态的顺序表。

typedef int HPDataType;

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

创建之后就是初始化了 ,整个初始化与顺序表一样。

void HeapInit(HP* php)
{
	assert(php);

	php->a = NULL;
	php->size = php->capacity = 0;;
}

2.销毁堆

销毁也与顺序表一样。

void HeapDestory(HP* php)
{
	assert(php);

	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;

3.打印堆

void HeapPrint(HP* php)
{
	assert(php);

	for (size_t i = 0; i < php->size; ++i)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}

4.插入数据,并保持依旧为堆

这个前面与顺序表的尾插一样,但是后面因为要控制插入数据后,保持依旧为堆,所以我在这里又实现了一个函数 AdjustUp 用来向上调整新插入的数,控制保持插入数据会依旧为堆。

void Swap(HPDataType* pa, HPDataType* pb)
{
	HPDataType tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}
void AdjustUp(HPDataType* a, size_t child)
{
	size_t 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 HeapPush(HP* php, HPDataType x)
{
	assert(php);

	if (php->size == php->capacity)
	{
		size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = realloc(php->a ,sizeof(HPDataType) * newCapacity);
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newCapacity;
	}

	php->a[php->size] = x;
	++php->size;

	//向上调整,控制保持是一个堆
	AdjustUp(php->a, php->size - 1);
}

 这里主要说一下这个AdjustUp这个函数。

首先因为是堆,所以是父节点连接着子节点,我们插入一个数据相当于在原本的二叉树中插入了一个叶节点,这时我们需要把这个叶节点和它的父节点比较。

因此我们定义一个变量parent 作为它的父节点,因此根据父节点和子节点的关系,我们可以得到parent = (child - 1) / 2. 接下来我们通过一个while循环来判断是否需要向上调整该新插入的节点,首先while的条件就是child > 0,这个child就是我们插入的节点,因为它最多能调整到根节点的位置,所以循环条件为child > 0.

while循环里面就是判断该节点与其父节点的大小关系,因为原本就已经是小堆(或大堆),所以如果该节点的值小于(或大于)其父节点就向上调整,让该节点和其父节点交换,因为要多次用到交换,所以我们实现了一个新的函数Swap用来交换子节点和父节点。交换之后再让原本的子节点变成父节点(child = parent),让父节点变成这个新的子节点的父节点(parent = (child - 1) / 2).

如果子节点大于(或小于)其父节点,说明此时的二叉树已经调整成了小堆(或大堆),所以我们直接break。

5.删除堆顶的数据

这个删除与顺序表的删除有一些一样的地方,都是直接通过--size来进行删除的。

但是堆的删除需要在之前先交换堆顶(根节点)和最后一个子节点,然后删除最后一个。

这里也需要进行调整,不过这里不是插入中的向上调整,而是向下调整

void Swap(HPDataType* pa, HPDataType* pb)
{
	HPDataType tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}
void AdjustDown(HPDataType* a, size_t size, size_t root)
{
	size_t parent = root;
	size_t child = parent * 2 + 1;
	while (child < size)
	{
		//1.选出左右孩子中小的那个
		if (child + 1 < size && a[child + 1] < a[child])
		{
			++child;
		}
		//2.如果孩子小于父亲,则交换,并继续往下调整
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapPop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	Swap(&php->a[0], &php->a[php->size - 1]);
	--php->size;

	AdjustDown(php->a, php->size - 1, 0);
}

 首先交换堆顶(根节点)和最后一个子节点,然后删除最后一个。

之后再调用AdjustDown(向下调整)函数去把这个新的根节点(原本的最后一个子节点)向下调整。

这里我们传的参数要比插入函数中向上调整的参数多。

首先我们定义两个变量parent和child,parent就是传来的root,而child可能有两个,我们先定义一个左孩子child = parent * 2 + 1。之后同样通过一个while循环,条件就是判断该父节点的子节点如果<size,就说明没有到最后一个节点,就可以继续循环。

这时我们需要判断左孩子和右孩子的大小关系,但是在此之前我们首先要判断是否有右孩子(child + 1 < size),如果满足,则说明有右孩子,这时再判断大小关系,如果要实现小堆(或大堆),那么如果右孩子小于(或大于)左孩子,就++child,让子节点变成这个更小(或更大)的子节点。

接下来就判断子节点和父节点的大小关系,如果子节点小于(或大于)父节点,就Swap子节点和父节点,然后让父节点的位置变成子节点的位置,这时值是交换过的,所以要改变这个位置,然后这个子节点再根据新的父节点而改变。

如果子节点大于(或小于)父节点就直接break,说明已经成为了一个新的小堆(或大堆)。

6.判断堆是否为空

通过判断size是否等于0,来判断。


bool HeapEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}

7.堆中数据个数

直接返回size的值,size的大小就是数据的个数。

size_t HeapSize(HP* php)
{
	assert(php);

	return php->size;
}

8.返回堆顶数据

先要判断一下堆中是否有数据,如果有就返回第1个根节点,即a[0]。

HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	return php->a[0];
}

三.堆总代码 

1.Heap.h

#pragma once

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

//小堆

typedef int HPDataType;

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

void Swap(HPDataType* pa, HPDataType* pb);//交换数据

void HeapInit(HP* php);//初始化堆

void HeapDestory(HP* php);//销毁堆

void HeapPrint(HP* php);//打印堆

void HeapPush(HP* php, HPDataType x);//插入后,保持依旧为堆

void HeapPop(HP* php);//删除堆顶的数据

bool HeapEmpty(HP* php);//判断堆是否为空

size_t HeapSize(HP* php);//计算堆中数据个数

HPDataType HeapTop(HP* php);//返回堆顶数据

2.Heap.c

#include "Heap.h"

void HeapInit(HP* php)
{
	assert(php);

	php->a = NULL;
	php->size = php->capacity = 0;;
}

void HeapDestory(HP* php)
{
	assert(php);

	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;

}

void HeapPrint(HP* php)
{
	assert(php);

	for (size_t i = 0; i < php->size; ++i)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}

void Swap(HPDataType* pa, HPDataType* pb)
{
	HPDataType tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

void AdjustUp(HPDataType* a, size_t child)
{
	size_t 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 HeapPush(HP* php, HPDataType x)
{
	assert(php);

	if (php->size == php->capacity)
	{
		size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = realloc(php->a ,sizeof(HPDataType) * newCapacity);
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newCapacity;
	}

	php->a[php->size] = x;
	++php->size;

	//向上调整,控制保持是一个堆
	AdjustUp(php->a, php->size - 1);
}

void AdjustDown(HPDataType* a, size_t size, size_t root)
{
	size_t parent = root;
	size_t child = parent * 2 + 1;
	while (child < size)
	{
		//1.选出左右孩子中小的那个
		if (child + 1 < size && a[child + 1] < a[child])
		{
			++child;
		}
		//2.如果孩子小于父亲,则交换,并继续往下调整
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapPop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	Swap(&php->a[0], &php->a[php->size - 1]);
	--php->size;

	AdjustDown(php->a, php->size - 1, 0);
}

bool HeapEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}

size_t HeapSize(HP* php)
{
	assert(php);

	return php->size;
}

HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	return php->a[0];
}

3.Test.c

void TestHeap()
{
	HP hp;
	HeapInit(&hp);
	HeapPush(&hp, 1);
	HeapPush(&hp, 4);
	HeapPush(&hp, 0);
	HeapPush(&hp, 7);
	HeapPush(&hp, 3);
	HeapPush(&hp, 9);
	HeapPrint(&hp);

	HeapPop(&hp);
	HeapPrint(&hp);

	if (!HeapEmpty(&hp))
	{
		printf("堆不为空\n");
	}
	else
	{
		printf("堆为空\n");
	}
	printf("堆size:%d\n", HeapSize(&hp));
	printf("堆顶数据:%d\n", HeapTop(&hp));

	HeapDestory(&hp);
}

int main()
{
	TestHeap();

	return 0;
}

这里展示的是小堆,如果想要改成大堆,只需要在向上调整函数和向下调整函数中的比较子节点和父节点大小那改变一下>和<即可。

四.堆排序

Swap函数

void Swap(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

向上调整函数

//向上调整
void AdjustUp(int* a, size_t child)
{
	size_t 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(int* a, size_t size, size_t root)
{
	size_t parent = root;
	size_t 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;
		}
	}
}

在这里我们需要用到之前写的Swap交换函数,AdjustUp向上调整函数和AdjustDown函数。

堆排序HeapSort函数

//堆排序 O(N * logN)
void HeapSort(int* a, int n)
{
	//向上调整--建堆 O(N * logN)
	/*for (int i = 1; i < n; ++i)
	{
		AdjustUp(a, i);
	}*/

	//向下调整--建堆 O(N)
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}

	size_t end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}
int main()
{
	int a[] = { 4, 2, 7, 8, 5, 1, 0, 6 };
	HeapSort(a, sizeof(a) / sizeof(int));

	for (int i = 0; i < sizeof(a) / sizeof(int); ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");

	return 0;
}

这里我们没有一个堆,我们只有一个数组,需要我们自己把这个数组转化为堆,再进行排序。

因此我们有两种方法,一种是向上调整建堆法(时间复杂度O(N * logN)),

另一种是向下调整建堆法(时间复杂度O(N)),根据时间复杂度来说,向下调整建堆法能好一点,但是差距也不是特别大。

首先说这个向上调整建堆法:因为向上调整是利用父节点和子节点交换,并且父节点是根据子节点得出的(parent = (child - 1) / 2)所以我们可以从数组中的第二个元素开始调用向上调整函数,一直到最后一个元素。

向下调整建堆法:不能直接让i从第一个元素开始,而是要从后往前用向下调整函数,因为只有从后往前,才能让前面调整过的都是小堆(或大堆),而从前往后不行。而叶子节点不需要调整,因为一个节点既可以看成小堆,也可以看成大堆,所以要从倒数的第一个非叶子节点开始调整,因此想要找到这个非叶子节点就通过最后一个节点来找,最后一个节点的父节点就是该非叶子节点。因为元素个数为n,所以最后一个节点为n-1,所以该父节点为(n-1 - 1) / 2,一直调整到第一个元素。

接下来就是排序了,建小堆和大堆分别为降序和升序。

在排序的过程中,我们采用的方法类似于堆中的删除,从后往前排序,直到只剩最后一个根节点。首先交换根节点和最后一个节点,然后让交换后的根节点(原最后一个节点)进行向下调整,调整完之后,此时根节点就已经作为降序或者升序中的最后一个元素,依次进行,即可完成。

五.TOP-K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
若求前k个最大的元素,则建小堆
若求前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
void PrintTopK(int* a, int n, int k)
{
	//1.建堆--用a中前k个元素建堆
	int* kminHeap = (int*) malloc(sizeof(int) * k);
	if (kminHeap == NULL)
	{
		printf("malloc fail\n");
	}

	for (int i = 0; i < k; ++i)
	{
		kminHeap[i] = a[i];
	}

	//建小堆
	for (int j = (k - 1 - 1) / 2; j >= 0; --j)
	{
		AdjustDown(kminHeap, k, j);
	}

	//2.将剩余n-k个元素依次与堆顶元素交换,不满则替换
	for (int i = k; i < n; ++i)
	{
		if (a[i] > kminHeap[0])
		{
			kminHeap[0] = a[i];
			AdjustDown(kminHeap, k, 0);
		}
	}

	for (int j = 0; j < k; ++j)
	{
		printf("%d ", kminHeap[j]);
	}
	printf("\n");
	free(kminHeap);
}

void TestTopk()
{
	int n = 10000;
	int* a = (int*)malloc(sizeof(int) * n);
	srand(time(0));
	for (size_t i = 0; i < n; ++i)
	{
		a[i] = rand() % 1000000;
	}
	a[5] = 1000000 + 1;
	a[1231] = 1000000 + 2;
	a[531] = 1000000 + 3;
	a[5121] = 1000000 + 4;
	a[115] = 1000000 + 5;
	a[2305] = 1000000 + 6;
	a[99] = 1000000 + 7;
	a[76] = 1000000 + 8;
	a[423] = 1000000 + 9;
	a[0] = 1000000 + 1000;

	PrintTopK(a, n, 10);
}

int main()
{
	TestTopk();

	return 0;
}

首先我们在TestTopk中通过srand,rand来创建一些<1000000的随机数据,然后我们在随机让10个数据变成>1000000的数,用来测试该排序,如果结果为这些数的排序,就说明排序成功。

之后调用PrintTopK函数来具体实现排序,并测试排序结果。

首先要先开辟一个可以存k个元素的数组,然后让这个数组a的前k个元素存进去。然后根据需要得到的前最大(或最小)k个元素,来选择建小堆(或大堆)。在这里我们调用AdjustDown向下调整函数来建堆。

之后,我们要让剩余的n - k个元素依次和堆顶元素比较,如果大于(或小于)堆顶元素,就让堆顶元素和该元素交换,然后在对该进行向下调整,依次循环n - k次,去掉每一个最小的元素(即堆顶元素),然后向下调整,剩下的最后就是这个数组a中最大(最小)的k个元素了。

最后我们可以写个循环,来判断一下这个Top-K问题是否完成。

这篇关于堆的相关实现及排序就到这结束了,下一篇就是二叉树的最后一篇了(关于二叉树的链式结构的实现)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

冰果滴

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

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

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

打赏作者

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

抵扣说明:

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

余额充值