【数据结构之堆的实现】

数据结构学习笔记---008

  • 数据结构
    • 1、的概念和结构
      • 1.1、如何实现
    • 2、的实现
      • 2.1、的Heap.h
      • 2.2、的Heap.c
        • 2.2.1、的初始化
        • 2.2.2、销毁
        • 2.2.3、的基本操作
          • 2.2.3.1、核心函数AdjustUp()向上调整功能函数
          • 2.2.3.2、核心函数AdjustDown()向下调整功能函数
        • 2.2.4、的插入操作
        • 2.2.5、的删除操作
        • 2.2.6、取的根节点
        • 2.2.7、取的结点个数
        • 2.2.8、的判空
        • 2.2.9、的排序
      • 2.3、的main.c
    • 3、的top K问题的延申
      • 3.1、top k 问题实现
    • 4、建时间复杂度分析

数据结构

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

/知识点汇总/

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

结语:下篇就根据排序 — 算法效率优化等问题,学习算法思想的巩固。


http://www.niftyadmin.cn/n/5325165.html

相关文章

Asp .Net Core 系列:基于 Swashbuckle.AspNetCore 包 集成 Swagger

什么是 Swagger? Swagger 是一个规范和完整的框架&#xff0c;用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。它提供了一种规范的方式来定义、构建和文档化 RESTful Web 服务&#xff0c;使客户端能够发现和理解各种服务的功能。Swagger 的目标是使部署管理和使用功…

第九部分 使用函数 (三)

目录 一、文件名操作函数 1、dir 2、notdir 3、suffix 4、basename 5、addsuffix 6、addprefix 7、join 一、文件名操作函数 下面我们要介绍的函数主要是处理文件名的。每个函数的参数字符串都会被当做一个或是 一系列的文件名来对待。 1、dir $(dir <names..>…

UML-通信图和交互概览图(通信图和顺序图的区别与联系)

UML-通信图和交互概览图&#xff08;通信图和顺序图的区别与联系&#xff09; 一、通信图简介1.消息2.链接 二、通信图和[顺序图](https://blog.csdn.net/weixin_65032328/article/details/135587782)的联系与区别三、交互概览图四、顺序图转化为通信图练习 一、通信图简介 通…

ARCGIS PRO Annotation 专业概念及操作

使用Annotation的API功能。Annotation 的API功能位于ArcGIS.Core.dll中。Annotation API通常与地理数据库、地图创作和编辑结合使用。ArcGIS.Core.dll ArcGIS.Core.Data.map API中的几乎所有方法都应该在MCT上调用。 一、Annotation featureclass 1、从GeodatabaseGeodatabase数…

Leetcode 第 380 场周赛 Problem C 价值和小于等于 K 的最大数字(Java + 二分答案 + 规律)

文章目录 题目思路Java 二分答案 规律第 1 步&#xff1a;第 2 步&#xff1a;第 3 步&#xff1a;第 4 步&#xff1a; 复杂度Code 题目 Problem: 100160. 价值和小于等于 K 的最大数字给你一个整数 k 和一个整数 x 。令 s 为整数 num 的下标从 1 开始的二进制表示。我们说…

半监督学习 - 半监督支持向量机(Semi-Supervised Support Vector Machines)

什么是机器学习 半监督支持向量机&#xff08;Semi-Supervised Support Vector Machines&#xff0c;S3VM&#xff09;是支持向量机&#xff08;SVM&#xff09;的一种扩展&#xff0c;旨在处理训练数据中只有少量样本被标记的情况。与传统的监督SVM不同&#xff0c;S3VM通过结…

LeeCode前端算法基础100题(21) 同构字符串

一、问题详情: 给定两个字符串 s 和 t ,判断它们是否是同构的。 如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。 每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字…

CTF伪随机数爆破

要了解伪随机数的爆破首先你的先知道什么是PHP种子&#xff0c; 借用在rand()函数中&#xff0c;我们可以通过设置随机数种子来影响随机数的生成。例如&#xff0c;在rand()函数中加入了随机数种子编码后&#xff0c;每次运行程序将会生成同样的随机整数序列。这个就是伪随机数…