堆、堆排序、TOP-K问题

目录

什么是堆

堆的实现

堆排序

TOP-K问题


今天整点堆玩玩

什么是堆

如果有一个关键码的集合K,把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足所有的父节点大于等于它的子节点的叫大堆,所有的父节点小于等于它的子节点的叫小堆。
性质:1.堆中某个节点的值总是不大于或不小于其父节点的值;
           2.堆总是一棵完全二叉树;


堆的实现

头文件:

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	size_t size;
	size_t capacity;
}Heap;
void Swap(HPDataType* pa, HPDataType* pb);
void HeapInit(Heap* php);
void HeapDestroy(Heap* php);
void HeapPrint(Heap* php);

// 插入x以后,保持他依旧是(小/大)堆
void HeapPush(Heap* php, HPDataType x);

// 删除堆顶的数据。(最小/最大)
void HeapPop(Heap* php);
bool HeapEmpty(Heap* php);
size_t HeapSize(Heap* php);
HPDataType HeapTop(Heap* php);

函数实现:

#include "heap.h"
void HeapInit(Heap* php)
{
	assert(php);
	php->a = NULL;
	php->size = php->capacity = 0;
}

void HeapDestroy(Heap* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}
void Swap(HPDataType* pa, HPDataType* pb)
{
	HPDataType tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}
void HeapPrint(Heap* php)
{
	assert(php);
	for (size_t i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}
void AdjustUp(HPDataType* a, size_t child)
{
	size_t parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
void HeapPush(Heap* php, HPDataType x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = realloc(php->a, newCapacity * sizeof(HPDataType));
		if (tmp == NULL)
			exit(-1);
		else
		{
			php->a = tmp;
			php->capacity = newCapacity;
		}
	}
	php->a[php->size++] = x;
	AdjustUp(php->a, php->size - 1);
}
void AdjustDown(HPDataType* a, size_t size, size_t root)
{
	size_t parent = root;
	size_t child = parent * 2 + 1;
	while (child < size)
	{
		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;
		}
	}
}
void HeapPop(Heap* php)
{
	assert(php);
	assert(php->size > 0);

	Swap(&php->a[0], &php->a[php->size - 1]);
	--php->size;

	AdjustDown(php->a, php->size, 0);
}
bool HeapEmpty(Heap* php)
{
	assert(php);

	return php->size == 0;
}
size_t HeapSize(Heap* php)
{
	assert(php);
	return php->size;
}
HPDataType HeapTop(Heap* php)
{
	assert(php);
	assert(php->size > 0);
	return php->a[0];
}

这里实现的是小堆,如果是大堆只需要把向上调整和向下调整部分判断条件换一下即可。

向上调整算法:在插入堆时在数组尾部插入一个数据,把这个数据当做子节点,不断与父节点比较,如果比父节点小,则交换

向下调整算法:在删除数据的时候先把第一个数据与最后一个数据交换位置,然后让数组大小-1,就实现删除了堆的根节点,删除后从根开始向下找左右子节点中较小的那个节点,如果较小的节点比父节点小,则交换


堆排序

先用一种比较麻烦但逻辑比较清楚的方法(要先实现一个堆),这并不是传统的堆排序

void HeapSort(int* a, size_t size)
{
	Heap hp;
	HeapInit(&hp);
	for (size_t i = 0; i < size; i++)
	{
		HeapPush(&hp, a[i]);
	}
	size_t j = 0;
	while (!HeapEmpty(&hp))
	{
		a[j++] = HeapTop(&hp);
		HeapPop(&hp);
	}
	HeapDestroy(&hp);
}
int main()
{
	int a[] = { 4, 2, 7, 8, 5, 1, 0, 6 };
	HeapSort(a, sizeof(a) / sizeof(int));
	for (int i = 0; i < sizeof(a) / sizeof(int); ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
	return 0;
}

这里还是以小堆为例,排序的时候把数组中的元素放入一个堆中,再不断拿出根节点(最小的元素)即可

下面来看传统的堆排序,要运用堆排序,首先要对给定的顺序表来建堆,建堆这里我们采用向下调整算法(小堆调整)

void Adjustdown(HPDataType* a, size_t size, size_t root)
{
	size_t parent = root;
	size_t child = parent * 2 + 1;
	while (child < size)
	{
		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;
		}
	}
}

但是向下调整调整的是堆啊,必须对堆进行调整,那既然不是堆,我们就把他变成堆,这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。也就是从最后一个节点的父节点开始,这里的图以建大堆为例

size-1是最后一个元素的下标,再-1然后除2得到它的父节点下标

for (int i = (size - 1 - 1) / 2; i >= 0; i--)//a是传进来的顺序表,size是顺序表的大小
	{
		Adjustdown(a, size, i);
	}

学会了如何建堆,来看一下建堆的时间复杂度

我们考虑最坏的情况,看一下建堆向下调整的次数,假设每一次都要交换,那么见下图 

最后第h层不需要再向下调整了

那么一共需要移动因此利用向下调整算法建堆的时间复杂度是O(N)  

建完小堆了,我们想一下我们此时这个堆是排升序好还是排降序好?很多人肯定会说,小堆的根节点是最小的元素,肯定是排升序啊,每次把根节点拿出来,不就排好了吗。假设我们排升序,那第一次拿出最小的元素后,我们想拿次小的元素,但是此时剩下的元素还能构成一个堆吗?我们已经把堆的结构给破坏了,因此如果要找出次小元素,就需要重新建堆,因此要不断新建不断新建,时间复杂度是O(N^2)。

因此如果我们要排升序,应该建大堆,排降序,应该建小堆,以排降序为例,我们应该把小堆的首元素和最后一个元素交换,把最小的元素放到末尾,这样堆的结构也没有被破坏,只需要像删除那样向下调整一下,但是找次小元素的时候要注意此时堆的大小变小了1,因为我们要忽略存储着最小元素的顺序表最后一个位置。整理一下,写出完整的堆排序:

void HeapSort(int* a, int size)//降序建小堆
{
	for (int i = (size - 1 - 1) / 2; i >= 0; i--)
	{
		Adjustdown(a, size, i);
	}
	size_t end = size - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		Adjustdown(a, end, 0);
		--end;
	}
}

以一个起初为升序的顺序表为例,我们排一下降序

int main()
{
	int a[10] = {0,1,2,3,4,5,6,7,8,9};
	HeapSort(a, sizeof(a) / sizeof(int));
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", a[i]);
	}
	return 0;
}

bingo


 

TOP-K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

比如找前k个最大的,建小堆后用n-k个元素里的替换的时候被替换进来的元素只会在堆的底部,而不会在顶部(小堆的特性)
因为我们刚刚写的向下调整是建小堆的,因此我们来找一下100个数里面前5个最大的数

int main()
{
	int n = 100;
	int* a = (int*)malloc(sizeof(int) * 100);
	srand(time(0));
	for (int i = 0; i < n; i++)
	{
		a[i] = rand() % 10000;//产生小于10000的数
	}
	a[4] = 10000 + 8;
	a[9] = 10000 + 6;
	a[16] = 10000 + 50;
	a[56] = 10000 + 200;
	a[99] = 10000 + 56;
	HPDataType* top=TopK(a, n, 5);//找前5个最大的
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", top[i]);
	}
	free(top);
	return 0;
}
int* TopK(int* a, size_t n,size_t k)
{
	HPDataType* minheap= (int*)malloc(sizeof(int) * k);
	for (int i = 0; i < k; i++)
	{
		minheap[i] = a[i];
	}
	//建小堆
	for (int i = (k - 2) / 2; i >= 0; i--)
	{
		AdjustDown(minheap, k, i);
	}
	//剩余n-k个元素来与堆元素比较
	for (int i = k; i < n; i++)
	{
		if (a[i] > minheap[0])
		{
			Swap(&a[i], &minheap[0]);
			AdjustDown(minheap, k, 0);
		}
	}
	return minheap;
}

先用随机数填充顺序表,然后把其中5个位置设置成比10000大的,我们要找出的就是这5个数,来看一下结果

顺利找出,这里并不是按照大小顺序排列的,返回的时候已经是一个堆了,因此如果想排序可以免去排序第一步建堆的过程。


 写完了写完了,二叉树和堆什么的快

  • 11
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

printf("雷猴");

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值