【数据结构】堆的全解析

大家好,我是白晨,一个不是很能熬夜,但是也想日更的人✈。如果喜欢这篇文章,点个赞关注一下白晨吧!你的支持就是我最大的动力!

【数据结构】堆的全解析_第1张图片

文章目录

  • 前言
    • 堆的定义及结构
    • 堆结构以及简单接口函数的代码实现
    • 堆的创建
      • 向下调整算法
      • 向上调整算法
      • 堆的插入
      • 堆的删除
    • 堆的应用
      • Topk问题
      • 堆排序
    • 堆的全局代码
  • 后记


前言


上一篇文章,我们详细介绍了二叉树的入门知识(如果没有二叉树基础的同学建议先看一下二叉树入门,我在本篇文章也会对其中二叉树中比较重要的点再次提及),在有了二叉树的相关知识后,我们就可以着手实现一些有着更多特性的数据结构了。今天,这篇文章我们要介绍到的数据结构是一种用来排序或者找寻数据中最大、最小的若干数据的树——堆。



上一篇文章我们讲到,二叉树的顺序存储结构一般只适用于完全二叉树,并且顺序存储结构的父子结点之间还有一些数学联系,现在我们来回顾一下。

一棵二叉树中最后一层结点以上结点构成满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树

【数据结构】堆的全解析_第2张图片

上图中,第四层以上的前三层构成满二叉树结构,最后一层从左到右结点连续,所以上树为一棵完全二叉树。

完全二叉树有如下特性:

【数据结构】堆的全解析_第3张图片

对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0开始编号,则对于序号为i的结点有:

  1. i>0,i位置结点的双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点
  2. i位置结点的孩子序号:
    • 左孩子:2*i+1;
    • 右孩子:2*i+2;
  3. 若2i+1=n,则无左孩子
  4. 若2i+2=n,则无右孩子

堆的定义及结构


如果有一个关键码的集合K = { k 0 k_0 k0 k 1 k_1 k1 k 2 k_2 k2 ,…, k n − 1 k_{n-1} kn1 },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: k i k_{i} ki <= k 2 i + 1 k_{2i+1} k2i+1 k i k_i ki<= k 2 i + 2 k_{2i+2} k2i+2 ( k i k_i ki >= k 2 i + 1 k_{2i+1} k2i+1 k i k_i ki >= k 2 i + 2 k_{2i+2} k2i+2 ),i= 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

翻译一下就是:

  1. 如果一棵完全二叉树树上父节点都小于等于子节点,那么这棵完全二叉树就被称为小堆

下图为逻辑结构:

【数据结构】堆的全解析_第4张图片

下表为存储结构:

下标 0 1 2 3 4 5
数据 1 2 3 6 5 4
  1. 如果一棵完全二叉树树上父节点都大于等于子节点,那么这棵完全二叉树就被称为大堆

    【数据结构】堆的全解析_第5张图片

下标 0 1 2 3 4 5
数据 6 4 5 1 2 3

从以上定义中我们可以得到堆的两个性质:

  1. 一定是完全二叉树
  2. 堆中某个节点的值总是不大于或不小于其父节点的值

特别注意:堆只要求父节点与子节点的关系,并没有要求前一层与后一层的关系,更没有要求存储顺序必须是有序的.


堆结构以及简单接口函数的代码实现


前面已经提过,堆的存储结构是顺序存储,所以需要用到顺序表(如果没有顺序表基础的同学建议先去了解一下顺序表<-点击跳转),以下操作均会涉及到顺序表的相关操作。

堆的代码实现与顺序表没有任何区别,所以这里我们就不再赘述:

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}Heap;

这样一个堆的存储结构就搭建完毕了,现在我们来实现一下几个简单的接口函数。

// 堆的初始化
void HeapInit(Heap* hp);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的打印
void HeapPrint(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
bool HeapEmpty(Heap* hp);
  1. 堆的初始化与销毁

由于堆的结构也是顺序表,所以初始化与销毁与销毁与顺序表没有任何区别,我们可以直接实现。

void HeapInit(Heap* hp)
{
	assert(hp);

	hp->a = NULL;
	hp->size = 0;
	hp->capacity = 0;
}

void HeapDestory(Heap* hp)
{
	assert(hp);

	free(hp->a);
	hp->capacity = 0;
	hp->size = 0;
}
  1. 堆的打印

这个操作只需要遍历一遍堆中元素就可以。

void HeapPrint(Heap* hp)
{
	assert(hp);

	int i = 0;

	for (i = 0; i < HeapSize(hp); i++)
	{
		printf("%d ", hp->a[i]);
	}

	printf("\n");
}
  1. 取堆顶的数据

堆的堆顶元素在物理上就是顺序表数组下标为0的元素,所以我们只需要保证栈不为空就可以拿到这个数据。

HPDataType HeapTop(Heap* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));

	return hp->a[0];
}
  1. 堆的数据个数及堆的判空
    • 堆的数据个数就是返回堆的size大小
    • 堆的判空就是判断size是否为0,如为0,返回真;如不为0,返回假
int HeapSize(Heap* hp)
{
	assert(hp);

	return hp->size;
}

bool HeapEmpty(Heap* hp)
{
	assert(hp);

	return hp->size == 0;
}
  1. 堆的查找

    • 遍历堆数组,如果找到返回下标,找不到返回-1
    int HeapSearch(Heap* hp, HPDataType x)
    {
    	assert(hp);
    
    	for (int i = 0; i < hp->size; i++)
    	{
    		if (hp->a[i] == x)
    		{
    			return i;
    		}
    	}
    
    	return -1;
    }
    
    

堆的创建


向下调整算法


我们通过上面学习了解到堆对父子结点的大小关系是有一定要求的,如果现在给我们一个完全二叉树,我们该如何将其变为大堆呢?(从这里开始,我们全都使用大堆来举例子,小堆的情况只需要根据大堆的思想类比就可以)

先放置这个问题,我们先来看一种情况:

【数据结构】堆的全解析_第6张图片

我们可以发现这个完全二叉树除了根结点以外,其余的结点都满足大堆的结构,我们如何调整这个完全二叉树可以使其成为一个大堆呢?

  1. 先让根结点与其孩子中比较大的交换

【数据结构】堆的全解析_第7张图片

这时候我们发现,根结点已经是全部结点中最大的了,这就说明根结点已经调整好了,不用再换了。

  1. 我们现在再比较03这两个结点的大小,发现0<3,不满足大堆的要求,所以让03

【数据结构】堆的全解析_第8张图片

调整完毕,我们得到了一个小堆。

通过上面的过程,我们得到了以下经验:

  • 向下调整时,被调整节点的左右子树必须都是大堆(或者小堆,根据所调整的堆而定)。

如果左右子树不是大堆,那会出现什么情况呢?

【数据结构】堆的全解析_第9张图片

这个堆的左右子树就全不为大堆,我们现在来向下调整根结点。

  1. 根结点与较大的孩子3

【数据结构】堆的全解析_第10张图片

  1. 由于0<5,05交换

【数据结构】堆的全解析_第11张图片

我们发现0结点已经调整完毕,但是这个完全二叉树仍然不是大堆。

所以,我们可以得出结论,向下调整必须要求根结点的左右子树为大堆(或者小堆)

  • 向下调整只会对二叉树从根结点到最后调整到的位置产生影响,不会对二叉树的全部结点产生影响

所以,我们可以根据以上过程抽象出一个向下调整的通法(大堆):

  1. 保证要调整结点N的左右子树都是大堆。
  2. 比较N与孩子结点的大小关系。如果N大于等于两个孩子结点,调整结束;不然就让N与较大的孩子交换。
  3. 重复2过程,直到N调整结束或者被调整到叶子结点。

小堆类比大堆思想就可以:

  1. 保证要调整结点N的左右子树都是小堆。
  2. 比较N与孩子结点的大小关系。如果N小于等于两个孩子结点,调整结束;不然就让N与较小的孩子交换。
  3. 重复2过程,直到N调整结束或者被调整到叶子结点。

上述过程N结点始终不变,向下调整的意思就是,向下调整N结点到应在的位置

根据以上思想,我们可以实现向下调整:

// 大堆向下调整
// 向下调整结束条件
// 1. 被调整结点 >= 孩子
// 2. 调整到叶子节点
void AdjustDown_big(HPDataType* a, int n, int parent)
{
	assert(a);

	int child = parent * 2 + 1;
	//孩子必须在堆的范围内
	while (child < n)
	{
		//选出两个孩子中最大的进行交换,并且要保证右孩子存在
		if (a[child + 1] > a[child] && child + 1 < n)
		{
			child++;
		}

		//孩子大于父亲,则交换
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
		}
		else
		{
			break;
		}

		parent = child;
		child = parent * 2 + 1;
	}
}

// 小堆向下调整
// 向下调整结束条件
// 1. 被调整结点 <= 孩子
// 2. 调整到叶子节点
void AdjustDown_little(HPDataType* a, int n, int parent)
{
	assert(a);

	int child = parent * 2 + 1;
	//孩子必须在堆的范围内
	while (child < n)
	{
		//选出两个孩子中最小的进行交换,并且要保证右孩子存在
		if (a[child + 1] < a[child] && child + 1 < n)
		{
			child++;
		}

		//孩子小于父亲,则交换
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
		}
		else
		{
			break;
		}

		parent = child;
		child = parent * 2 + 1;
	}
}

这里我们假设我们堆就是满二叉树,向下调整一次最坏情况的时间复杂度为 O ( l o g 2   n ) O(log_2~n) O(log2 n)

我们现在回到最开始提出的问题,一个完全二叉树怎么才能调整为一个大堆?

因为向下调整要保证左右子树都是大堆,所以我们不可能从根结点开始调整,但是我们可以倒着调整。

【数据结构】堆的全解析_第12张图片

  1. 我们可以从倒数第一个非叶子结点开始向下调整,如上图3结点

【数据结构】堆的全解析_第13张图片

  1. 再向下调整倒数第二个非叶子结点5

【数据结构】堆的全解析_第14张图片

3.这时根结点的左右子树就是大堆了,我们现在来调整它

【数据结构】堆的全解析_第15张图片

调整完毕,这时我们就得到了一个大堆。

具体调整过程如下:

【数据结构】堆的全解析_第16张图片

现在我们就可以抽象出一种向下调整建堆的通法:

  • 从最后一个非叶子结点开始向下调整,一直调整到根结点

最后,再补充一点向下建堆的时间复杂度(了解即可)

【数据结构】堆的全解析_第17张图片


向上调整算法


【数据结构】堆的全解析_第18张图片

来看这样一种情况,现在在一个大堆后面插入一个数9,由于9>6,所以现在构不成大堆了,要怎么调整才能将其再变为大堆呢?

  1. 由于9>6,所以我们交换69结点。

【数据结构】堆的全解析_第19张图片

  1. 比较89,8 < 9,所以根据大堆规则,交换89

【数据结构】堆的全解析_第20张图片

  1. 9已经被调整到根结点,调整结束。

同样我们可以上述过程中得到一些经验:

  • 被调整的结点以前的结点必须构成一个大堆
  • 该调整也只会影响从被调整结点到最后调整到的位置的结点这一条路径,完全二叉树其余结点都不受影响

从以上我们可以抽象出向上调整的过程(大堆):

  1. 保证被调整的结点N以前的结点构成一个大堆。
  2. 被调整的结点N与其父节点比较大小,如果N大于父节点,则交换这两个结点;如果N小于等于父节点,则结束调整。
  3. 重复2过程,直到N被调整调整完毕或者调整到根结点。

小堆的向上调整类比大堆可得:

  1. 保证被调整的结点N以前的结点构成一个小堆。
  2. 被调整的结点N与其父节点比较大小,如果N小于父节点,则交换这两个结点;如果N大于等于父节点,则结束调整。
  3. 重复2过程,直到N被调整调整完毕或者调整到根结点。
// 大堆向上调整
// 调整结束条件:
// 1.父节点 >= 被调整结点
// 2.被调整结点已经调整到根结点
void AdjustUp_big(HPDataType* a, int child)
{
	assert(a);

	while (child > 0)
	{
		int parent = (child - 1) / 2;

		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
		}
		else
		{
			break;
		}
		child = parent;
	}
}

// 小堆向上调整
// 调整结束条件:
// 1.父节点 <= 被调整结点
// 2.被调整结点已经调整到根结点
void AdjustUp_little(HPDataType* a, int child)
{
	assert(a);

	//当child为0时,结束循环
	while (child > 0)
	{
		int parent = (child - 1) / 2;

		//孩子小于父亲时交换,否则退出
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
		}
		else
		{
			break;
		}
		child = parent;
	}
}

那么向上调整可不可以将一个完全二叉树调整成堆呢?

答案是可以的,但是向上调整前提是被调整的结点以前的结点必须构成一个堆,所以必须要从上到下向上调整,具体要从第二个结点开始调整,保证前面的都是大堆,一直要调整到最后一个结点。

所以向上调整建堆所花费的时间要比向下调整建堆花费的时间长,一般调整一棵完全二叉树成堆我们使用向下调整法。

向上调整的真正用处在于在堆后面插入数据,我们就要用向上调整将这个新插入的数据调整到正确的位置上,使这棵完全二叉树再建成堆。这个用处我们后面还会提到。


堆的插入


上文提到了向上调整一般用来插入数据,现在我们就来具体探讨一下堆的插入。

  • 当堆中插入数据时,首先要判断堆中是否已经满了,如果满了的话,就要扩容;
  • 然后将数据插入到堆的最后;
  • 最后将插入的数据向上调整。

具体代码实现:

// 大堆数据的插入
void HeapPush_big(Heap* hp, HPDataType x)
{
	assert(hp);

	if (hp->capacity == hp->size)
	{
		int newCapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(hp->a, newCapacity*sizeof(HPDataType));
		if (tmp == NULL)
		{
			printf("relloc fail");
			exit(-1);
		}
		hp->capacity = newCapacity;
		hp->a = tmp;
	}

	hp->a[hp->size] = x;
	AdjustUp_big(hp->a, hp->size);
	hp->size++;
}

// 小堆数据的插入
void HeapPush_little(Heap* hp, HPDataType x)
{
	assert(hp);

	if (hp->capacity == hp->size)
	{
		int newCapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		HPDataType* tmp = realloc(hp->a, newCapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			printf("relloc fail");
			exit(-1);
		}
		hp->capacity = newCapacity;
		hp->a = tmp;
	}

	hp->a[hp->size] = x;
	AdjustUp_little(hp->a, hp->size);
	hp->size++;
}

代码逻辑运行过程:

【数据结构】堆的全解析_第21张图片


堆的删除


堆的删除是指删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。

堆的删除指的是删除堆顶的元素,也就是说删除顺序表数组中下标为0的元素。

直接删除堆顶元素肯定行不通,因为直接删除这个元素会,将数据整体向前移动一位,这样大概率会破坏堆的结构,所以我们要利用堆的特性删除栈顶元素。

我们以大堆为例,要删除栈顶元素(也就是整个栈中最大的元素)

  • 我们可以将栈顶元素与栈底元素交换,然后删除数组最后一个数据,这时候栈顶元素已经被删除了。
  • 现在根结点左右子树都是大堆,所以我们可以使用向下调整,调整根结点的位置,重新构建成堆。

小堆具体实例如下:

【数据结构】堆的全解析_第22张图片

具体代码实现如下:

// 大堆数据的删除
void HeapPop_big(Heap* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));

	Swap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;
	AdjustDown_big(hp->a, hp->size, 0);
}

// 小堆数据的删除
void HeapPop_little(Heap* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));

	Swap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;
	AdjustDown_little(hp->a, hp->size, 0);
}

代码逻辑运行过程:

【数据结构】堆的全解析_第23张图片

进阶

我们可以思考一个问题,堆如何实现任意删除?

任意删除其实思想仍是上述思想,我们知道移除一个元素会破坏大堆或者小堆属性,所以我们需要将要删除的元素和最后一个元素交换,再向下调整原最后一个元素。

具体思路:

  • 上文已经讲述了堆的查找,所以我们要删除指定的元素的话,先可以使用堆的查找函数把要删除的元素下标找到
  • 然后必须保证下标大于等于0,小于堆的大小
  • 接着交换要删除的元素和最后一个元素,删除最后一个数组数据
  • 向下调整原最后一个元素

具体代码实现如下:

// 大堆任意删除元素
void HeapDelect_big(Heap* hp,int x)
{
	assert(hp);
	assert(!HeapEmpty(hp));
	assert(x >= 0 && x < hp->size);

	Swap(&hp->a[x], &hp->a[hp->size - 1]);
	hp->size--;
	AdjustDown_big(hp->a, hp->size, x);
}

// 小堆任意删除元素
void HeapDelect_little(Heap* hp,int x)
{
	assert(hp);
	assert(!HeapEmpty(hp));
	assert(x >= 0 && x < hp->size);

	Swap(&hp->a[x], &hp->a[hp->size - 1]);
	hp->size--;
	AdjustDown_little(hp->a, hp->size, x);
}

其实上面的任意删除函数仍然有优化的空间,因为下标是随着堆的结构变化不停变动的,所以使用一次删除前就必须查找一次。

但是我们如果可以改进查找函数,使其返回对应位置的指针,在删除时,传递指针,找到此数据进行删除。此功能较好实现,大家可以自己试一试将其实现。

最后,堆的删除大部分都是删除堆顶元素,所以任意删除不是必须要掌握,大家做一了解即可。


堆的应用


我们已经了解了堆的结构、特性和常用的接口函数,现在就可以将其应用到日常的问题中。堆最常用的应用一般有两个——Topk问题和堆排序。

Topk问题


Topk问题指的是选出一株数据中最大或者最小的几个。

如,王者荣耀中英雄的国服最强,考试成绩的全校前十等

如果给我们一组数据,如果要让我们选出最大的10个数,那么我们可能有以下思路:

【数据结构】堆的全解析_第24张图片

方式2到方式1的时间复杂度小了很多,但是这两个方法都有一个致命的缺陷——空间复杂度为 O ( N ) O(N) O(N)

我们来假设一种场景,如果我们有10亿个数据,内存中无法存储这么多数据,那应该怎么办呢?

有机智的小伙伴可能就会说用外部排序不就可以给磁盘中的数据排序了吗?这是一种可行的方法,但是如果为了找10个数据就要排序全部的数据,时间成本和消耗都太大,得不偿失。

所以,现在就是体现我们堆结构优越性的地方了,我们可以得到第三种方式:

【数据结构】堆的全解析_第25张图片

  • 首先解释为什么要建小堆,如果要排最大几个数,一般思路应该是建大堆,但是大堆有个致命的缺点,大堆堆顶元素是最大的元素,如果刚好最大的元素在前k个数中,那么直接就会堵死堆,应该用什么方法判断一个数据是否是最大的几个数(是否能进堆)呢?
  • 当然不能与堆中的全部元素比较,这样时间复杂度太高。所以我们可以先建一个大小为k的小堆,这样堆顶数据就是堆中数据中最小的数据,其余的数据只要大于栈顶的数据就可以进堆,然后再向下调整,使堆中最小的数据再次回到堆顶。
  • 这里我们可以感性认识一下,小堆可以让小的数据占据堆顶,而让大的数据向下滑,所以不会堵死堆。
  • 最后比较完全部的数,堆里的数就是最大的k个数。

选出最小的数也是一个道理:

  1. 用前k个数建一个大堆(最大的数在堆顶)
  2. 用剩下的数与栈顶数据比较,如果小于这个数就替换堆顶的数,再向下调整,使堆中最大的数始终在堆顶
  3. 比较结束后,堆中的k个数就是最小的k个数

特别提醒:这种方法只是选出最大或者最小的前k个数,并不会给这k个数排序。

具体代码实现如下:

// 找出最大的前k个数
void PrintTopK(int* a, int n, int k)
{
	assert(a);
	
	// 建小堆
	Heap hp;
	HeapInit(&hp);

	for (int i = 0; i < k; i++)
	{
		HeapPush_little(&hp, a[i]);
	}
	
	// 堆顶元素与其余元素比较
	for (int i = k; i < n; i++)
	{
		if (a[i] > HeapTop(&hp))
		{
			hp.a[0] = a[i];
			AdjustDown_little(hp.a, hp.size, 0);
		}
	}

	HeapPrint(&hp);

	HeapDestory(&hp);
}

// 找出最小的前k个数
void PrintTopK(int* a, int n, int k)
{
	assert(a);
	
	// 建大堆
	Heap hp;
	HeapInit(&hp);

	for (int i = 0; i < k; i++)
	{
		HeapPush_big(&hp, a[i]);
	}
	
	// 堆顶元素与其余元素比较
	for (int i = k; i < n; i++)
	{
		if (a[i] > HeapTop(&hp))
		{
			hp.a[0] = a[i];
			AdjustDown_big(hp.a, hp.size, 0);
		}
	}

	HeapPrint(&hp);

	HeapDestory(&hp);
}

堆排序


堆排序(Heapsort) 是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

所以说堆排序就是利用堆的性质完成排序的过程,堆排序主要妙就妙在它的思想上,这里我们直接来看堆排序的思想:

总共分为两个步骤:

  1. 建堆

    升序:建大堆
    降序:建小堆

  2. 利用堆删除思想来进行排序

建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

具体解释(升序):

  1. 升序就是从小到大排列,正常思维应该是建小堆,可是建小堆无法保证除了堆顶元素其余元素的顺序。所以我们要反其道而行之,我们先建大堆,堆顶元素就是最大的元素。
  2. 然后利用堆删除的思想,交换堆顶和堆底的元素,然后将堆的大小减小1个数据,这时候最大的元素就被换到了最后,再向下调整现在的栈顶元素。
  3. 重复过程2,直到剩堆中只剩一个元素,就完成了堆排序。

降序的思想也与升序相似:

  1. 降序就是从大到小排列,正常思维应该是建大堆,可是建大堆无法保证除了堆顶元素其余元素的顺序。所以我们要反其道而行之,我们先建小堆,堆顶元素就是最小的元素。
  2. 然后利用堆删除的思想,交换堆顶和堆底的元素,然后将堆的大小减小1个数据,这时候最小的元素就被换到了最后,再向下调整现在的栈顶元素。
  3. 重复过程2,直到剩堆中只剩一个元素,就完成了堆排序。

具体实例图解如下:

【数据结构】堆的全解析_第26张图片

堆排序动态的逻辑过程:

【数据结构】堆的全解析_第27张图片

具体代码实现:

// 堆排序升序
void HeapSortUp(HPDataType* a, int n)
{
	assert(a);

	// 向下调整建大堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown_big(a, n, i);
	}

	// 将最后一个元素与堆顶元素交换,选出最大的元素,堆大小减一,然后向下调整,选出此大的元素,以此类推
	for (int i = n - 1; i > 0; i--)
	{
		Swap(&a[0], &a[i]);
		AdjustDown_big(a, i, 0);
	}
}

// 堆排序降序
void HeapSortDown(HPDataType* a, int n)
{
	assert(a);

	// 向下调整建小堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown_little(a, n, i);
	}

	// 将最后一个元素与堆顶元素交换,选出最小的元素,堆大小减一,然后向下调整,选出此大的元素,以此类推
	for (int i = n - 1; i > 0; i--)
	{
		Swap(&a[0], &a[i]);
		AdjustDown_little(a, i, 0);
	}
}

代码逻辑动态展示:



堆的全局代码


typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}Heap;

// 堆的构建
void HeapInit(Heap* hp);
// 堆的销毁
void HeapDestory(Heap* hp);
// 向上调整
void AdjustUp_big(HPDataType* a, int child);
void AdjustUp_little(HPDataType* a, int child);
// 堆的插入
void HeapPush_big(Heap* hp, HPDataType x);
void HeapPush_little(Heap* hp, HPDataType x);
// 堆的打印
void HeapPrint(Heap* hp);
// 堆的删除
void HeapPop_big(Heap* hp);
void HeapPop_little(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
bool HeapEmpty(Heap* hp);
// 堆的查找
int HeapSearch(Heap* hp, HPDataType x);
// 堆的任意删除
void HeapDelect_big(Heap* hp, int x);
void HeapDelect_little(Heap* hp, int x);

// TopK问题:找出N个数里面最大/最小的前K个问题。
// 需要注意:
// 找最大的前K个,建立K个数的小堆
// 找最小的前K个,建立K个数的大堆
void PrintTopK(int* a, int n, int k);

// 对数组进行堆排序
void HeapSortUp(HPDataType* a, int n);
void HeapSortDown(HPDataType* a, int n);


void HeapInit(Heap* hp)
{
	assert(hp);

	hp->a = NULL;
	hp->size = 0;
	hp->capacity = 0;
}

void HeapDestory(Heap* hp)
{
	assert(hp);

	free(hp->a);
	hp->capacity = 0;
	hp->size = 0;
}

bool HeapEmpty(Heap* hp)
{
	assert(hp);

	return hp->size == 0;
}

HPDataType HeapTop(Heap* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));

	return hp->a[0];
}

int HeapSize(Heap* hp)
{
	assert(hp);

	return hp->size;
}

void HeapPrint(Heap* hp)
{
	assert(hp);

	int i = 0;

	for (i = 0; i < HeapSize(hp); i++)
	{
		printf("%d ", hp->a[i]);
	}

	printf("\n");
}

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

void AdjustUp_big(HPDataType* a, int child)
{
	assert(a);

	while (child > 0)
	{
		int parent = (child - 1) / 2;

		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
		}
		else
		{
			break;
		}
		child = parent;
	}
}

void AdjustUp_little(HPDataType* a, int child)
{
	assert(a);

	//当child为0时,结束循环
	while (child > 0)
	{
		int parent = (child - 1) / 2;

		//孩子小于父亲时交换,否则退出
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
		}
		else
		{
			break;
		}
		child = parent;
	}
}

void HeapPush_big(Heap* hp, HPDataType x)
{
	assert(hp);

	if (hp->capacity == hp->size)
	{
		int newCapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(hp->a, newCapacity*sizeof(HPDataType));
		if (tmp == NULL)
		{
			printf("relloc fail");
			exit(-1);
		}
		hp->capacity = newCapacity;
		hp->a = tmp;
	}

	hp->a[hp->size] = x;
	AdjustUp_big(hp->a, hp->size);
	hp->size++;
}

void HeapPush_little(Heap* hp, HPDataType x)
{
	assert(hp);

	if (hp->capacity == hp->size)
	{
		int newCapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		HPDataType* tmp = realloc(hp->a, newCapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			printf("relloc fail");
			exit(-1);
		}
		hp->capacity = newCapacity;
		hp->a = tmp;
	}

	hp->a[hp->size] = x;
	AdjustUp_little(hp->a, hp->size);
	hp->size++;
}

void AdjustDown_big(HPDataType* a, int n, int parent)
{
	assert(a);

	int child = parent * 2 + 1;
	//孩子必须在堆的范围内
	while (child < n)
	{
		//选出两个孩子中最大的进行交换,并且要保证右孩子存在
		if (a[child + 1] > a[child] && child + 1 < n)
		{
			child++;
		}

		//孩子大于父亲,则交换
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
		}
		else
		{
			break;
		}

		parent = child;
		child = parent * 2 + 1;
	}
}

void AdjustDown_little(HPDataType* a, int n, int parent)
{
	assert(a);

	int child = parent * 2 + 1;
	//孩子必须在堆的范围内
	while (child < n)
	{
		//选出两个孩子中最小的进行交换,并且要保证右孩子存在
		if (a[child + 1] < a[child] && child + 1 < n)
		{
			child++;
		}

		//孩子小于父亲,则交换
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
		}
		else
		{
			break;
		}

		parent = child;
		child = parent * 2 + 1;
	}
}

void HeapPop_big(Heap* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));

	Swap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;
	AdjustDown_big(hp->a, hp->size, 0);
}

void HeapPop_little(Heap* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));

	Swap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;
	AdjustDown_little(hp->a, hp->size, 0);
}

int HeapSearch(Heap* hp, HPDataType x)
{
	assert(hp);

	for (int i = 0; i < hp->size; i++)
	{
		if (hp->a[i] == x)
		{
			return i;
		}
	}

	return -1;
}

void PrintTopK(int* a, int n, int k)
{
	assert(a);

	Heap hp;
	HeapInit(&hp);

	for (int i = 0; i < k; i++)
	{
		HeapPush_little(&hp, a[i]);
	}
	
	for (int i = k; i < n; i++)
	{
		if (a[i] > HeapTop(&hp))
		{
			hp.a[0] = a[i];
			AdjustDown_little(hp.a, hp.size, 0);
		}
	}

	HeapPrint(&hp);

	HeapDestory(&hp);
}

void HeapSortUp(HPDataType* a, int n)
{
	assert(a);

	// 向下调整建大堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown_big(a, n, i);
	}

	// 将最后一个元素与堆顶元素交换,选出最大的元素,堆大小减一,然后向下调整,选出此大的元素,以此类推
	for (int i = n - 1; i > 0; i--)
	{
		Swap(&a[0], &a[i]);
		AdjustDown_big(a, i, 0);
	}
}

void HeapSortDown(HPDataType* a, int n)
{
	assert(a);

	// 向下调整建小堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown_little(a, n, i);
	}

	// 将最后一个元素与堆顶元素交换,选出最小的元素,堆大小减一,然后向下调整,选出此大的元素,以此类推
	for (int i = n - 1; i > 0; i--)
	{
		Swap(&a[0], &a[i]);
		AdjustDown_little(a, i, 0);
	}
}

void HeapDelect_big(Heap* hp,int x)
{
	assert(hp);
	assert(!HeapEmpty(hp));
	assert(x >= 0 && x < hp->size);

	Swap(&hp->a[x], &hp->a[hp->size - 1]);
	hp->size--;
	AdjustDown_big(hp->a, hp->size, x);
}

void HeapDelect_little(Heap* hp,int x)
{
	assert(hp);
	assert(!HeapEmpty(hp));
	assert(x >= 0 && x < hp->size);

	Swap(&hp->a[x], &hp->a[hp->size - 1]);
	hp->size--;
	AdjustDown_little(hp->a, hp->size, x);
}

后记


恭喜呀!你已经掌握了堆的全部用法了!!距离大佬又近了一步!!!

写这篇文章对白晨来说也是一种挑战,特别是堆的精华部分——向下调整算法和向上调整算法,希望大家看的还算清楚明白。

白晨会不断提高自己的制图水平和语言描述能力,希望能让每个人都看得清楚明白,大家有什么想法都可以私信白晨呀,无论是提意见还是交朋友,白晨都会一一回复的。


最后如果大家喜欢我这篇文章,不如给我一个大拇指小星星 ⭐️,支持一下白晨吧!喜欢白晨《数据结构》系列的话,不如关注白晨,以便看到最新更新哟!!!

我是不太能熬夜的白晨,我们下篇文章见。

你可能感兴趣的:(数据结构,数据结构,排序算法,链表,c语言,算法)