【数据结构之堆的实现】

这篇具有很好参考价值的文章主要介绍了【数据结构之堆的实现】。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

数据结构之堆

前言:
前篇学习了 数据结构之树和二叉树的基本概念知识,那么这篇继续学习怎么实现基本的操作。所以先建议看完上篇知识点,才有助于消化知识和理解。

/知识点汇总/

1、堆的概念和结构

概念:堆(Heap)是计算机科学中一类特殊的数据结构,是最高效的优先级队列。堆通常是一个可以被看作一棵完全二叉树的数组对象。
堆总是满足下列性质

1.堆中某个结点的值总是不大于或不小于其父结点的值;
2.堆总是一棵完全二叉树。
3.堆的物理结构本质上是顺序存储的,是线性的。但在逻辑上不是线性的,是完全二叉树的这种逻辑储存结构。

将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。
一般是把数组数据看作一颗完全二叉树,并有以下要求:

小堆:任意一个父亲结点 <= 孩子结点
大堆:任意一个父亲结点 >= 孩子结点

1.1、如何实现堆?

树/堆/二叉树的存储结构
通过前篇知识点就可以清楚的知道,有三种存储形式。不管哪种方式,取决于实际应用场景和需求即可
方法一:

#define N 6
struct TreeNode
{
	int val;
	struct TreeNode* childArr[N];//指针数组
};

方法二:

struct TreeNode
{
	int val;
	SeqList childSL;//顺序表
	//SeqList,C++的库可调用
};

方法三,最优方法:左孩子右兄弟表示法

struct TreeNode
{
	int val;
	struct TreeNode* leftChild;
	struct TreeNode* rightBother;
};

那么接下来根据堆最常采用普遍的数组形式,定义结构体成员和变量进行学习。

typedef int HPDataType;

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

另外,书上还有常见的存储形式如下所示:

#define MaxSize 100
typedef char DataType;
typedef struct
{
	DataType data[MaxSize];
	int bitTreeNum; //结点个数
}SeqBitTree;

2、堆的实现

2.1、堆的Heap.h

void HeapInit(HP* php);

void HeapDestory(HP* php);

//向上调整法
void HeadPush(HP* php, HPDataType x);

void HeapPop(HP* php);//规定默认删除堆顶的数据,即根结点
//因为,堆顶被删除,那么小堆就是删除最小值,大堆就是删除最大值。
//删除后就是次大值,次小值,为排序这些操作做铺垫。
//而且尾删的代价很低,容易操作。

HPDataType HeapTop(HP* php);

size_t HeapSize(HP* php);

bool HeapEmpty(HP* php);

void HeapSort(int* a, int n);

void Swap(HPDataType* p1, HPDataType* p2);

void AdjustDown(int* a, int size, int parent);

void AdjustUp(HPDataType* a, int child);

2.2、堆的Heap.c

2.2.1、堆的初始化

堆的初始化和前面学过的栈的初始化几乎相同,其次初始化一般都是先置空,操作简单。

void HeapInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->size = 0;
	php->capacity = 0;
}
2.2.2、堆销毁

这里使用的指针数组,那么相应的销毁,肯定就需要对应的释放开辟的空间,对防止野指针或者其它的内存泄漏等情况的处理。

void HeapDestory(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->size = 0;
	php->capacity = 0;
}
2.2.3、堆的基本操作

堆的核心操作就是涉及到怎么取得根结点数据,以及叶子结点之间的关系。那么结合大根堆和小根堆的思想,那要解决此类问题,不妨封装两个功能函数,一个负责把数据向上排序调整,另一个负责把数据向下排序调整,即,建大堆:升序,建小堆:降序,大小堆的区别就是调整上调下调,本质算法思路一致。交换其中的顺序就是升序和逆序的区别。

2.2.3.1、核心函数AdjustUp()向上调整功能函数

首先,结合上篇知识点中,二叉树的基本性质其中的父子结点的位置关系,得到parent = (child - 1) / 2,写法多种但目的就是比较大小,将大的数据给父结点。直到child≤0结束循环,因为此时到的根节点了,没有比较对象了。

//交换
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向上调整
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] < a[parent])//小于就交换
		{
			Swap(&a[child], &a[parent]);//交换数据
			//上调父亲和孩子的位置
			//写法1:
			child = parent;
			//写法2:child = parent;
			//写法3:child = (child-1)/2;
			parent = (child - 1) / 2;
		}
		else//大于等于父亲,停止交换break
		{
			break;
		}
	}
}
2.2.3.2、核心函数AdjustDown()向下调整功能函数

向下调整就是建小堆的思想,把小的数据向根结点挪动,起初不知道左右孩子的大小,采用假设法,初步比较大小,如果假设错误就交换假设对象即可,目的就是保证child变量存放的是为较小孩子,才能继续后面的交换,接下来就得考虑是否有两个孩子,所以判断语句if((child+1) <size && a[child + 1] < a[child])要考虑全面,然后进行父子结点得比较,把小的向根结点挪动即可,直到child≥size结束循环。

//交换
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向下调整
void AdjustDown(int* a, int size, int parent)
{
	//假设法,假设左孩子小,如果假设错了,就交换为右孩子小
	//根本目的就是保证child为较小孩子
	int child = parent * 2 + 1;
	while (child < size)
	{
		//假设错误,则交换
		//if (a[child + 1] < a[child])//但是这样写,存在一定的问题,能保证有左孩子,不能保证右孩子的情况
		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;
		}
	}
}
2.2.4、堆的插入操作

基于核心函数的理解,那么堆的插入操作就是调用之前,判断一下容量是否满即可。

void HeadPush(HP* php, HPDataType x)
{
	assert(php);
	//检测容量
	if (php->size == php->capacity)
	{
		size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			//return;
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newCapacity;
	}
	//判满
	php->a[php->size] = x;
	php->size++;
	//二叉树/堆,插入后需要向上调整 -- 封装一个函数AdjustUp()
	AdjustUp(php->a, php->size-1);//因为size++了,所以插入数据的下标就是size-1
}
2.2.5、堆的删除操作

堆的删除基于核心函数AdjustDown()删除掉根节点即可。

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,0);
}
2.2.6、取堆的根节点

不管大堆还是小堆,a[0]放的都是根结点数据,直接return即可。

HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(php->size > 0);
	return php->a[0];//a[0]始终放的最值
}
2.2.7、取堆的结点个数

判断大小或个数多少,当时定义结构体成员时定义了size,那么此时就体现了优势,直接返回size的大小即可。

size_t HeapSize(HP* php)
{
	assert(php);
	return php->size;
}
2.2.8、堆的判空

同理判断空,当时定义结构体成员时定义了size,那么此时就体现了优势,直接返回size的大小即可。

bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size;
}
2.2.9、堆的排序

基于上面的操作,实现堆的排序就是大根堆和小根堆,建大堆就是升序,建小堆就是降序,但是有了核心函数,可以根据下标之间的关系,只用AdjustDown函数中的比较的大小关系,就可以操作升序和降序了。

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--) //(n-1-1)/2 -- (n-1)是下标
	{
		AdjustDown(a, n, 1);
	}

	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

2.3、堆的main.c

#include "Heap.h"

//测试一:
void TestHp1(void)
{
	int a[8] = { 4,6,2,1,5,8,2,9 };
	HP hp;
	HeapInit(&hp);
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	{
		HeadPush(&hp, a[i]);
	}
	//实现:top k
	//int k = 3;
	//while (k--)
	//{
	//	printf("%d ", HeapTop(&hp));
	//	HeapPop(&hp);
	//}

	//小堆实现方法一:升序
	while (HeapEmpty(&hp))
	{
		printf("%d ", HeapTop(&hp));
		HeapPop(&hp);
	}
	printf("\n");
	HeapDestory(&hp);
	return 0;
}

//测试二:
void TestHp2(void)
{
	int a[8] = { 4,6,2,1,5,8,2,9 };
	HeapSort(a, sizeof(a) / sizeof(a[0]));
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
	return 0;
}

int main()
{
	//TestHp1();
	TestHp2();
}

3、堆的top K问题的延申

问题探究:N个数里面找最大前K个,(N远大于K)
小数据很好解决,那么对于,假如N为100亿,k为10(100亿数据量相当于40G),该如何处理top k问题呢?

思路1:
N个数插入到堆里面,Pop k次
时间复杂度:NlogN+klogN -->O(NlogN)
此题,对于思路1,就是存在内存不足
提供思路2:
步骤1.读取前k个值,建立k个数的小堆
步骤2.依次再读取后面的值,跟堆顶比较,如果比堆顶大,替换堆顶然后进堆(替换堆顶值,再向下调整算法)
这里利用小堆的特点,大的值被替换为堆顶后,会执行向下调整算法,将大的值不断向叶子节点沉下去
时间复杂度:O(N*logk)

3.1、top k 问题实现

#include "Heap.h"

void CreateNDate()
{
	//造数据
	int n = 1000000;
	srand((unsigned int)time(0));
	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) % 1000000;
		fprintf(fin, "%d\n", x);//方便,写入文件·方便后面的读出以空格或换行作为分割。
	}
	fclose(fin);
}

void PrintTopk(const char* file, int k)
{
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return;
	}
	//建k个数的小堆
	int* minheap = (int*)malloc(sizeof(int) * k);
	if (minheap == NULL)
	{
		perror("malloc error");
		return;
	}
	//读取前k个,建小堆
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &minheap[i]);
		AdjustUp(minheap, i);
	}
	int x = 0;
	while (fscanf(fout, "%d", &x) != EOF)
	{
		if (x > minheap[0])
		{
			minheap[0] = x;
			AdjustDown(minheap, k, 0);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", minheap[i]);
	}
	printf("\n");
	free(minheap);
	fclose(fout);
}
int main()
{
	//CreateNDate();
	PrintTopk("data.txt",5);
	return 0;
}

4、建堆时间复杂度分析

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

//满二叉树:
//第一层, 2^0个结点,需要向下移动 h-1层;
//第二层, 2^1个结点,需要向下移动 h-2层;
//第三层, 2^2个结点,需要向下移动 h-3层;
//第四层, 2^3个结点,需要向下移动 h-4层;
//…
//第h-1层, 2^(h-2)个结点,需要向下移动 1层;
//第h层,2^(h-1)个结点

//向下调整建堆的累积调整次数是T(h)
//T(h) = 2^(h-2)*1 + 2(h-3)*2+…+21(h-2)+2^0(h-1)
//错位相减法:
//T(h) = 2^(h-1)1 + 2(h-3)+…+21 - 2^0(h-1)
//T(h) = 2^(h-1) +2^(h-2) + 2(h-3)+…+20 - h
//T(h) = 2^h-1 - h

//结合,h是树的高度,N是树的节点个数:
//满二叉树:2^(h-1) = N --> h = log(N+1)
//得到:
//T(h) = 2^h-1-h --> T(N) = N -log(N+1)
//向上调整建堆的累积调整次数是T(h)
//T(h) = 2^11 + 2^22 + 2^33 +…+2^(h-2)(h-2) + 2^(h-1)*(h-1)
//所以衡量对比知道:向上调整劣于向下调整
//T(h) = N + (N+1)*log((N+1)-1) + 1

结语:下篇就根据排序 — 算法效率优化等问题,学习算法思想的巩固。文章来源地址https://www.toymoban.com/news/detail-805848.html

到了这里,关于【数据结构之堆的实现】的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 数据结构之堆的应用

    在我们学习了堆之后我们知道,无论是大堆还是小堆,堆顶的元素要么最大,要么最小。 对于堆插入的时间复杂度为O(logn)也就是向上调整算法的复杂度,删除一个堆中的元素为O(logn),堆在我们日常生活中还是常用到的。 目录 1.top k问题(优质筛选问题) 2.堆排序 1.向

    2024年02月08日
    浏览(37)
  • 数据结构学习分享之堆的详解以及TopK问题

    💓博主CSDN主页:杭电码农-NEO💓   ⏩专栏分类:数据结构学习分享⏪   🚚代码仓库:NEO的学习日记🚚   🌹关注我🫵带你了解更多数据结构的知识   🔝🔝 本章就给大家带来久违的堆的知识,如果你还不知道数的相关知识,或者什么是完全二叉树,请跳转 树的介绍, 本章的堆结

    2024年02月05日
    浏览(71)
  • 数据结构之堆——算法与数据结构入门笔记(六)

    本文是算法与数据结构的学习笔记第六篇,将持续更新,欢迎小伙伴们阅读学习。有不懂的或错误的地方,欢迎交流 当涉及到高效的数据存储和检索时,堆(Heap)是一种常用的数据结构。上一篇文章中介绍了树和完全二叉树,堆就是一个完全二叉树,可以分为最大堆和最小

    2024年02月11日
    浏览(33)
  • 【数据结构初阶】之堆(C语言实现)

    前言 :在二叉树基础篇我们提到了二叉树的顺序实现,今天让我们来学习一下特殊的二叉树———堆的相关知识。 📃 博客主页: 小镇敲码人 💞 热门专栏:数据结构与算法 🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏 🌏 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月

    2024年04月09日
    浏览(74)
  • 数据结构:堆的实现

    如果有一个关键码的集合 K = { k1 ,k2 ,k3 ,…,kn },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并且 k(i)  k(i*2+1) 和 k(i)  k(i*2+2), i = 0 , 1 , 2…,则称为小堆 ( 或大堆 ) 。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小

    2024年02月13日
    浏览(27)
  • 数据结构---堆的实现

    前言 一、什么是堆? 二、 堆的实现 1. 堆的结构  2. 接口实现 总结 堆(Heap)是计算机科学中一类特殊的数据结构,是最高效的优先级队列。堆通常是一个可以被看做一棵完全二叉树的数组对象。 现实中我们通常把 堆(一种二叉树) 使用 顺序结构的数组 来存储, 大根堆 :根节

    2024年02月02日
    浏览(29)
  • 数据结构—小堆的实现

    前言:前面我们已经学习了二叉树,今天我们来学习堆,堆也是一个二叉树,堆有大堆有小堆,大堆父节点大于子节点,小堆父节点总小于子节点,我们在学习C语言的时候也有一个堆的概念,那个堆是操作系统中的堆,与我们今天所学的堆全然不同。我们就来实现下小堆。

    2024年02月05日
    浏览(28)
  • 数据结构:堆的实现(C实现)

    个人主页 : 个人主页 个人专栏 : 《数据结构》 《C语言》 当一颗完全二叉树用顺序表来存储时,其被称为堆。 堆总是一棵完全二叉树 堆的某个节点的值总是不大于(大堆)或不小于(小堆)其父节点的值 其可以被用来解决top k 问题 或 堆排序 下面就是要实现的堆的功能 重点在

    2024年02月13日
    浏览(30)
  • 【数据结构】堆的实现及应用

    简单不先于复杂,而是在复杂之后 。 1.1 二叉树的顺序结构 普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。 而完全二叉树更适合使用顺序结构存储。 现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系

    2024年02月21日
    浏览(37)
  • 数据结构——二叉树(堆的实现)

    目录   树概念及结构 树的相关概念 树的表示  二叉树的概念及结构   堆 堆的实现   结构体建立 初始化   添加元素  打印堆  删除堆首元素  返回首元素  判断是否为空 空间销毁  刷题找工作的好网站——牛客网 牛客网 - 找工作神器|笔试题库|面试经验|实习招聘内推,

    2024年02月11日
    浏览(32)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包