【数据结构之堆的实现】

数据结构学习笔记---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)

//交换
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

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

你可能感兴趣的:(数据结构,数据结构,笔记,堆,小根堆,大根堆,堆的基本操作,二叉树)