堆的介绍与堆排序算法

在这里插入图片描述

堆的介绍与堆排序算法

一:堆的介绍

堆的存储结构和逻辑结构

堆的两种初始化

堆的销毁

堆的数据插入之向上调整算法

堆的数据移除之向下调整算法

建堆的两种方法以及时间复杂度分析

二: 堆排序

刨析堆排序

升序建大堆or升序建小堆

嗨喽大家好呀,我叫鑫鑫,这是我第一次写博客,想和大家分享一下我学习堆这一数据结构的感悟,希望对大家进一步深入理解堆能有帮助,那么,让我们开始领略堆的魅力吧!

1.数据结构之堆
定义:如果有一个关键码的集合,把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,则称之为堆
在这里插入图片描述
上面的图片很明显的反应了堆的存储结构和逻辑结构,并且可以看出建出堆并不代表堆的存储结构是有序的,只能得出数组中的最大值和最小值,相信眼尖的朋友已经发现了

小堆:堆顶的数字为最小,父亲节点的值一定小于子节点,而兄弟和叔侄之间的大小并不能确认
大堆:堆顶的数字最大,结论同小堆

那么这时候就会有同学问了,那堆这个数据结构有什么用呀?只能得到最大和最小的值也不能够排序呀,堆排序真的有用吗?嘿,别着急呀,相信你看下去一定会认为堆排序是一个又快又简便的方法。

2.堆的父子节点下标的关系
同学们一定要将下面这两个关系记牢,我们在后面将反复用到这个关系
我们将一个父亲节点的两个孩子分别记为leftchild和rightchild

leftchild=2parent+1
rightchild = 2
parent+2
parent = (child-1)/2

3.堆的类型定义以及初始化

typedef int HPDatetype;
typedef struct Heap//堆的底层为顺序表
{
	HPDatetype* a;
	int size;
	int capacity;
}HP;
1.void HPInit(HP* php)//对堆进行初始化
{
	assert(php);
	php->a = NULL;
	php->capacity = 0;
	php->size = 0;
}
还有一种初始化等我们介绍完向下调整算法再给出

4.堆的销毁

void HPDestory(HP* php)//销毁堆
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->capacity = php->size = 0;
}

5.堆的插入以及向上调整算法
下面我们还是先给出代码

void Swap(HPDatetype* px, HPDatetype* py)
{
	HPDatetype m =*px;
	*px =* py;
	*py = m;
}
void Adjustup(HPDatetype* a,int child)//小堆
{
	int parent = (child - 1) / 2;
	while (child>0)//?
	{
		if (a[child] < a[parent])//如果子比父亲小,子当父亲
		{
			Swap(&child, &parent);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
void HPPush(HP* php, HPDatetype x)//对堆进行插入 
{
	assert(php);
	if (php->size == php->capacity)//说明空间不够需要进行扩容
	{//判断是否为空
		HPDatetype newcapacity = php->capacity == 0 ? 4 : (php->capacity) * 2;
		HPDatetype* tmp = realloc(php->a, sizeof(HPDatetype)*newcapacity);
		if(tmp == NULL)
		{
			perror("realloc failed");
			return;
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}//扩容完成
	php->a[php->size] = x;
	//进行向上排序算法;
	php->size++;
	Adjustup(php->a, php->size - 1);
}

注意我们这里的堆建的都是小堆,同学们在阅读完本文章后可以自己去尝试写出大堆的代码
在上面的代码中,我们一共创建了三个函数,分别用来插入时空间不够进行扩容,向上调整算法还有交换两个数组元素的值
1.简单的realloc函数,我们不再过多赘述,后面我会将c中的字符串函数和内存函数整理出来供大家参考
2.我们着重来讲解一下向上调整算法的思路分析:我们在函数内定义了两个局部变量child和parent,此处的parent用我们上文给出的公式能轻易得到,我们采用一个循环的方式,循环条件是child>0,注意为什么不是parent>0?原因如下:如果结束条件是parent>=0的话,在child等于0时,(parent-1)/2准确意思是-1除2等于0,再次进入循环a[child]=a[parent],巧合的结束循环,当建小堆时,如果孩子的值比父亲的值大,那么将孩子与父亲的值进行交换,将父亲的下标给孩子,继续求出下一个父亲的下标,当child等于0时,结束循环。
3.Adjustup(php->a, php->size - 1);此处size开始指向的0,当数组元素满时指向的是最后一个元素的下一个位置,所以传size-1

6.如何将一个数组按堆的形式输出

int a[10] = { 5,6,7,2,1,78,44,11,22,33, };
int tmp = sizeof(a) / sizeof(a[0]);
HP hp;
HPInit(&hp);
for (int i = 0; i < tmp; i++)
{
	HPPush(&hp, a[i]);
}
HPDestory(&hp);

7.堆的数据移除之向下调整算法
在我们引出向下调整算法前,我们先了解一下我们堆的另外两个接口函数,这两个函数互相配合使用,相辅相成

HPDatetype HPTop(HP* php)//取堆顶元素
{
	assert(php);
	HPDatetype tmp = php->a[0];
	return tmp;
}
void HPPop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;

	AdjustDown(php->a, php->size, 0);
}

第一个接口时取出堆顶的元素,而第二个接口便是去除堆顶元素,那么迎接我们的将是如何去除堆顶元素

问题1.去除堆顶元素怎么去除
问题2.去除堆顶元素会对堆的结构有影响吗
答案是肯定的哈哈,如果直接进行元素的移位,那么堆的结构是会被破坏的哦,多说无益,上图!
在这里插入图片描述
此时是不是很明显,右边的堆结构已经被破坏。所以直接删除法不可取,就有一位大佬提出一个非常厉害的算法,叫做向下调整算法,代码如下

void Adjustdown(HPDatetype* a, HPDatetype parent,int n)//进行向下调整算法
{ 
	int child = 2 * parent + 1;
	while (child<n)
	{
		//此时左跟右也要进行比较,假设法
		if (child+1<n && a[child + 1] < a[child])//说明此时较小的为child+1
		{
			child = child + 1;
		}
		if (a[child] < a[parent])//说明孩子比父亲小
		{
			Swap(&child,& parent);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

此算法需要注意的是
//此时左跟右也要进行比较,假设法
if (child+1<n && a[child + 1] < a[child]) //说明此时较小的为child+1
一个要注意的是细节的控制,还有假设法的应用
思路:我们采取向下调整算法来解决这题,当我们采用HPPop这个函数接口时,我们先进行首位下标元素值的交换,然后将交换之后的堆顶元素向下调整,调整前,先进行左孩子和右孩子大小的比较(假设法),因为是小堆嘛,所以谁的值小谁是爸爸!哈哈,后续思路按照代码就可以很容易得出了,不过这里也要注意细节的把控。

注意:采取向下调整算法的时候,堆顶的左右子树一定要是堆,如果是一堆杂乱的数字,我们是不能使用此算法的

8.建堆的两种方法以及时间复杂度分析
下面进入我们本博客的重头戏,两种建堆方法的时间复杂度分析,有的同学会说,我们上文不是已经将数组的值用堆输出了吗,怎么还要学习建堆的方法。
是的,上面那种方法确实能建堆,但还不是真正的堆排序哦,想实现堆排序,那么先让我们来认识两种建堆的方法吧。

在我们学习这两种建堆方式之前,我们认识一下节点个数与我们堆高度的关系

在这里插入图片描述

第一个式子h = log(N+1)是满二叉树高度与个数的关系
第二个式子logN-1则是当最后一层只有一个叶子节点时堆高度与个数的关系

有了这两个式子我们就能尝试进击这两种建堆方式了,我们从向上建堆开始
在这里插入图片描述

从这张图我们很明显就能得出节点个数与建堆消耗时间复杂度的关系是O(NlogN)
思路:最后被一层的节点向上最多调整h-1次,第一次的不需要调整以此类推,可以通过高中数列的方法来求解,这里不再赘述

而有了向上调整建堆的经验,向下调整建堆那不是信手拈来?

//进行向下建堆,向下建堆传的是堆的元素个数,所以n-1为最后一个节点的下标
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
	AdjustDown(a, n, i);
}//建堆结束

这里我们也是直接给出结论和代码,思路:最后一层叶子节点不需要向下调整,不管是大堆还是小堆都是如此,找到第一个不是叶子节点的节点开始向下调整,解释一下int i = (size-1-1)/2的意思,size-1是个数,此时看作最后一个子孩子,此式利用我们上文给出的结论便可轻易得到,我们求的是父亲节点的位置。
给出向上调整建堆的时间复杂度为O(N),和上文求解方式一致,不再赘述

看到这来,同学们肯定都知道了,日常我们建堆多采取的是向下建堆,它的时间复杂度更小
接下来,我就来解决当问遗留下来的第二个初始化的方法,运用我们的向下调整建堆

void HPInitArray(HP* php,int* a, int n)
{
	assert(php);

	php->a = (int*)malloc(sizeof(int) * n);
	if (php->a == NULL)
	{
		perror("malloc fail");
		return;
	}
	memcpy(php->a, a, sizeof(int) * n);
	php->capacity = php->size = n;

	// 向上调整,建堆 O(N*logN)
	//for (int i = 1; i < php->size; i++)
	//{
	//	AdjustUp(php->a, i);
	//}

	// 向下调整,建堆 O(N)
	for (int i = (php->size - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(php->a, php->size, i);
	}
}

此时我们采用memcpy拷贝一个数组,再运用向下建堆这个函数接口,是不是很轻易的就将堆建好l呀,哈哈

9.堆排序!!!
相信看到这里的同学们早就已经忍耐不住想要知道堆排序究竟是何方神圣了,我想说的是,如果你能理解到前面我所讲的这些知识点。堆排序对你来说轻而易举。
下面来介绍第一种,采取拷贝一个数组和创建一个堆的数据结构的方法来实现

void HeapSort(int* a, int n)
{
	HP hp;
	HPInitArray(&hp, a, n);

    int i = 0;
	while (!HPEmpty(&hp))
	{
		a[i++] = HPTop(&hp);

		HPPop(&hp);
	}

	HPDestroy(&hp);
}

这种方法时间复杂度也能达到O(NlogN),但是c写出来不太方便,c++有现成的数据结构会方便不少,但这种方法需要较大的空间复杂度

下面介绍一种直接对数组建堆的方法,此时就要考验大家对堆的逻辑结构和存储结构的理解深不深刻了,如果不太熟练,还是不太容易绕出来的哦哈哈。

10.升序建大堆,降序建小堆
在这里插入图片描述
从图中很明显的能看出,如果你采用升序建小堆的话,注意这个时候你的第一个位置是正确的元素,但是剩下的元素此时在数组中的下标对应的元素映射到堆结构,是构不成堆的,此时堆结构被破坏了,和我们上面创建接口函数HPPop时直接删除犯了同样的错误
此时,若想继续排序,需要堆n-1个数重新向下建堆,这时这个算法的时间复杂度达到了n²的量级,还不如采用一个最简单的冒泡排序,何须大费周章学堆排序

所以,我么得出排升序我们应该建大堆
步骤1.首尾下标的值进行交换,因为建大堆,所以堆顶的元素是最大的
步骤2.最后一个值不看做堆里的,向下调整选出次大的数据,重复操作,最后将数组变成有序数组

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

	// O(N*logN)
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

此时的end表示的是最后一个元素的下标,至于为什么先向下调整,再进行–end,你听我给你道来:

因为向下调整函数中的其中一个参数传的是n而n是元素的个数,上文提到,首位交换后,将最后一个元素不看做堆里的,所以传的n应该是n-1,而此时end的值刚好为n-1,如果我先–end,那么堆排序将出bug。

好滴,感谢大家的浏览,这是我第一次写博客,希望大家能有所得,觉得我写的还可以的同学留下三连哦,谢谢!

  • 37
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值