【数据结构】堆 和 堆的应用TopK问题

二叉树的顺序存储结构

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


在这里插入图片描述
如上图所示,如果使用顺序存储的话(即用数据存储),此时,非完全二叉树是会有空间浪费的,而完全二叉树不会;

有人说,直接把非完全二叉树的 4 ,5 的位置移到空出来的位置不就好了嘛?
这样是不可以的,二叉树的规则就规定了左右子树是由顺序之分的,把左右子树的位置挪动,那么他就不是二叉树了。


还有一个问题需要理解:二叉树的形式:是我们想象出来的逻辑图,也是逻辑空间,而顺序表数组存储二叉树才是真正的实际的物理空间;也就是说,在计算机中,没有所谓的树的内存结构给我们存储,只是我们想象出来的,实际计算机用的是数组形式,我们只是把逻辑空间映射到了物理空间上;因为逻辑空间比较形象,所以我们就是这么想象的;


顺序表的下标和二叉树结点位置的规律

在这里插入图片描述
这样就建立了二叉树和顺序表的映射关系;


堆的基础理解

我就简单的语言概括以下:
给你一堆数据,这些数据需要以一定的顺序存储在一位数组中,以什么顺序呢?以完全二叉树的摆放顺序,并且满足一定的关系,什么关系呢?就是上面说的完全二叉树的结点和数组下标的关系;
而堆又分大堆和小堆;
在这里插入图片描述
注意哦:大堆小堆,在数组种顺序表是不能直接看成顺序的!


堆有啥用啊?堆的应用是什么?

  1. 堆我们一般应用于排序,我们把这种排序较为堆排序,有排序,肯定因为这种结构的效率很高效;
  2. 还有一种是TOPK问题的解决,即在N个树种,找出前K 个最小的,或者前K个最大的数;

实现堆的数据结构

这里以大堆为例子

1 堆的头文件

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

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

void HeapInit(HP *hp);
void HeapDestroy(HP* hp);
void HeapPush(HP* hp, HPDateType x);
void AdjustUp(HPDateType* a,int child);
void HeapPop(HP* hp);
void AdjustDown(HPDateType* a, int n, int parent);
int HPSize(HP* hp);
bool HPEmpty(HP* hp);
void Swap(HPDateType* px, HPDateType* py);

void Print(HP* hp);

2 堆的插入

堆额插入问题,要清楚的知道,插入数据后,还是堆,同时我们插入时候是实际尾插数组,但是想象时候是想象成为在完全二叉树的最后位置插入;插入数据时候要保持还是对结构,那么就需要向上调整算法:该算法就是调整插入的数据后,还要满足堆结构;


//a:数组;
//child:要调整的位置
void AdjustUp(HPDateType* a,int child)
{
	assert(a);

	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 HeapPush(HP* hp, HPDateType x)
{
	assert(hp);
	if (hp->capacity == hp->size)
	{
		int newCapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		HPDateType* temp = (HPDateType*)realloc(hp->a,sizeof(HPDateType)*newCapacity);
		if (temp == NULL)
		{
			printf("fail realloc!\n");
			exit(-1);
		}
		hp->capacity = newCapacity;
		hp->a = temp;

	}
	//插入数据后,开始调整位置
	hp->a[hp->size] = x;
	hp->size++;
	AdjustUp(hp->a, hp->size - 1);
}

3 删除堆顶元素

删除元素,其实就是删除数组第一个元素,那么也就是堆顶元素;
如果我们直接把数组后面的数据都往前移动,这样就不可以满足堆结构了!
所以们要其他的方式删除:首先把数组的最后一个元素和第一个元素交换,那么这样再删除最后一个元素,最相当于删掉了堆顶
删除堆顶元素,也要保持堆的结构,此时用的算法是向下调整算法;
算法的模拟如下图:

在这里插入图片描述

void AdjustDown(HPDateType* a, int n, int parent)
{
	int child = parent * 2 + 1; //假设需要换上去的结点为左孩子,如果假设不成立,下面会有判断调整假设

	while (child < n) //父亲结点到叶子结点就不用调整了,此时孩子结点一定是越界情况,那么用来判循环结束条件即可
	{
		//判断右孩子是否大于父节点,做这个判断是因为我们一开始就假设孩子为左孩子
		//但由于我们需要整体判断孩子(左和右)看看哪个是比父亲更大的(因为需要大堆,所以把更大的数字换上去)
		//做child+1 < n 判断是因为防止右孩子会越界的情况
		if (child + 1 < n && a[child + 1] > a[parent]) //大堆的写法,即孩子大于父亲要调整
		{
			++child;
		}

		//来到这里就完成了假设,开始调整,把孩子往上拉
		if (a[child] > a[parent]) //大堆的情况,所以是大于号
		{
			Swap(&a[child], &a[parent]); //把孩子拉上去到父节点的位置
			//继续迭代
			parent = child;
			child = parent * 2 + 1;
		}
		else //来到这里说明,孩子结点(包含左右孩子)都是比父节点小的,那么这直接说明不需要调整了满足了大堆的情况
		{
			break;
		}
	}
}
void HeapPop(HP* hp)
{
	assert(hp);
	assert(!HPEmpty(hp));
	
	//先把最后一个元素和根交换
	Swap(&hp->a[hp->size - 1], &hp->a[0]);
	hp->size--;
	//开始向下调整
	AdjustDown(hp->a, hp->size, 0);
}

4 堆的实现文件

堆的主文件

#include"Heap.h"

void HeapInit(HP *hp)
{
	assert(hp);
	hp->a = NULL;
	hp->capacity = hp->size = 0;

}
void HeapDestroy(HP* hp)
{
	assert(hp);
	free(hp);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}
//a:数组;

//child:要调整的位置
void AdjustUp(HPDateType* a,int child)
{
	assert(a);

	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 HeapPush(HP* hp, HPDateType x)
{
	assert(hp);
	if (hp->capacity == hp->size)
	{
		int newCapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		HPDateType* temp = (HPDateType*)realloc(hp->a,sizeof(HPDateType)*newCapacity);
		if (temp == NULL)
		{
			printf("fail realloc!\n");
			exit(-1);
		}
		hp->capacity = newCapacity;
		hp->a = temp;

	}
	//插入数据后,开始调整位置
	hp->a[hp->size] = x;
	hp->size++;
	AdjustUp(hp->a, hp->size - 1);
}
bool HPEmpty(HP* hp)
{
	return hp->size == 0;
}
int HPSize(HP* hp)
{
	return hp->size;
}

void Swap(HPDateType* px, HPDateType* py)
{
	HPDateType temp = *px;
	*px = *py;
	*py = temp;
}
void AdjustDown(HPDateType* a, int n, int parent)
{
	int child = parent * 2 + 1; //假设需要换上去的结点为左孩子,如果假设不成立,下面会有判断调整假设

	while (child < n) //父亲结点到叶子结点就不用调整了,此时孩子结点一定是越界情况,那么用来判循环结束条件即可
	{
		//判断右孩子是否大于父节点,做这个判断是因为我们一开始就假设孩子为左孩子
		//但由于我们需要整体判断孩子(左和右)看看哪个是比父亲更大的(因为需要大堆,所以把更大的数字换上去)
		//做child+1 < n 判断是因为防止右孩子会越界的情况
		if (child + 1 < n && a[child + 1] > a[parent]) //大堆的写法,即孩子大于父亲要调整
		{
			++child;
		}

		//来到这里就完成了假设,开始调整,把孩子往上拉
		if (a[child] > a[parent]) //大堆的情况,所以是大于号
		{
			Swap(&a[child], &a[parent]); //把孩子拉上去到父节点的位置
			//继续迭代
			parent = child;
			child = parent * 2 + 1;
		}
		else //来到这里说明,孩子结点(包含左右孩子)都是比父节点小的,那么这直接说明不需要调整了满足了大堆的情况
		{
			break;
		}
	}
}
void HeapPop(HP* hp)
{
	assert(hp);
	assert(!HPEmpty(hp));
	
	//先把最后一个元素和根交换
	Swap(&hp->a[hp->size - 1], &hp->a[0]);
	hp->size--;
	//开始向下调整
	AdjustDown(hp->a, hp->size, 0);
}
void Print(HP* hp)
{
	for (int i = 0; i < hp->size; i++)
	{
		printf("%d ", hp->a[i]);
	}
	printf("\n");
}

堆的应用–topK 问题

topK问题就是:给定你N个数,要你找出k最大的或者最小的值;

解决思路:(假如获取k个最大数)
思路1 .直接降序排序N个数据,获取前k个元素即可,时间复杂度度为O(n* logn);

思路2.将N个数插入大堆结构,再pop k次大堆,再依次获取堆顶元素即可,时间复杂度O(n+k*logn);

思路3.假如右N = 10亿个数,内存放不下,它们只能存在文件中,K = 100;此时思路1和思路2都不行;
将前K个数建小堆,剩下N-K的数依次和小堆堆顶比较,如果比堆顶大,就替换堆顶,再向下调整成为堆结构,当比较完后,小堆里面的K个元素就是最大的K个数了;


模拟topK 问题代码:
随机生成100w个数,插入到到100w个空间数组中;
然后设定10个数,比100w大的数,随机放入到100w个数组中,假如能找到这10个数,则证明topK问题的思路3有用

#include<stdio.h>
#include<time.h>
//打印TopK的最大前10个数
void PrintTopK(int* a,int size,int k)
{
	HP hp;
	HeapInit(&hp);
	
	//先把100w个数前K个数插入到小堆中
	for(int i = 0;i < k;i++)
	{
		HeapPush(&hp,a[i]);
	}
	//将N-k个数和小堆的前k个数比较
	for(int i = k;i<size;i++)
	{
	//判断小堆的元素和前N-K个元素的大小
	//剩下的N-K个数比它大,那么就替换堆顶,向下调整;
		if(a[i] > HeapTop($hp))
		{
			HeapPop(&hp);
			HeapPush(&hp,a[i]);
			//或者这样
			//ph.a[0] = a[i];
			//AdjustDown(hp.a,hp.size,0);
		}
	}
	Print(&hp);
}
//测试topK问题
void TestTopk()
{
	int n = 1000000;
	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[2335] = 1000000 + 6;
	a[9999] = 1000000 + 7;
	a[76] = 1000000 + 8;
	a[423] = 1000000 + 9;
	a[3144] = 1000000 + 10;
	PrintTopK(int* a, n, 10);
}
int main()
{
	TestTopk();
	
	return 0;
}

在这里插入图片描述


找到即验证成功!!!!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

呋喃吖

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

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

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

打赏作者

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

抵扣说明:

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

余额充值