【数据结构】堆

一、堆的概念及结构

有一个集合 K = {k0 , k1 , k2 , … , kn-1} ,把它的所有元素按完全二叉树顺序存储方式存储在一 个一维数组中 ,并满足: Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >=K2i+2) i = 0 , 1 , 2… ,(即双亲比孩子的数值小(大))则称为小堆 ( 或大堆) 。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质:堆中某个节点的值总是不大于或不小于其父节点的值;堆的逻辑结构是一棵完全二叉树。

在这里插入图片描述

二、堆的实现

1. 结构的定义

堆的元素按完全二叉树的顺序存储方式存储在一维数组中,所以堆的结构和顺序表的结构一样。

typedef int HeapDataType;

typedef struct heap
{
	HeapDataType* data;
	int size;
	int capacity;
}heap;

2. 堆的初始化

和顺序表一样,先在Init函数外创建一个堆变量,再把堆的地址传进初始化函数,创建一个空堆。

void InitHeap(heap* ph)
{
	assert(ph);
	ph->data = NULL;
	ph->capacity = 0;
	ph->size = 0;
}

3. 堆的插入

堆只能在尾部进行插入,由于堆要求在插入元素后仍保持堆的性质,所以我们需要对堆进行向上调整,把新插入的元素调整到正确的位置,向上调整的过程其实也是建堆的过程。

//小根堆
void HeapPush(heap* ph, HeapDataType x)
{
	assert(ph);
	if (ph->capacity == ph->size) //扩容
	{
		int newcapacity = ph->data == NULL ? 4 : ph->capacity * 2;
		HeapDataType* temp = (HeapDataType*)realloc(ph->data, sizeof(HeapDataType) * newcapacity);
		if (temp == NULL)
		{
			perror("realloc");
			exit(-1);
		}
		ph->data = temp;
		ph->capacity = newcapacity;
	}
	ph->data[ph->size] = x;
    //向上调整,保持堆结构
	AdjustUp(ph->data, ph->size);
	ph->size++;
}

4. 向上调整

这里以小根堆为例,如图:假设现在我们已经有了一个小根堆,现在我们往堆尾插入一个元素,那么可能会出现两种情况:

在这里插入图片描述

1、插入的元素大于父节点,此时我们的堆仍保持小根堆结构,所以不需要改动;比如我们往堆中插入30;

在这里插入图片描述

2、插入的元素小于父节点;这种情况下我们就需要把该节点不断往上调整,直到把堆调整为小根堆,最坏的情况是该节点被调整为根节点,比如我们插入10;

在这里插入图片描述

void Swap(HeapDataType* a, HeapDataType* b)
{
	assert(a && b);
	HeapDataType temp = *a;
	*a = *b;
	*b = temp;
}

//向上调整 - 小堆
void AdjustUp(HeapDataType* nums, int child)
{
	assert(nums);
	while (child > 0)//最坏调整到根节点停止
	{
		int parents = (child - 1) / 2;//父节点下标
		if (nums[child] < nums[parents]) //交换条件
		{
			Swap(&nums[child], &nums[parents]);
			child = parents;          //子节点取代父节点下标
		}
		else //否则直接退出,此时已经满足堆结构
		{
			break; 
		}
	}
}

如果我们需要调整大根堆,只需要把交换的条件修改一下即可。

if(nums[child] > nums[parents])

5. 堆的删除

对于堆的删除有明确的规定:我们只能删除堆顶的元素;但是头删之后存在两个问题:

1、顺序表头删需要挪动数据,效率低下;

2、挪动数据之后堆中各节点的父子关系完全破坏,很明显不满足堆的结构了;

对于上面的这些问题,我们有如下解决办法:

1、我们在删除之前先将堆顶和堆尾的元素交换,然后让size减一,这样相当于删除了堆顶的元素,且效率达到了O(1);

2、由于我们把堆尾元素交换到了堆顶,堆的结构遭到了局部破坏,所以需要设计一个向下调整算法来保持堆的结构;

//小根堆
void HeapPop(heap* ph)
{
	assert(ph && ph->size > 0);
	Swap(ph->data, ph->data + ph->size - 1);
	ph->size--;
	AdjustDown(ph->data, ph->size, 0);
}

6. 向下调整

堆向下调整的思路和向上调整刚好相反 (还是以小根堆为例):

1、找出子节点中较小的节点;2、比较父节点与较小子节点,如果父节点大于较小子节点则交换两个节点;3、交换之后,原来的子节点成为新的父节点,然后继续 1 2 步骤,直到调整为堆的结构。

注意

  1. 向下调整算法比向上调整多一个参数size,因为向下调整需要判断子节点不能超过堆的范围

  2. 向下调整算法有一个前提:左右子树必须都是堆,才能调整。

在这里插入图片描述

void Swap(HeapDataType* a, HeapDataType* b)
{
	assert(a && b);
	HeapDataType temp = *a;
	*a = *b;
	*b = temp;
}

//向下调整 - 小堆
void AdjustDown(HeapDataType* nums, int size, int sub)
{
	assert(nums);
	int parents = sub;
	int minchild = parents * 2 + 1;//先默认最小孩子为左孩子
	//当子结点超过堆的范围就结束
	while (minchild < size)
	{
        //选出真正的较小子节点
		if (minchild + 1 < size && nums[minchild + 1] < nums[minchild])
		{
			minchild++;
		}
		//如果父节点大于最小的子结点,就交换双方位置
		if (nums[parents] > nums[minchild])
		{
			Swap(nums + parents, nums + minchild);
			//迭代
			parents = minchild;
			minchild = parents * 2 + 1;
		}
		//否则不用向下调整,直接跳出循环
		else
		{
			break;
		}
	}
}

和向上调整类似,如果我们想要向下调整为大堆,也只需要改变交换条件:

//选出真正的较大子节点
if (minchild + 1 < size && nums[minchild + 1] > nums[minchild])
//如果父节点小于最大的子结点,就交换双方位置
if (nums[parents] < nums[minchild])

7、取出堆顶的元素

HeapDataType HeapTop(heap* ph)
{
	assert(ph && ph->size > 0);
	return ph->data[0];
}

8、返回堆的元素个数

int HeapSize(heap* ph)
{
	assert(ph);
	return ph->size;
}

9、判断堆是否为空

bool IsHeapEmpty(heap* ph)
{
	assert(ph);
	return ph->size == 0;
}

10、打印堆中的数据

void PrintHeap(heap* ph)
{
	assert(ph);
	for (int i = 0; i < ph->size; i++)
	{
		printf("%d ", ph->data[i]);
	}
	printf("\n");
}

11、堆的销毁

void DestroyHeap(heap* ph)
{
	assert(ph);
	free(ph->data);
	ph->capacity = 0;
	ph->size = 0;
}

三、堆的应用

1. 堆排序

堆排序是选择排序的一种,它的时间复杂度为 O(N*logN),空间复杂度为 O(1),是一种十分优秀的排序算法,主要步骤就是建堆和选择数

建堆

建堆有两种方法:向上调整建堆和向下调整建堆。

向上调整建堆: 把数组的第一个元素作为堆的根节点,然后把其余元素看作插入结点在堆末尾插入,每插入一个元素就向上调整一次,从而始终保证堆的结构;

在这里插入图片描述

向上调整建堆的时间复杂度: 由于堆是完全二叉树,而满二叉树是完全二叉树的一种,所以此处为了简化计算,使用满二叉树来求时间复杂度

在这里插入图片描述

如上图,我们把每一层的节点个数乘以每一个节点需要调整的次数,最后再求和,就可以得到一共需要调整的次数;然后再根据满二叉树节点总数与树的高度的关系将表达式中的h替换掉,最终可以得到向上调整建堆的时间复杂度为:O(N*logN);

向下调整建堆: 从倒数第一个非叶子节点 (即最后一个叶子节点的父节点) 开始向下调整,直到调整到根。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dois3qci-1692531353764)(C:\Users\86185\AppData\Roaming\Typora\typora-user-images\image-20230818180009394.png)]

向下调整建堆的时间复杂度: 使用满二叉树来求时间复杂度

在这里插入图片描述

如上图,向下调整建堆的时间复杂度为:O(N),比向上调整的效率更高,这似乎有点反直觉,因为两者看起来并没有什么本质的不同,无非就是方向相反,但是简单分析就能发现原因,因为向下调整随着调整次数的增大,需要调整的结点越来越少,两者相乘就不会特别大,另外最后一层(接近一半的结点)是不需要调整的。

综合上面两种建堆方法,最好选择向下调整建堆,建堆的时间复杂度为:O(N)

选数

建堆完成后,接下来就是选数,假设现在我们要排降序,那么方法一共有两种:

1、建大堆:开辟一个和原数组同等大小的新数组,然后对原数组建大堆,每次取出堆顶的元素 (最大的元素) 按顺序放在新数组中,最后再将新数组中的数据覆盖至原数组;

缺点:需要开辟额外的空间,删除堆顶元素后向下调整的时间复杂度是O(logN),所以嵌套后时间复杂度:O(N*logN),空间复杂度:O(N);

2、建小堆:先对原数组建小堆,然后将堆顶和堆尾的数据进行交换,使得数组中最小的元素处于数组末尾,然后向下调整前n - 1个元素,使得次小的数据位于堆顶,最后重复前面的步骤,把次小的数据存放到最小的数据之前,直到数组有序;

优点:没有额外的空间消耗,且时间复杂度达到了 O(N*logN);

综合上面两种选数的方法,选数的时间复杂度为:O(N*logN),空间复杂度为:O(1);

void Swap(HeapDataType* a, HeapDataType* b)
{
	assert(a && b);
	HeapDataType temp = *a;
	*a = *b;
	*b = temp;
}

//向下调整 - 小堆
void AdjustDown(HeapDataType* nums, int size, int sub)
{
	assert(nums);
	int parents = sub;
	int minchild = parents * 2 + 1;//先默认最小孩子为左孩子
	//当子结点超过堆的范围就结束
	while (minchild < size)
	{
        //选出真正的较小子节点
		if (minchild + 1 < size && nums[minchild + 1] < nums[minchild])
		{
			minchild++;
		}
		//如果父节点大于最小的子结点,就交换双方位置
		if (nums[parents] > nums[minchild])
		{
			Swap(nums + parents, nums + minchild);
			//迭代
			parents = minchild;
			minchild = parents * 2 + 1;
		}
		//否则不用向下调整,直接跳出循环
		else
		{
			break;
		}
	}
}

//升序 -> 建大根堆
//降序 -> 建小根堆
void HeapSort(int* nums, int numsSize)
{
	//建堆method1:插入法 向上调整 O(N * logN)
	/*for (int i = 1; i < numsSize; i++)
	{
		AdjustUp(nums, i);
	}*/
	
	//建堆method2:向下调整 O(N)
	for (int i = (numsSize - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(nums, numsSize, i);
	}
    
    //选数
	//降序 -> 小根堆 -> 向下调整 O(N * logN) 
	while (numsSize > 1)
	{
		Swap(nums, nums + numsSize - 1);
		AdjustDown(nums, --numsSize, 0);
	}
}

int main()
{
	int nums[] = { 10, 26, 85, 4, 23, 14, 12, 36, 16 };
	HeapSort(nums, sizeof(nums) / sizeof(int));
	for (int i = 0; i < sizeof(nums) / sizeof(int); i++)
	{
		printf("%d ", nums[i]);
	}
	printf("\n");
    return 0;
}

在这里插入图片描述

2. Topk问题

TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大,无法排序;比如:世界500强、富豪榜、王者荣耀巅峰前十等。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是,如果数据量非常大,排序就不太可取了 (数据都不能一下子全部加载到内存中),最佳的方式就是用堆来解决,基本思路如下:

  1. 用数据集合的前K个元素来建堆, 求前k个最大的元素,则建小堆,反之则建大堆;

  2. 用剩余的N-K个元素依次与堆顶元素来比较,满足条件则替换堆顶元素,并向下调整保持堆的结构;

void Swap(HeapDataType* a, HeapDataType* b)
{
	assert(a && b);
	HeapDataType temp = *a;
	*a = *b;
	*b = temp;
}

//向下调整 - 小堆
void AdjustDown(HeapDataType* nums, int size, int sub)
{
	assert(nums);
	int parents = sub;
	int minchild = parents * 2 + 1;//先默认最小孩子为左孩子
	//当子结点超过堆的范围就结束
	while (minchild < size)
	{
        //选出真正的较小子节点
		if (minchild + 1 < size && nums[minchild + 1] < nums[minchild])
		{
			minchild++;
		}
		//如果父节点大于最小的子结点,就交换双方位置
		if (nums[parents] > nums[minchild])
		{
			Swap(nums + parents, nums + minchild);
			//迭代
			parents = minchild;
			minchild = parents * 2 + 1;
		}
		//否则不用向下调整,直接跳出循环
		else
		{
			break;
		}
	}
}

int* Topk(int* nums, int numsSize, int k)
{
	int* ans = (int*)malloc(sizeof(int) * k);
	if (ans == NULL)
	{
		perror("Topk");
		exit(-1);
	}
	for (int i = 0; i < k; i++)
	{
		ans[i] = nums[i];
	}
    //向下建堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(ans, k, i);
	}
	for (int i = k; i < numsSize; i++)
	{
		if (nums[i] > ans[0]) //如果数组中其他元素大于堆顶元素
		{
			ans[0] = nums[i]; //替换堆顶元素
			AdjustDown(ans, k, 0); //向下调整保持堆的结构
		}
	}
	return ans;
}

int main()
{
	srand((unsigned int)time(NULL)); //用时间戳产生随机数种子
	int* nums = (int*)malloc(sizeof(int) * 10000);
	if (nums == NULL)
	{
		perror("TestTopk");
		exit(-1);
	}
    //生成10000个小于10000的整数
	for (int i = 0; i < 10000; i++)
	{
		nums[i] = rand() % 10000;
	}
    //生成10个大于10000的整数
	for (int i = 0; i < 10; i++)
	{
		nums[rand() % 10000] = 10000 + i;
	}
	int* ans = Topk(nums, 10000, 10);
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", ans[i]);
	}
	printf("\n");
	free(ans);
	ans = NULL;
	free(nums);
	nums = NULL;
    return 0;
}

在这里插入图片描述

四、完整代码

Heap.h

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>

typedef int HeapDataType;

typedef struct heap
{
	HeapDataType* data;
	int size;
	int capacity;
}heap;

//初始堆
void InitHeap(heap* ph);

//打印堆
void PrintHeap(heap* ph);

//往堆里插入数据
void HeapPush(heap* ph, HeapDataType x);

//删除堆的首元素
void HeapPop(heap* ph);

//获取堆的首元素
HeapDataType HeapTop(heap* ph);

//判断堆是否为空
bool IsHeapEmpty(heap* ph);

//求堆的大小
int HeapSize(heap* ph);

//销毁堆
void DestroyHeap(heap* ph);

//堆排序
void HeapSort(int* nums, int numsSize);

//Topk
int* Topk(int* nums, int numsSize, int k);

Heap.c

#include"heap.h"

void InitHeap(heap* ph)
{
	assert(ph);
	ph->data = NULL;
	ph->capacity = 0;
	ph->size = 0;
}

void PrintHeap(heap* ph)
{
	assert(ph);
	for (int i = 0; i < ph->size; i++)
	{
		printf("%d ", ph->data[i]);
	}
	printf("\n");
}

void Swap(HeapDataType* a, HeapDataType* b)
{
	assert(a && b);
	HeapDataType temp = *a;
	*a = *b;
	*b = temp;
}

//向上调整 - 小堆
void AdjustUp(HeapDataType* nums, int child)
{
	assert(nums);
	while (child > 0)
	{
		int parents = (child - 1) / 2;
		if (nums[child] < nums[parents])
		{
			Swap(&nums[child], &nums[parents]);
			child = parents;
		}
		else
		{
			break;
		}
	}
}

//向下调整 - 小堆
void AdjustDown(HeapDataType* nums, int size, int sub)
{
	assert(nums);
	int parents = sub;
	int minchild = parents * 2 + 1;//先默认最小孩子为左孩子
	//当子结点超过堆的范围就结束
	while (minchild < size)
	{
		if (minchild + 1 < size && nums[minchild + 1] < nums[minchild])
		{
			minchild++;
		}
		//如果父节点大于最小的子结点,就交换双方位置
		if (nums[parents] > nums[minchild])
		{
			Swap(nums + parents, nums + minchild);
			//迭代
			parents = minchild;
			minchild = parents * 2 + 1;
		}
		//否则不用向下调整,直接跳出循环
		else
		{
			break;
		}
	}
}

//小根堆
void HeapPush(heap* ph, HeapDataType x)
{
	assert(ph);
	if (ph->capacity == ph->size) //扩容
	{
		int newcapacity = ph->data == NULL ? 4 : ph->capacity * 2;
		HeapDataType* temp = (HeapDataType*)realloc(ph->data, sizeof(HeapDataType) * newcapacity);
		if (temp == NULL)
		{
			perror("realloc");
			exit(-1);
		}
		ph->data = temp;
		ph->capacity = newcapacity;
	}
	ph->data[ph->size] = x;
	AdjustUp(ph->data, ph->size);
	ph->size++;
}

//小根堆
void HeapPop(heap* ph)
{
	assert(ph && ph->size > 0);
	Swap(ph->data, ph->data + ph->size - 1);
	ph->size--;
	AdjustDown(ph->data, ph->size, 0);
}

HeapDataType HeapTop(heap* ph)
{
	assert(ph && ph->size > 0);
	return ph->data[0];
}

bool IsHeapEmpty(heap* ph)
{
	assert(ph);
	return ph->size == 0;
}

int HeapSize(heap* ph)
{
	assert(ph);
	return ph->size;
}

void DestroyHeap(heap* ph)
{
	assert(ph);
	free(ph->data);
	ph->capacity = 0;
	ph->size = 0;
}


//升序 -> 大根堆
//降序 -> 小根堆
void HeapSort(int* nums, int numsSize)
{
	//建堆method1:插入法 向上调整 O(N * logN)
	/*for (int i = 1; i < numsSize; i++)
	{
		AdjustUp(nums, i);
	}*/
	
	//建堆method2:向下调整 O(N)
	for (int i = (numsSize - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(nums, numsSize, i);
	}
	//降序 - 小根堆 - 向下调整 O(N * logN) 
	while (numsSize > 1)
	{
		Swap(nums, nums + numsSize - 1);
		AdjustDown(nums, --numsSize, 0);
	}
}

int* Topk(int* nums, int numsSize, int k)
{
	int* ans = (int*)malloc(sizeof(int) * k);
	if (ans == NULL)
	{
		perror("Topk");
		exit(-1);
	}
	for (int i = 0; i < k; i++)
	{
		ans[i] = nums[i];
	}
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(ans, k, i);
	}
	for (int i = k; i < numsSize; i++)
	{
		if (nums[i] > ans[0])
		{
			ans[0] = nums[i];
			AdjustDown(ans, k, 0);
		}
	}
	return ans;
}

test.c

#include"heap.h"
#include<time.h>

void TestHeap()
{
	heap H;//创建堆变量
	InitHeap(&H);
	int nums[] = { 10, 26, 85, 4, 23, 14, 12, 36, 16 };
	for (int i = 0; i < sizeof(nums) / sizeof(int); i++)
	{
		HeapPush(&H, nums[i]);
	}
	PrintHeap(&H);
	int n = sizeof(nums) / sizeof(int);
	while (n--)
	{
		printf("%d ", HeapTop(&H));
		HeapPop(&H);
	}
}

void TestHeapSort()
{
	int nums[] = { 10, 26, 85, 4, 23, 14, 12, 36, 16 };
	HeapSort(nums, sizeof(nums) / sizeof(int));
	for (int i = 0; i < sizeof(nums) / sizeof(int); i++)
	{
		printf("%d ", nums[i]);
	}
	printf("\n");
}

void TestTopk()
{
	srand((unsigned int)time(NULL));
	int* nums = (int*)malloc(sizeof(int) * 10000);
	if (nums == NULL)
	{
		perror("TestTopk");
		exit(-1);
	}
	for (int i = 0; i < 10000; i++)
	{
		nums[i] = rand() % 10000;
	}
	for (int i = 0; i < 10; i++)
	{
		nums[rand() % 10000] = 10000 + i;
	}
	int* ans = Topk(nums, 10000, 10);
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", ans[i]);
	}
	printf("\n");
	free(ans);
	ans = NULL;
	free(nums);
	nums = NULL;
}

int main()
{
	//TestHeap();
	//TestHeapSort();
	TestTopk();
	return 0;
}
}

void TestHeapSort()
{
	int nums[] = { 10, 26, 85, 4, 23, 14, 12, 36, 16 };
	HeapSort(nums, sizeof(nums) / sizeof(int));
	for (int i = 0; i < sizeof(nums) / sizeof(int); i++)
	{
		printf("%d ", nums[i]);
	}
	printf("\n");
}

void TestTopk()
{
	srand((unsigned int)time(NULL));
	int* nums = (int*)malloc(sizeof(int) * 10000);
	if (nums == NULL)
	{
		perror("TestTopk");
		exit(-1);
	}
	for (int i = 0; i < 10000; i++)
	{
		nums[i] = rand() % 10000;
	}
	for (int i = 0; i < 10; i++)
	{
		nums[rand() % 10000] = 10000 + i;
	}
	int* ans = Topk(nums, 10000, 10);
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", ans[i]);
	}
	printf("\n");
	free(ans);
	ans = NULL;
	free(nums);
	nums = NULL;
}

int main()
{
	//TestHeap();
	//TestHeapSort();
	TestTopk();
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值