堆的理论基础

堆分为大根堆和小根堆。堆的结构是一颗完全二叉树。

大根堆是父结点大于孩子结点。小根堆是父结点小于孩子结点。

这就是一个小根堆 

这是一个大根堆

堆的存储结构 

堆可以使用一个数组存储,因为它的底层是一颗完全二叉树,并不会造成浪费空间。而孩子结点的下标和父结点下标,存在公式关联。

假如父结点下标为 i : 左孩子结点下标:i * 2 + 1, 右孩子结点下标: i * 2 + 2

假如孩子结点下标为i: 父节点下标:(i - 1) / 2

堆的存储结点结构

typedef struct Heap 
{
	int* _a;
	int _size;
	int _capacity;
}HP;

小根堆的实现

初始化

void HeapInit(HP* php) 
{
	assert(php);
	php->_a = NULL;
	php->_size = php->_capacity = 0;
}

向下调整

对小根堆向下调整,若想调整完之后是一个小根堆,前提是该树的子树已经是一个小根堆。这里的parent可以接受任意数组下标的任意位置。

假如需要调整的结点到最后一层的高度为n,那么只需要向下移动n - 1步,时间复杂度O(n - 1)

void AdjustDown(int* a, int n, int parent) 
{
	int child = parent * 2 + 1; // 先把左孩子结点看成最小的孩子结点
	while (child < n) {
		if (child + 1 < n && a[child] > a[child + 1]) {
			child++;
		}
		if (a[child] < a[parent]) {
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else {
			break;
		}
	}
}

向上调整

对数组任意一个位置向上调整,接受的位置就是child。

假如要向上调整的结点处于第n层,那么需要向上走n - 1

void AdjustUp(int* a, int child)
{
	int 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(HP* php, int x)
{
	assert(php);
    // 扩容操作
	if (php->_size == php->_capacity)
	{
		int new_capacity = php->_capacity == 0 ? 4 : php->_capacity * 2;
		int* temp = (int*)realloc(php->_a, new_capacity * sizeof(int));
		if (temp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		php->_a = temp;
		php->_capacity = new_capacity;
	}

	php->_a[php->_size++] = x;
	AdjustUp(php->_a, php->_size - 1);
}	  

堆删除

 堆删除堆顶的元素,先将末尾的元素和堆顶元素交换 ,然后删除末尾元素, 最后向下调整

void HeapPop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));
	Swap(&php->_a[0], &php->_a[php->_size - 1]);
	php->_size--;
	AdjustDown(php->_a, php->_size, 0);
}

向上调整和向上调整

向上调整和向下调整,都可以用来创建堆,但是两者时间复杂度却不一样,向下调整更优

在使用向上调整和向下调整建堆,是基于一个数组来创建。

下面则是使用向下调整建堆。

void AdjustDown(int* arr, int n, int parent)//向下调整
{
	int child = parent * 2 + 1;//左孩子节点下标

	while (child < n) //向下调整,child只会越来越大,当超出数组范围时,退出循环
	{
		if (child + 1 < n && arr[child] < arr[child + 1])//判断是否存在右孩子节点,存在左右孩子结点谁大,要是建小堆,取小的那个元素
			child++;
		if (arr[parent] > arr[child])//判断父亲结点和孩子结点那个大,若孩子结点大,则进行交换,并继续向下调整
		{
			swap(&arr[parent], &arr[child]);//两节点进行交换
			parent = child;//孩子结点的下标赋给父亲结点
			child = parent * 2 + 1;//孩子节点获得新的左孩子下标
		}
		else
			break;
	}
}

int CreatHeap(int* arr, int n)//arr为要建堆的数组
{
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)//循环的初始值为倒数第一个非叶子节点的下标
	{
		AdjustDown(arr, n, i);//向下调整
	}
}

下面使用向上调整建堆

void AdjustUp(int* arr, int child)
{
	int parent = (child - 1) / 2;//获得父亲节点的下标

	while (child)
	{
		if (arr[parent] < arr[child])//孩子结点数据小于父亲结点数据,两数交换
		{
			swap(&arr[parent], &arr[child]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
			break;
	}
}
void CreatHeap(int* a, int n)
{
	for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}
}

时间复杂度分析

设高度从1开始,n个结点。

使用向下调整建堆: 

使用向上调整建堆:

根据向下调整可得 O(n * logn)

Top K 问题

例如在100 个数中求前k个最小的数。

求最小就建大堆,求最大就建小堆。

 

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值