数据结构与算法11 - 堆

 堆

1. 概念

堆的性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树;

这里结点之间:父亲等于孩子 或小于孩子 或大于孩子;

若节点相等,则小根堆和大根堆都满足;

2. 分析堆进行插入数据的情况

如果该数组已满,此时再插入数据需要扩容,并且判断插入的数据是否满足堆的性质;

如果插入后不满足堆的性质,那我们需要向上调整;

如图所示,若孩子比双亲结点大,那么我们需要交换位置,直到满足堆的性质;

此时我们最多要交换的次数为堆的高度:log2(N+1) ~ log2(N) + 1

3. 堆的删除

堆的删除实际上是删除根节点,直接删除尾的结点很简单,但是删除后的结构还是一个堆,没有意义,因此堆的pop函数应该是删除根节点的元素,剩下的元素重新排列满足堆的结构;

堆删除的实际意义是:取出当前的最大值 / 最小值(无论是最大值还是最小值肯定是位于堆顶元素的位置上);

问题:怎么进行堆顶结点的删除?

分析:如果我们采用覆盖的方式进行删除,此时后面的元素全部往前移动,这时候的效率太低了,时间复杂度太高;除此之外,结点之间的关系也发生错乱了,父子兄弟之间的关系全部错乱了;

解决:将堆顶元素和最后一个元素的位置进行调换,然后再将最后一个元素进行删除(size--即可),最后需要进行向下调整,此时可以满足:根节点下的左子树和右子树之间的关系是满足堆的性质的(如下图所示);

4. 堆的排序

问题:实际应用中,给定我们的通常是一个数组,而不是直接点堆,那么我们怎么进行堆排序呢?

分析:直接将数组进行push组建成一个堆也可以,但是这样子做的时间复杂度太大了!

解决:

例如,我们由上面的数组,当我们插入第一个数据的时候,我们可以将后面的结点想象为孩子结点并进行向上调整,从而模拟建堆的过程(这里使用的是向上调整算法);

数据全部都插入,然后从第二个结点开始向上调整

这种建堆的过程的时候复杂度为O(N * logn);

排升序是建立大堆还是小堆?

结论:

  • 排升序需要建立大堆;
  • 排降序需要建立小堆;

分析:如果我们排升序建立小堆

每次pop之后结点的位置直接就消失,此时左右孩子结点会成为新的父亲节点,导致下面的顺序就乱了!

因此我们每次选出一个最小值,都需要建堆,而建堆的时间复杂度是O(logN * N)!

效率太地下!

相反如果排升序建大堆:

此时根节点是最大值,然后我们将根节点和最后一位的叶子结点进行交换,最大值就位于叶子节点,然后我们进行size--,分析0~size-2位置上的其他节点即可,且中间的堆的结构没有被破坏!

向上调整和向下调整的前提关键:

左右子树必须是大堆或者小堆!

堆排序的代码如下所示(前提是已经有了向下调整的代码):

5. 建堆的过程

堆的创建有两种方法:通过向上调整 / 通过向下调整

向下调整

对于向下调整函数来说,有3个参数:指向的数组,数组的元素个数,父亲结点i;

因此分析上面的图:向下调整函数实际上就是先调整 3 所在的父结点,即 6,然后再调整7、5、1、2,即根据父亲节点来调整

使用向下调整来建堆的过程的时间复杂度为O(N) = N;

向上调整

对于向上调整来说,整体的时间复杂度是O(N) = log(N) * N;

因此向下调整的效率比向上调整的效率高很多!

其实可以看出:

对于向下调整来说:调整的是第一层到倒数第二层!(将不满足的数据移动到下面,最后一层不需要调整);

对于向上调整来说:调整的是第二层到最后一层!(将不满足的数据移动到上面,第一层不需要调整);

但是此时有一个致命的因素:最后一层的数据最多的情况下是上面所有层数的数据之和!因此向下调整需要调节的数据更少,效率更高!

根据堆进行排序

        当我们有了堆的结构之后,此时根节点的值就是我们需要的当前的最大值,那么排序的思路就是:将最大值换到最后一个节点的位置上,然后再将除了最后一个节点的剩下部分进行向下调整,使其结构再次成为一个堆,再重复上面的操作!

这里的n指的是数组元素的个数,end指的是最后一个元素的下标;

这里排序的时间复杂度是O(N) = N * logN;

堆的应用

堆的应用主要包括:排序和topK问题;

关于堆排序的实际应用我们后面讲排序的时候再讲解。

堆的模拟实现代码

heap.h

#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
#include<time.h>

typedef int HPDateType;
typedef struct Heap
{
	HPDateType* a;
	int size;
	int capacity;
}HP;

void HeapInit(HP* php);
void HeapPush(HP* php ,HPDateType x);
// 向上调整  child是需要调整的坐标
void AdjustUp(HPDateType* a, int child);
void Swap(HPDateType* x, HPDateType* y);

void HeapPop (HP* php);
// 向下调整
// n为数据的个数
void AdjustDown(HPDateType* a, int n, int parent);
size_t HeapSize(HP* php);
bool HeapEmpty(HP* php);
HPDateType HeapTop(HP* php);

void HeapDestory(HP* php);

heap.c

#define  _CRT_SECURE_NO_WARNINGS 1
#include"heap.h"

void HeapInit(HP* php)
{
	assert(php);
	// 默认开辟四个空间
	php->a = (HPDateType*)malloc(sizeof(HPDateType) * 4);
	if (php->a == NULL)
	{
		perror("malloc fail...");
		return;
	}
	php->size = 0;
	php->capacity = 4;
}

void Swap(HPDateType* x, HPDateType* y)
{
	HPDateType tmp = *x;
	*x = *y;
	*y = tmp;
}


// 向上调整
void AdjustUp(HPDateType* 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, HPDateType x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		// 扩容
		HPDateType* tmp = (HPDateType*)realloc(php->a, sizeof(HPDateType) * php->capacity * 2);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		php->a = tmp;
		php->capacity *= 2;
	}
	// 尾巴处插入数据
	php->a[php->size] = x;
	php->size++;
	// 向上调整
	AdjustUp(php->a, php->size -1);

}
// 向下调整
void  AdjustDown(HPDateType* a, int sz, int parent)
{
	// 假设当前左孩子为孩子(下标)
	int child = 2 * parent + 1;
	while (child < sz)
	{
		if (child + 1 < sz && a[child] < a[child + 1])
		{
			// 右孩子为当前的孩子
			child++;
		}
		if (a[parent] < a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent + 1;
		}
		else
		{
			break;
		}
	}

}
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);
}

HPDateType HeapTop(HP* hp)
{
	return hp->a[0];
}

size_t HeapSize(HP* hp)
{
	assert(hp);
	return hp->size;
}

bool HeapEmpty(HP* hp)
{
	assert(hp);
	return hp->size == 0;
}

void HeapDestory(HP* hp)
{
	assert(hp);
	free(hp->a);
	hp->a = NULL;
	hp->size = 0;
	hp->capacity = 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一道秘制的小菜

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

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

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

打赏作者

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

抵扣说明:

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

余额充值