堆的实现(以小堆为例)

文章介绍了堆的概念,特别是小根堆的实现,包括初始化、交换、向上排序和向下排序算法。此外,讨论了建堆的时间复杂度,并提出了用堆解决Topk问题的三种方法,强调了解法2和解法3在效率和空间复杂度上的权衡。
摘要由CSDN通过智能技术生成

目录

一、堆的概念和结构

二、小根堆的实现

三、建堆的时间复杂度

四、用堆解决经典Topk问题

代码实现


一、堆的概念和结构

堆的概念:堆是一种特殊的数据结构,是一种完全二叉树。满足一下性质:

1. 父节点的值总是大于等于(或小于等于)它的子节点的值,称为大根堆小根堆
2. 堆中的最大元素(或最小元素)总是在根节点上(根据堆的种类不同)。

堆的结构图:

二、小根堆的实现

创建一个顺序表用来存储堆。由于堆是完全二叉树,用数组可以依次将堆上的所有数据进行保存,并不会出现错位指向的情况。二叉树在逻辑上是一种链式结构,虽然数组在物理上是线性结构,但存储的数据可以通过下标对堆的父子结点进行访问。

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int capacity;
	int size;
}HP;
  • 初始化

void HeapInit(HP* php)
{
	assert(php);

	php->a = NULL;
	php->capacity = php->size = 0;
}
  • 交换两个数据

void Swap(HPDataType* pchild, HPDataType* pparent)
{
	assert(pchild && pparent);
	HPDataType tmp = *pchild;
	*pchild = *pparent;
	*pparent = tmp;
}
  • 向上排序算法

根据堆的性质,新插入的数据作为叶子结点,如果小于它的父亲结点,将二者进行交换。依次向上遍历,直到根节点。

void AdjustUp(HPDataType* a, int child)
{
	assert(a);
	int parent = (child - 1) / 2;

	while (child)
	{
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
  • 向下排序算法

将根节点和数组最后一个元素交换位置,也就是将堆从根节点处断开,根结点的两端依旧是两个小根堆。数组的最后一个元素将变成原来的根节点的数据,数组的元素个数减1,也就实现了根结点的删除。但新的数组并不满足堆的性质,为了保持堆的性质,找到根结点,将根节点和它的两个左右子节点进行比较,和最小的子节点进行交换。不断重复,直到叶子结点或父节点都小于其子节点

void AdjustDwon(HPDataType* a , int size, int parent)
{
	int 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 HeapPush(HP* php, HPDataType x)
{
	assert(php);
	if (php->capacity == php->size)
	{
		int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}
	
	php->a[php->size] = x;
	AdjustUp(php->a, php->size);
	php->size++;
}
  • 删除

删除堆上数据时,如果直接将根节点(数组首元素)删除,在重新创建堆,效率会很低,为了保证更高效,调用向下排序算法。

void HeapPop(HP* php)
{
	assert(php && php->a);
	assert(!HeapEmity(php));

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

	AdjustDwon(php->a, php->size, 0);

}
  • 判断堆是否为空

bool HeapEmity(HP* php)
{
	assert(php);
	return php->size == 0;
}
  • 返回根节点的数据

HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(!HeapEmity(php));

	return php->a[0];
}
  • 打印

void HeapPrint(HP* php)
{
	assert(php && php->a);
	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}
  • 销毁

void HeapDestory(HP* php)
{
	assert(php);

	free(php->a);
	php->capacity = php->size = 0;

}

三、建堆的时间复杂度

将一个无序数组变为堆的形式的时候,我们既可以使用向上排序算法也可以使用向下排序算法来实现。使用向上排序算法的时候,最坏的情况下没次进入的数据都需要额外建堆,那么时间复杂度为O(N*logN)。而使用向下排序算法时,由于向下排序算法的特殊性,要保证每次进入数据之后,都可以将原来的堆从根结点处分为两个堆,不用在新建堆。时间复杂度为O(N)

四、用堆解决经典Topk问题

Topk问题:从给定的N个数据中找到前K个数。

解法1:排序。以笔者目前掌握的排序方法,最快的排序方法为堆排序。时间复杂度为O(N*logN)

解法2:建堆,在弹出前K个根节点。时间复杂度为O(N + K * logN)

解法3:建数量为k的小大堆。以求前k最大值为例:建小堆,根为堆中最小的数,将N-K依次和根比较,大于替换重建堆,能保证堆中的数据永远是前k。时间复杂度为O(K + (N - K) * logK)

解法2在数据量小的时候时间复杂度时优于解法3的,但可能存在数据量过大,内存没有那么大的空间。解法3在保证效率的同时,兼顾了最小的空间复杂度。

代码实现

void PrintTopK(int* a, int n, int k)
{
	int* KMinHeap = (int*)malloc(sizeof(int) * k);
	assert(KMinHeap);
	//建堆
	for (int i = 0; i < k; i++)
	{
		KMinHeap[i] = a[i];
	}
	for (int i = (k - 2) / 2; i >= 0; i--)
	{
		AdjustDwon(KMinHeap, k, i);
	}
	//将N-K个数和堆根比较
	for (int i = k; i < n; i++)
	{
		if (KMinHeap[0] < a[i])
		{
			KMinHeap[0] = a[i];
			AdjustDwon(KMinHeap, k, 0);
		}
	}

	//弹出
	for (int i = k - 1; i >= 0; i--)
	{
		Swap(&KMinHeap[0], &KMinHeap[i]);
		AdjustDwon(KMinHeap, --k, 0);
		printf("%d ", KMinHeap[k]);
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Ljiyu0506

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

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

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

打赏作者

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

抵扣说明:

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

余额充值