【数据结构】堆(包含堆排序和TopK问题)

在这里插入图片描述

本篇博客由 CSDN@先搞面包再谈爱 原创,转载请标注清楚,请勿抄袭。

堆是啥

其实堆也算是完全二叉树(不知道完全二叉树的先了解一下完全二叉树是啥再来看这个),只不过堆专门是用顺序表的形式来存储的。

堆的分类

大根堆:父比儿子大,就是大根堆。
小根堆:父比儿子小,就是小根堆。

来个图:

芝士小根堆:
在这里插入图片描述

芝士大根堆:
在这里插入图片描述

根据上面这两句话和这张图就可以得出:

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

上面基本上涵盖了所有堆的概念了,我们下来讲讲怎么实现。

堆的实现(以大根堆为例)

首先,堆使用顺序表存储的,也就是数组,那么怎么初始化一个堆呢?

我们先来给一组数据:

a[6] = { 27, 39, 18, 5, 9, 32};

怎么用这个数组建堆?

看图解:

初始状态下是这样的:
在这里插入图片描述

建堆有两种方法:向上调整和向下调整

向上调整

我们要从数组中第0个元素开始建堆。我们每次选第i个数(i从0开始),将这个数与先前建好的堆进行构建新堆,不断往堆顶挪,直到到达堆顶或者是遇到了比自己大的数就停止,这样调整堆的方法就叫做向上调整。

我可能解释的不是很到位,但是我已经很尽力了,下面就看一下图解:
在这里插入图片描述
在这里插入图片描述

代码实现:

//向上调整
void AdjustUp(HPDataType* 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;
	}
}

向下调整

从倒数第一个非叶子节点开始,将该数与其下方的数进行对比,若出现比两个子树元素大的情况,就将子树中最大的挪到父结点处直至越界停止,然后再判断非叶子节点的前一个结点不断重复直至到堆顶,这样的调整就叫做向下调整。

看图解:在这里插入图片描述

代码实现:

//向下调整
void AdjustDown(HPDataType* a, int parent, int n)
{							//这里parent是下标
	//直接先选择左子树
	int child = parent * 2 + 1;
	//确定范围
	while (child < n)
	{
		//找出左右子树中最大的那个
		//先判断右越界了没有
		if (child + 1 < n && a[child + 1] > a[child])
			child++;//如果右大就选右边的

		if (a[child] > a[parent])
			swap(&a[child], &a[parent]);

		//调整儿子与爹的位置
		parent = child;
		child = parent * 2 + 1;
	}
}

建堆

建堆的时候,是要将堆中的数组进行调整,一步一步调整成大堆/小堆的。

// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n)
{
	//上面的这些就是把数组中的元素放到堆里,没什么用,下面的两个调整堆才有用。
	/*hp->_a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (hp->_a == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	for (int i = 0; i < n; i++)
	{
		hp->_a[i] = a[i];
	}

	hp->_size = n;
	hp->_capacity = n;*/

	//向上调整建堆
	for (int i = 1; i < hp->_size; i++)
	{
		AdjustUp(hp->_a, i);
	}

	//向下调整建堆
	//这里i的初始值就是第一个非叶子节点的下标。
	for (int i = (hp->_size - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(hp->_a, i, hp->_size);
	}
}

用向上调整建堆和向下调整建堆的时间复杂度

向下调整建堆的时间复杂度
在这里插入图片描述

我们可以看到,向下调整建堆时,最后一层的结点不需要向下移动,而满二叉树中最后一层的结点占了总结点个数的一半+1;但是向上调整的时候最后一层的结点还需要移动,只有堆顶的数不需要移动。所以向下调整是要比向上调整更简单一些的。

向上调整的时间复杂度计算方法类似,我这里就直接给结果了:O(N*logN)

所以总的来说,建堆还是推荐使用向下调整建堆。

堆的插入

插入数的时候是在数组的末尾插的,所以我们只需要将该节点与其祖先结点惊醒调整就可以了,也就是说只需要调一次。

比如说我现在在末尾插入一个57,那么看图:
在这里插入图片描述
代码实现:

// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
	//上面这些是判断当前堆中的数组是否满了
	/*if (hp->_capacity == hp->_size)
	{
		int NewCapacity = hp->_capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(hp->_a, NewCapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}

		hp->_a = tmp;
		hp->_capacity = NewCapacity;
	}*/

	hp->_a[hp->_size] = x;
	hp->_size++;
	//插入是在数组的尾插入的,所以要用到向上调整
	AdjustUp(hp->_a, hp->_size - 1);
}

堆的删除

删除删的是堆顶元素,我们需要先将堆顶元素与数组中的最后一个元素交换,然后再从堆顶开始调整堆。

看图解:
在这里插入图片描述

上面这些基本上就是堆实现的方法了,最主要掌握的就是向上调整和向下调整。

给几道题奖励一下:

  1. 下列关键字序列为堆的是:()
    A 100,60,70,50,32,65
    B 60,70,65,50,32,100
    C 65,100,70,32,50,60
    D 70,65,100,32,50,60
    E 32,50,100,70,65,60
    F 50,100,70,65,60,32
  2. 已知小根堆为8,15,10,21,34,16,12,删除关键字 8 之后需重建堆,在此过程中,关键字之间的比较次
    数是()。
    A 1
    B 2
    C 3
    D 4
  3. 一组记录排序码为(5 11 7 2 3 17),则利用堆排序方法建立的初始堆为
    A(11 5 7 2 3 17)
    B(11 5 7 2 17 3)
    C(17 11 7 2 3 5)
    D(17 11 7 5 3 2)
    E(17 7 11 3 5 2)
    F(17 7 11 3 2 5)
  4. 最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是()
    A[3,2,5,7,4,6,8]
    B[2,3,5,7,4,6,8]
    C[2,3,4,5,7,8,6]
    D[2,3,4,5,6,7,8]

答案:1.A 2.C 3.C 4.C

堆排序

大致思路

首先堆排序,得先建堆,建大堆还是建小堆是由排升序还是排降序来决定的。

排升序就建大堆,排降序就建小堆。(记住)

如果你想对一个数组排序,那么数组本身就是一个堆,所以我们就可以直接对数组进行调整,而不是再创建一个堆来进行堆排序。

怎么调整呢,和调整堆中的数组一样的方法,向上调整或者向下调整,这两个方法都可以建堆。上面也讲到了,推荐使用向下调整建堆。所以我下面就使用向下调整来建堆了。

代码实现:

	//建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, i, n);
	}

建完堆之后就要开始堆排序了。

这是一个循环,每次都要将头(0)尾(n-1)进行交换,将尾–,然后再从头开始向下调整,直到当尾变成1就停止。

看图:

在这里插入图片描述
代码实现:

void HeapSort(int* a, int n)
{
	//建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, i, n);
	}

	//首尾交换,向下调整
	int end = n - 1;
	while (end > 0)
	{
		swap(&a[0], &a[end]);
		end--;
		AdjustDown(a, 0, end + 1);
	}
}

TopK问题

// TopK问题:找出N个数里面最大/最小的前K个问题。
比如:陕西排名前10的羊肉泡馍,郑州排名前五的烩面,王者荣耀全国排名前10的李白。等等问题都是Topk问题,

首先还是建堆
如果要的是N个数中最大的前K个数,那就建小堆。
如果要的是N个数中最小的前K个数,那就建大堆。

大致思路

以大的前K个数为例:

先搞一个大小为K的数组,然后把N个数中的前K个数放到数组中,然后把大小为K的数组建堆成小堆,然后将后N - K个数与堆顶进行比较,如果比堆顶数小,就比较下一个,如果比堆顶数大,就替换掉堆顶(这样能保证前K个数能进入堆内)。不断遍历,直到N个数遍历完毕。

如果你建的是大堆的话,当我们没有遍历完N个数时,最大的数可能已经占到堆顶位置了,所以无论再怎么遍历,都无法将前K大的数入队

代码实现

void PrintTopK(int* a, int n, int k)
{
	int* Top = (int*)malloc(k * sizeof(int));
	if (Top == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	//搞出存放前K个数的数组
	for (int i = 0; i < k; i++)
	{
		Top[i] = a[i];
	}

	//建堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(Top, i, k);
	}

	for (int i = k; i < n; i++)
	{
		if (a[i] > Top[0])
			Top[0] = a[i];

		AdjustDown(Top, 0, k);
	}

	for (int i = 0; i < k; i++)
	{
		printf("%d ", Top[i]);
	}

}

到此结束。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

先搞面包再谈爱

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

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

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

打赏作者

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

抵扣说明:

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

余额充值