顺序结构二叉树--堆

1 堆的概念和结构

1、简单来说,堆就是一个数组,只不过它的所有元素是按完全二叉树的顺序来存储,来模拟实现二叉树。
2、堆分为小堆和大堆,就比如下图的小根堆示例图:每一个非叶子节点都比它的左右孩子都要小,根节点是最小的元素,并且它的逻辑结构是一棵完全二叉树;对于大根堆示例图则相反:每一个非叶子节点都比它的左右孩子都要大,根节点是最大的元素,并且它的逻辑结构是一棵完全二叉树
3、对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从 0 开始编号,则对于序号为 i 的结点有:

  • 若 i>0 , i 位置结点的双亲序号: (i - 1) / 2 ; i=0 , i 为根结点编号,无双亲结点
  • 若 2i+1 < n ,左孩子序号: 2i+1 ;若 2i+1>=n ,无左孩子
  • 若 2i+2 < n ,右孩子序号: 2i+2 ;若 2i+2>=n ,无右孩子

2 堆的实现

了解了大堆和小堆后,我们来实现一个小堆,因为堆本质是一个数组,所以我们定义堆的结构为:

#pragma once
//定义堆的结构---数组
#include<stdio.h>
#include<stdbool.h>
typedef int HPDataType;

typedef struct Heap
{
	HPDataType* arr;
	int size;//有效的数据个数
	int capacity;//空间大小
}HP;

void HPInit(HP* php);
void HPDestroy(HP* php);

void HPPush(HP* php, HPDataType x);
void HPPop(HP* php);

HPDataType HPTop(HP* php);
// 判空
bool HPEmpty(HP* php);


void Swap(int* x, int* y);
void AdjustUp(HPDataType* arr, int child);
void AdjustDown(HPDataType* arr, int parent, int n);

我们重点放在向上和向下调整算法

2.1 向上调整算法

定义:对于堆的插入,先将元素插入到堆的末尾,即最后一个孩子之后;插入之后如果堆的性质遭到破坏(这里指比它的父节点要小),将新插入结点顺着其双亲往上调整到合适位置即可,下面给出图例

数组原来是小根堆,现在插入10,因为要建立小根堆,10的父节点28比10大,那么10就要和父节点28交换位置;交换之后还没有结束,因为此时10比父节点18小,两者还要交换,一直交换到10成为根节点,或者10比它的父节点还要小才能停止向上调整。(编号都是从0开始)

//小堆
void HPPush(HP* php, HPDataType x)
{
	assert(php);
	//空间是否足够
	if (php->capacity == php->size)
	{
		int capacity = (php->capacity == 0) ? 4 : 2 * php->capacity;
		HPDataType* tmp = (HPDataType*)realloc(php->arr, capacity * sizeof(HPDataType));
		if (tmp == NULL) return;
		php->arr = tmp;
		php->capacity = capacity;
	}
	php->arr[php->size++] = x;
	AdjustUp(php->arr, php->size - 1);
}

//向上调整算法
void AdjustUp(HPDataType* arr, int child)
{
	int parent = (child - 1) / 2;
	//根节点不需要往上调
	while (child > 0)
	{
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else break;//一旦不用往上调整,前面自然是都符合小堆的结构
	}
}

计算向上调整算法建堆时间复杂度:堆是完全二叉树,为了便于计算,我们用特殊的完全二叉树--满二叉树来进行计算。

分析:第1层,2^{0}个结点,需要向上移动0层;第2层,2^{1}个结点,需要向上移动1层;第3层,2^{2}个结点,需要向上移动2层....第h层,2^{h-1}个结点,需要向上移动h-1层。由于需要移动结点总的移动步数为:每层结点个数 * 向上调整次数(第⼀层调整次数为0)可以得到总的移动步数为:^{}

根据错位相减和公式  n=2^{h}-1 和h=\log_2(n+1)可得F(n)=(n+1)(\log_2(n+1) -2)+2时间复杂度为O(NlogN)。

2.2 向下调整算法

定义:删除堆是删除堆顶的数据,将堆顶的数据跟最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法,直到满足小堆特性。

对元素28向下调整时,需要满足它的左右子树都是一个小堆

首先,28的左右子树都是小堆。第一步:比较左右孩子,15更小去和28比较,发现需要交换位置;第二步也是重复第一步的过程,直到第三步,发现28小于37,已经符合小堆特性了,就不用再向下调整。

//删除堆顶元素
void HPPop(HP* php)
{
	assert(php && php->size);
	Swap(&php->arr[0], &php->arr[php->size - 1]);
	php->size--;
	AdjustDown(php->arr, 0, php->size);
}

//n-->arr有效个数
void AdjustDown(HPDataType* arr, int parent, int n)
{
	int child = parent * 2 + 1;//左孩子
	while (child < n)
	{
		if (child + 1 < n && arr[child] > arr[child+1])
		{
			child++;
		}
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else break;
	}
}

void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

计算向下调整算法建堆时间复杂度

计算方式和向上调整算法一样,最后得到向下调整算法建堆时间复杂度为O(N),从另一方面来讲:向上调整法建堆时,层数小的结点少,需要往上移动次数少,层数大的结点多,需要往上移动次数多;向下调整法建堆时,层数小的结点少,需要往下移动次数多,层数大的结点多,需要往下移动次数少,明显向下调整法建堆效率更高。所以建堆时可以优先考虑向下调整法(从最后一个非叶子节点开始建堆,一直到根节点)。

3 堆的应用

3.1 堆排序

堆排序有两种做法,第一种就是前面的建堆函数HpPush()和删除根节点元素函数HpPop()结合,但是有个弊端就是,必须要借助类似现成的数据结构Hp才能实现;第二种就是在数组建堆,建堆后,首尾交换,交换后的堆尾数据从堆中删掉,将堆顶数据向下调整。下面给出版本二代码:

//堆排序
void HeapSort(int* arr, int n)
{
	//原arr不是堆
	// 建堆
	//升序--建大堆
	//降序--建小堆
	
	// 向上调整法建堆O(NlogN)
	//for (int i = 0; i < n; i++)
	//{
	//	AdjustUp(arr, i);
	//}
	//向下调整法建堆O(N)
	for (int i = (n-1-1)/2; i >=0; i--)
	{
		AdjustDown(arr, i, n);
	}
	int end = n - 1;
    //O(NlogN)
	while (end)
	{
		Swap(&arr[end], &arr[0]);
		AdjustDown(arr, 0, end);
		end--;
	}
}

3.2 Top-K问题

即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

(1)用数据集合中前K个元素来建堆:要前k个最大的元素,则建小堆(可以理解为,要往堆里放大的数,那每一次都要牺牲最小的数,就是堆顶元素);要前k个最小的元素,则建大堆(可以理解为,要往堆里放小的数,那每一次都要牺牲最大的数,就是堆顶元素)
(2)用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素:前k个最大的元素,比堆顶大就替换;前k个最小的元素,比堆顶小就替换。
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素

下面给出创造一些数据的代码,和如何求前K个最大的元素或者最小的元素(注意小堆和大堆的向下调整代码要区分以及srand,rand,malloc头文件的添加!!!)

void CreateNDate()
{
	//造100000个数据
	int n = 100000;
	srand(time(NULL));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}
	for (int i = 0; i < n; i++)
	{
		int x = (rand() + i) % 500000;
		fprintf(fin, "%d\n", x);
	}
	fclose(fin);
}
void Top_K_Min()
{

	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		exit(1);
	}

	//若要前K个小的数据
	int k = 0;
	printf("输入K的大小:");
	scanf("%d", &k);
	int* minHeap = (int*)malloc(k * sizeof(int));
	if (minHeap == NULL)
	{
		perror("malloc error");
		exit(2);
	}


	//1 取文件前K个数据建大堆
	for (int i = 0; i < k; i++)
	{
		if (fscanf(fout, "%d", &minHeap[i]) != 1)
		{
			printf("文件数据不足或格式异常!在第 %d 个数据处失败\n", i);
			exit(4);
		}
	}

	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(minHeap, i, k);
	}
	//2 用剩余的N-K个元素依次与堆顶元素比较,比堆顶元素大就替换
	int x = 0;
	while (fscanf(fout, "%d", &x) != EOF)
	{
		if (x < minHeap[0])
		{
			minHeap[0] = x;
			AdjustDown(minHeap, 0, k);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", minHeap[i]);
	}
	fclose(fout);
}
void Top_K_Max()
{

	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		exit(1);
	}

	//若要前K个大的数据
	int k = 0;
	printf("输入K的大小:");
	scanf("%d", &k);
	int* minHeap = (int*)malloc(k * sizeof(int));
	if (minHeap == NULL)
	{
		perror("malloc error");
		exit(2);
	}


	//1 取文件前K个数据建小堆
	for (int i = 0; i < k; i++)
	{
		if (fscanf(fout, "%d", &minHeap[i]) != 1)
		{
			printf("文件数据不足或格式异常!在第 %d 个数据处失败\n", i);
			exit(4);
		}
	}

	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(minHeap, i, k);
	}
	//2 用剩余的N-K个元素依次与堆顶元素比较,比堆顶元素大就替换
	int x = 0;
	while (fscanf(fout, "%d", &x)!=EOF)
	{
		if (x > minHeap[0])
		{
			minHeap[0] = x;
			AdjustDown(minHeap, 0, k);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", minHeap[i]);
	}
	fclose(fout);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值