堆的实现以及应用(详细解析,包含图文解析)

目录

前言

预备知识

堆的概念

堆的分类

堆的基本操作

建堆(BuildHeap)

插入操作(HeapInsert)

删除操作(HeapPop)

堆的应用


前言

在学习堆之前需要先了解树的相关概念。如果了解过树可以直接跳过预备知识。

预备知识

树的概念:

树是一种非线性的数据结构。树(tree)可以用几种方式定义。定义树的一种自然方式是递归的方法。一棵树是一些节点的集合。这个集合可以是空集;若非空,则一颗树由称做根的节点r以及0个或多个非空的子树T1,T2,...Tk组成,这些子树中的每一颗的根都被来自根r的一条有向的边(edge)所连接。

每一颗子树的根叫做根r的儿子(child),而r是每一颗子树的根的父亲(parent)。图4-1显示用递归定义的典型的树。

树的相关概念

  1. 节点的:一个节点含有的子树的个数称为该节点的度; 如上图:A的度为6、B的度为0。
  2. 叶节点终端节点度为0的节点称为叶节点; 如上图:B、C、H...等节点为叶节点。
  3. 树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
  4. 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推。
  5. 父节点若一个节点含有子节点,则这个节点称为其子节点的父节点。 如上图:A是B的父节点。
  6. 子节点与子树的根节点直接相连的节点称为该根节点的子节点。 如上图:B是A的孩子节点。

二叉树的概念

二叉树(binary tree)是一棵特殊的树,其中每个节点都不能有多于2个的子节点

图4-11显示一颗由一个根和两颗子树组成的二叉树,TL和TR均可能为空

特殊的二叉树

  • 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。
  • 完全二叉树:对于深度为h的,有n个结点的二叉树,当且仅当其每一个结点都与深度为h的满二叉树中编号从1至n的结点一一对应时,称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。

二叉树的性质

  • 规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2^{h-1}结点。
  • 规定根节点的层数为1,则深度为h的二叉树的最大结点数是 2^{h}-1
  • 对任何一棵二叉树, 如果叶结点个数为{n_{0}}^{} , 度为2的分支结点个数为{n_{2}}^{} ,则有 {n_{0}}^{}{n_{2}}^{}+1
  • 规定根节点的层数为1,具有n个结点的满二叉树的深度,h= {log_{2}}^{n+1}
  • 若n>0,n位置节点的父节点(n-1)/2当n=0,n为根节点编号,无父节点 。若2n+1<2^{h}-1,左孩子序号2i+1若2n+1>=2^{h}-1否则无左孩子 。 若2n+2<2^{h}-1,右孩子序号:2n+2若2n+2>=2^{h}-1否则无右孩子

堆的概念

堆又称二叉堆(binary heap),C++中也称优先队列(priority_queue)。堆是一颗完全二叉树。完全二叉树很有规律,所以堆可以用一个数组表示,不需要指针。图6-3中的数组对应图6-2中的堆。

堆的定义

typedef int HeapDataType;
typedef struct Heap
{
	HeapDataType* _arr;
	int _size;
	int _capacity;
}Heap;

堆的分类

  • 大堆:每颗子树的根节点总是大于其他节点的值,也就是说根节点的值就是最大的。
  • 小堆:每颗子树的根节点总是小于其他节点的值,也就是说根节点的值就是最小的。

堆的基本操作

通过建立小堆案例分析,我们通过下图可知,根节点的左右子树都是一个小堆,那我们可以从根节点开始向下去调整堆的结构,保持堆的特性。这种的操作称为下滤。下滤的前提一定是左右子树必须是一个堆,才能进行操作。

下滤操作的过程:

调整的位置为parent,首先选出parent的左右孩子中最小的一个记作child,让child和parent相比较。以小堆为例,如果parent < child 那就不需要调整直接结束,如果parent > child那么将parent和child的关键值互换,并把child赋值给parent进行迭代,重复上述过程直到结束。

下滤算法代码参考:

void Swap(HeapDataType* x, HeapDataType* y)
{
	HeapDataType temp = *x;
	*x = *y;
	*y = temp;
}
void AdjustDown(HeapDataType* arr, int n, int root)
{
	int parent = root;//根结点
	int child = parent * 2 + 1;//左孩子
	while (child < n)
	{
		//注意右孩子不能超出数组大小
		if ( (child + 1 < n) && (arr[child + 1] < arr[child]))
		{
			child++;//寻找左右孩子中最小的数
		}
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

建堆(BuildHeap)

建堆需要利用下滤算法,下滤算法的前提是下调位置的左右子树均为堆,而我们要怎么保证左右子树均为堆?

如下图所示,利用一随意的数组来建立堆,不能直接从根节点开始下滤。通过观察可以发现最后一层的树都没有左右子树,我们其实就可以从数组的最后一个元素开始调,但是,我们可以发现最后一层的叶子节点都不需要调,只需要从数组的最后一个元素的父节点开始调。

建堆代码演示:

void BuildHeap(Heap* php, HeapDataType* arr, int n)
{
	assert(php);
	php->_arr = (HeapDataType*)malloc(sizeof(HeapDataType) * n);
	if (php->_arr == NULL)
	{
		printf("内存不足\n");
		exit(1);
	}
	//数组的数据拷贝到堆中
	memcpy(php->_arr, arr, sizeof(HeapDataType) * n);
	php->_size = n;
	php->_capacity = n;
	//构造堆
    //n -1 代表数组的最后一个元素
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(php->_arr, php->_size, i);
	}
}

插入操作(HeapInsert)

为将元素x插入堆中,我们要先把x插入数组下标为size的位置,否则可能会破坏堆的特性。如果x可以放在size位置而不破坏堆的序,那么插入完成。否则,我们需要往上去调整,该操作称为上滤

上滤算法:

如下图所示;我们只需要调整插入位置到根节点的路径上的节点。记插入位置为child,插入位置的父节点为parent。通过比较child的值和parent的值。如果child > parent 那就不需要调整。如果child < parent,那就需要进行上滤调整,将parent赋值给child进行迭代重复上述操作,直到调整结束。

上滤算法演示:

void AdjustUp(HeapDataType* arr, int n, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (arr[parent] > arr[child])//小堆
		{
			Swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child - 1) / 2;;
		}
		else
		{
			break;
		}
	}
}

插入操作代码演示:

void HeapInsert(Heap* php, HeapDataType x)
{
	assert(php);
	if (php->_size == php->_capacity)//扩容
	{
		php->_capacity *= 2;
		HeapDataType* temp = (HeapDataType*)realloc(php->_arr, 
			sizeof(HeapDataType) * php->_capacity);
		assert(temp);
		php->_arr = temp;
	}
	php->_arr[php->_size++] = x;
	AdjustUp(php->_arr, php->_size, php->_size - 1);
}

删除操作(HeapPop)

如下图所示;删除最后一个元素是没有任何意义的,我们应该删除(pop)堆顶的数据,来获得第二大或第二小的元素才有意义。

  • 具体操作:将堆顶的元素最后一个元素互换size--,再从堆顶位置进行下滤算法操作,就可以得到第二大或第二小的元素。

删除代码演示:

void HeapPop(Heap* php)
{
	assert(php);
	assert(php->_size > 0);
	Swap(&php->_arr[0], &php->_arr[php->_size - 1]);
	php->_size--;
	AdjustDown(php->_arr, php->_size, 0);
}

到此堆的一些基本操作以及基本实现。完整的代码如下所示。(含测试案例)

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
typedef int HeapDataType;
typedef struct Heap
{
	HeapDataType* _arr;
	int _size;
	int _capacity;
}Heap;
void Swap(HeapDataType* x, HeapDataType* y)
{
	HeapDataType temp = *x;
	*x = *y;
	*y = temp;
}
void AdjustDown(HeapDataType* arr, int n, int root)
{
	int parent = root;//根结点
	int child = parent * 2 + 1;//左孩子
	while (child < n)
	{
		//注意右孩子不能超出数组大小
		if ( (child + 1 < n) && (arr[child + 1] < arr[child]))
		{
			child++;//寻找左右孩子中最小的数
		}
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void HeapInit(Heap* php, HeapDataType* arr, int n)
{
	assert(php);
	php->_arr = (HeapDataType*)malloc(sizeof(HeapDataType) * n);
	if (php->_arr == NULL)
	{
		printf("内存不足\n");
		exit(1);
	}
	//数组的数据拷贝到堆中
	memcpy(php->_arr, arr, sizeof(HeapDataType) * n);
	php->_size = n;
	php->_capacity = n;
	//构造堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(php->_arr, php->_size, i);
	}
}
void HeapDestory(Heap* php)
{
	assert(php);
	free(php->_arr);
	php->_arr = NULL;
	php->_capacity = php->_size = 0;
}
void AdjustUp(HeapDataType* arr, int n, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (arr[parent] > arr[child])//小堆
		{
			Swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child - 1) / 2;;
		}
		else
		{
			break;
		}
	}
}
void HeapInsert(Heap* php, HeapDataType x)
{
	assert(php);
	if (php->_size == php->_capacity)//扩容
	{
		php->_capacity *= 2;
		HeapDataType* temp = (HeapDataType*)realloc(php->_arr, 
			sizeof(HeapDataType) * php->_capacity);
		assert(temp);
		php->_arr = temp;
	}
	php->_arr[php->_size++] = x;
	AdjustUp(php->_arr, php->_size, php->_size - 1);
}
void HeapPop(Heap* php)
{
	assert(php);
	assert(php->_size > 0);
	Swap(&php->_arr[0], &php->_arr[php->_size - 1]);
	php->_size--;
	AdjustDown(php->_arr, php->_size, 0);
}
HeapDataType HeapTop(Heap* php)
{
	assert(php);
	assert(php->_size > 0);
	return php->_arr[0];
}
int main()
{
	HeapDataType a[] = { 27,10,19,16,28,35,65,48,30,45 };
	Heap hp;
	BuildHeap(&hp, a, sizeof(a) / sizeof(HeapDataType));
	for (int i = 0; i < hp._size; i++)
	{
		printf("%d ", hp._arr[i]);
	}
	printf("\n");
	HeapInsert(&hp, 15);
	for (int i = 0; i < hp._size; i++)
	{
		printf("%d ", hp._arr[i]);
	}
	printf("\n");
	HeapPop(&hp);
	for (int i = 0; i < hp._size; i++)
	{
		printf("%d ", hp._arr[i]);
	}
	return 0;
}

堆的应用

堆排序就是利用堆的思想进行排序

堆排步骤:

1、建堆

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

2、利用堆删除的思想进行排序(以升序为例)

  • 堆采取的存储结构为数组,假设这个数组的大小为n堆顶(数组首元素)的数据一定是最大的,将堆顶的数据与数组的最后一个元素进行交换,那么最大的元素就出现在数组的最后一个元素;这时只要将n--并且在堆顶进行一次下滤算法,就可以得到第二大的元素,同时再将堆顶的元素和当前数组的最后一个元素交换,就得到第二大的元素,如此重复上述的操作,直到n == 0就停止,最终就会得到一个升序的数组。具体操作下图所示。

堆排代码演示:

void HeapSort(int* a, int n)
{
	//1、建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
		
	}
	//排序
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
}

有关堆排的特点这里不做详解,后续会在排序部分一起分析。

Top-k问题

求数据集合N中前k个最大或最小的元素,这个数据集合一般会很大。

对于这部分的问题我们可以有2种思路

第一种思路:

  • 直接利用堆排对数据进行排序,从而筛选出前K个最大或最小的数据。这个思路就比较暴力,因为这个数据的集合很大,而堆排需要建堆,空间复杂度为O(n),万一内存存不下这么多数据就没有办法进行堆排

第二种思路(推荐)

  • 首先建立k个数的大堆或小堆,根据题目要求来决定。如果是要选出最大的k个数,那么建立小堆;否则建立大堆。其次遍历N-k个数据,如果比堆顶的元素大就覆盖堆顶的元素对堆顶进行下滤操作。遍历结束后,这k个数的小堆中,就是N中最大的前K个数

总结

堆就是一颗完全二叉树,关于对堆的后续学习可以去参考C++的优先队列(priority_queue)

  • 19
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值