堆的概念、结构与应用详解

实现顺序结构二叉树------堆

是一种特殊完全二叉树 ,它不仅有完全二叉树的性质还具备其它特性,一般使用顺序结构的数组来存储数据。

1 堆的概念和结构
1.1 堆的概念

堆分为大根堆 (也称为最大堆)和小根堆(最小堆)。

  • 堆中某个结点的值总是不大于其父节点(大根堆)或不小于其父节点(小根堆)。(也可理解为父节点总是>=或<=左右孩子结点的值(孩子结点存在的话))
  • 堆总是一棵完全二叉树。

小根堆:

大根堆:

下面性质很重要,堆的一些函数实现会用到。堆也是二叉树,所以下面性质堆也有。

1.2 二叉树的性质(底层数组)

对于具有n个结点的完全⼆叉树,如果按照从上⾄下从左⾄右的数组顺序对所有结点从0号开始则对于序号为i的结点有:

  • i>0i位置结点的双亲序号:(i-1)/2i=0i为根结点编号,无双亲结点
  • 2i+1<n,左孩⼦序号:2i+12i+1>=n否则无左孩子
  • 2i+2<n,右孩子序号:2i+22i+2>=n否则无右孩子
1.2 堆的实现
1.2.1 Heap.h头文件的初始设置
c 复制代码
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
#include<time.h>
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* arr;
	int size;
	int capacity;
}HP;

typedef int HPDataTypetypedef重命名数据类型int,从而在替换堆的数据类型时就只用替换这条语句,实现代码可扩展性Heap堆结构包含一个指向数组的指针arr,堆中元素个数的size,堆的容量的capacity

1.2.2 堆的初始化
c 复制代码
void HPInit(HP* php)
{
	assert(php);
	php->arr = NULL;
	php->size = php->capacity = 0;
}

assert(php)判断php是否为NULL,如果为NULL则停止执行后面步骤并报错,将arr数组指针初始化为NULL,并将sizecapacity都置为0

1.2.3 堆的插入
c 复制代码
void HPPush(HP* php, HPDataType x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDataType* temp = (HPDataType*)realloc(php->arr, sizeof(HPDataType) * newcapacity);
		if (temp == NULL)
		{
			perror("realloc failed!");
			exit(1);
		}
		php->arr = temp;
		php->capacity = newcapacity;
	}
	php->arr[php->size] = x;
	//插入后要用向上调整算法
	AdjustUp(php->arr, php->size);
	php->size++;
}

堆的插入详细刨析

第一部分:

c 复制代码
	assert(php);
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDataType* temp = (HPDataType*)realloc(php->arr, sizeof(HPDataType) * newcapacity);
		if (temp == NULL)
		{
			perror("realloc failed!");
			exit(1);
		}
		php->arr = temp;
		php->capacity = newcapacity;
	}

assert(php)判断php是否为是否为NULL,插入的时候我们要用php->size == php->capacity判断数组是否还有多余空间插入,如果没有则用三目表达式int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity确定新容量newcapacity,并用realloc动态开辟函数扩容,并更新capacity的大小。

第二部分:

c 复制代码
php->arr[php->size] = x;

x插入数组末位即size下标位置

上面的部分只是将数据添加到数组里面了,但是并不能保证还是堆这个结构。所以我们必须用向上调整算法AdjustUp保证添加后仍是堆结构
AdjustUp函数如下:

c 复制代码
void AdjustUp(HPDataType* a, int child)
{
	while (child >0)//假如是根结点就不用移动
	{
		int parent = (child - 1) / 2;
		//小根堆<
		//大根堆>
		if (a[parent] > a[child])
		{
			//要在数组内部进行交换
			swap(&a[parent], &a[child]);
			child = parent;
		}
		else
		{
			break;
		}
	}
}

函数参数说明:a是数组首元素地址,child是新插入结点的下标。

首先用二叉树性质parent = (child - 1) / 2;找到最后一个尾节点的parent父结点,如果parent父节点>child子结点,说明不满足小根堆的结构性质,将父节点parent和子节点child交换即可。这样就可以保证在局部下是一个小根堆。

堆只要求了父节点和子节点之间的关系,并没有涉及到一个父节点的左右子结点之间的关系,所以我们只用判断该点和父节点关系就行了。

循环结束条件:child子节点更新到了根结点即child=0则不用比较了或者此时child结点与父结点parent符合堆的关系。所以外层大循环为while(child>0)

第三部分:

c 复制代码
php->size++;

最后将size++更新。

1.2.4 堆的删除

有元素才能进行删除,所以我们首先要写一个HPEmpty函数判断堆是否为空

c 复制代码
bool HPEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

size代表的就是堆的元素数目。

堆的删除代码实现:

c 复制代码
void HPPop(HP* php)
{
	assert(php && !HPEmpty(php));
	swap(&php->arr[0], &php->arr[php->size - 1]);
	php->size--;
	AdjustDown(php->arr, php->size, 0);
}

堆的删除是删除堆顶元素,删除堆顶元素的方法是将堆顶元素与最后一个元素互换,再将size--,并重新用向下调整算法AdjustDown将变换后的数组再次变成堆结构。

下面重点说明向下调整算法AdjustDown

特别说明:向下调整算法的左右子树必须满足堆结构

c 复制代码
void AdjustDown(HPDataType* a, int n, int parent)
{
	//找左右子树最小值与之交换
	int child = parent * 2 + 1;
	while (child < n)
	{
		//假设左孩子为较小值
		//小跟堆 a[child + 1] < a[child]
		//大根堆 a[child + 1] > a[child]
		if ((child + 1 < n)&&(a[child + 1] < a[child]))
		{
			child = child + 1;
		}
		//找到最小的孩子了
		//小根堆 a[parent] > a[child]
		//大根堆 a[parent] < a[child]
		if (a[parent] > a[child])
		{
			swap(&a[parent], &a[child]);
		}
		else
		{
			break;
		}
		parent = child;
		child = parent * 2 + 1;
	}
}

核心思想:以该结点为父节点,找到孩子左右结点的最小值,并与之交换。

堆的向下调整算法详解

第一部分:

c 复制代码
//找左右子树最小值与之交换
int child = parent * 2 + 1;
while (child < n)

先假设左孩子为左右孩子中的最小者(如果是建大堆的话则假设为最大者),循环结束条件为child<n,孩子child结点最大为n-1不能超过n

第二部分:

c 复制代码
if ((child + 1 < n)&&(a[child + 1] < a[child]))
	{
		child = child + 1;
	}

child + 1 < n判断右结点是否存在,如果存在右结点并且它比左结点要小则假设不成立,将最小结点变为右结点child = child + 1(即是由左结点变为右结点)。

第三部分:

c 复制代码
	if (a[parent] > a[child])
		{
			swap(&a[parent], &a[child]);
		}
		else
		{
			break;
		}

这里的swap函数必须地址从而实现两个数本身的互换

c 复制代码
void swap(HPDataType* a, HPDataType* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}

将最小结点child与父结点parent比较,如果符合a[parent] > a[child],两个数组元素就互换位置。否则代表堆已经排好了,直接break跳出循环就行了。

第四部分:

c 复制代码
parent = child;
child = parent * 2 + 1;

改变父节点parent和最小结点child的值

1.2.5 返回堆顶数据
c 复制代码
HPDataType HPTop(HP* php)
{
	assert(php && !HPEmpty(php));
	return php->arr[0];
}

php指针不为NULL并且堆不为空assert(php && !HPEmpty(php)),直接返回第一个元素就是堆顶数据php->arr[0]

1.2.6 堆的元素数
c 复制代码
int HPSize(HP* php)
{
	assert(php);
	return php->size;
}

直接返回php->size

1.3 堆的应用
1.3.1 堆排序
c 复制代码
void HeapSort(int* arr, int n)
{
	//向下调整算法建堆
	for(int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, n, i);
	}
	//向上调整算法建堆
	//for (int i = 0; i < n; i++)
	//{
		//AdjustUp(arr, i);
	//}
	int end = n - 1;
	while (end > 0)
	{
		swap(&(arr[0]), &(arr[end]));
		AdjustDown(arr,end, 0);
		end--;
	}
}

第一步部分:

c 复制代码
	//向下调整算法建堆
	for(int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, n, i);
	}
	//向上调整算法建堆
	//for (int i = 0; i < n; i++)
	//{
		//AdjustUp(arr, i);
	//}

先用向上调整算法和向下调整算法将数组构成一个堆结构

向上调整算法建堆思想:先找到最后一个结点的父结点,将这一个父节点的局部向下调整建成一个最小堆,再parent--将父节点转移,从而将一个个父节点为根结点的树建成堆结构,直到最后一个整个树的根结点作为父结点parent

向下调整算法思想:从数组第一个元素开始,通过向下调整建成局部堆,并将数组元素逐渐加入,并最后建成一个完整的堆。

第二部分:

c 复制代码
    int end = n - 1;
	while (end > 0)
	{
		swap(&(arr[0]), &(arr[end]));
		AdjustDown(arr,end, 0);
		end--;
	}

我建的是小根堆所以数组第一个元素就是整个数组的最小值,将数组的最小值与数组最后一个元素交换swap(&(arr[0]), &(arr[end])),这样就将最小的元素放到最后面,并再次向下调整AdjustDown(arr,end, 0)确保这是一个堆结构(此时堆顶的数据就是第二小的元素了),并将end--,更新未被排序的最后一个数组元素小标和未被排序的元素数量。这样就能将小的元素放在后面从而形成降序排列
如果想升序排列则建大根堆,将堆顶最大的元素放在后面。

1.3.2 TOP-K问题

TOP-K问题:即求数据结合中前K个最⼤的元素或者最⼩的元素,⼀般情况下数据量都⽐较⼤。

⽐如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

对于Top-K问题,能想到的最简单直接的⽅式就是排序,但是:如果数据量⾮常⼤,排序就不太可取了

(可能数据都不能⼀下⼦全部加载到内存中)。最佳的⽅式就是⽤堆来解决,基本思路如下:

1)⽤数据集合中前K个元素来建堆

k个最⼤的元素,则建⼩堆

k个最⼩的元素,则建⼤堆

2)⽤剩余的N-K个元素依次与堆顶元素来⽐较,不满⾜则替换堆顶元素

将剩余N-K个元素依次与堆顶元素⽐完之后,堆中剩余的K个元素就是所求的前K个最⼩或者最⼤的元素

c 复制代码
void CreateNDate()
{
	//造数据
	int n = 100000;
	srand(time(0));
	FILE* pf = fopen("data.txt", "w");
	if (pf == NULL)
	{
		perror("flie open failed!");
		exit(1);
	}
	for (int i = 0; i < n; i++)
	{
		int x = rand() % 100000 + 1;
		fprintf(pf, "%d\n", x);
	}
	fclose(pf);
}
//我建的是小堆,可以找最大的k个数
void topk()
{
	int k = 0;
	int num = 0;
	printf("请输入k:>");
	scanf("%d", &k);
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("file open failed!");
		exit(1);
	}
	//将k个数据导入到数组中
	int* minHeap = (int*)malloc(sizeof(int) * k);
	for (int i = 0; i < k; i++)
	{
		fscanf(pf, "%d", &minHeap[i]);
	}
	//建立小根堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(minHeap, k, i);
	}
	while (fscanf(pf, "%d", &num)!= EOF)
	{
		if (num > minHeap[0])
		{
			minHeap[0] = num;
		}
		AdjustDown(minHeap, k, 0);
	}
	fclose(pf);
	for (int i = 0; i < k; i++)
	{
		printf("%d ", minHeap[i]);
	}
	
}

核心代码:

c 复制代码
while (fscanf(pf, "%d", &num)!= EOF)
	{
		if (num > minHeap[0])
		{
			minHeap[0] = num;
		}
		AdjustDown(minHeap, k, 0);
	}

建的是小根堆,则堆顶是最小 的元素,如果data.txt里面的元素比堆顶的数据大则替换,并再次向下调整保证堆结构,这样我们一次次的将堆结构里面最小的元素替换掉,则里面剩余的就是最大k个元素了。

相关推荐
aloha_78911 小时前
力扣hot100做题整理91-100
数据结构·算法·leetcode
Tiny番茄11 小时前
31.下一个排列
数据结构·python·算法·leetcode
挂科是不可能出现的11 小时前
最长连续序列
数据结构·c++·算法
_Aaron___11 小时前
List.subList() 返回值为什么不能强转成 ArrayList
数据结构·windows·list
一念&12 小时前
每日一个C语言知识:C 结构体
c语言·开发语言
GilgameshJSS12 小时前
STM32H743-ARM例程24-USB_MSC
c语言·arm开发·stm32·单片机·嵌入式硬件
码农多耕地呗13 小时前
力扣146.LRU缓存(哈希表缓存.映射+双向链表数据结构手搓.维护使用状况顺序)(java)
数据结构·leetcode·缓存
小莞尔13 小时前
【51单片机】【protues仿真】基于51单片机电压测量多量程系统
c语言·单片机·嵌入式硬件·物联网·51单片机
晚枫~13 小时前
数据结构基石:从线性表到树形世界的探索
数据结构