数据结构——原来二叉树可以这么学?(3.堆的应用)

前言:

  在讲述完成堆以后,下面我们就可以借助堆的思想完成一个比较牛的排序:堆排序了,下面废话不多说,开启今天的代码之旅~


目录

1.堆排序

1.1.复习一下冒泡排序 

1.2.堆排序的理论讲解(小堆)

 1.2.1.版本一讲解

 1.2.2.版本二讲解

1.向上调整建堆

 2.向下调整建堆

1.3.堆排序的代码讲解(小堆)  

1.3.1.版本一 

1.3.2.版本二 

 2.1向上调整建堆

 2.2.向下调整建堆

1.4.时间复杂度分析

 1.4.1.版本一的时间复杂度分析

 1.4.2.版本二的时间复杂度分析

 1.向上调整建堆

 2.向下调整建堆

2.总结


正文:

1.堆排序

1.1.复习一下冒泡排序 

  对于排序,可能已经有很多的读者朋友已经不太陌生了,因为小编相信大多数读者朋友在C语言阶段已经学过了一个排序——冒泡排序,小编也写过冒泡排序的文章,下面小编放上链接:C语言重要算法之一——冒泡排序详解(干货满满,欢迎各位朋友的观看)_c语言冒泡算法-CSDN博客

  感兴趣的读者朋友可以看一看,这篇文章我们将会学到一个新的排序算法,那就是堆排序,在讲堆排序之前,小编先简单的带着读者朋友去复习一下冒泡排序,冒泡排序的思想就是让一系列数,从头开始进行两两比较,如果是升序,那么把最小的放在前面,通过循环我们便可以实现出冒泡排序,具体的内容小编就不在这里多说了,小编相信读者朋友都记得,如果有的读者朋友忘记的话,那么可以点开上面的链接来复习冒泡排序 (ps:其实小编就是想推销自己以前写的博客),下面小编直接放上代码,然后就开始堆排序的讲解。

void bubblesort(int* arr,int sz)
{
	int i = 0;
	int flag = 1;
	for (i = 0; i < sz; i++)
	{
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int m = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = m;
				flag = 0;
			}
		}
		if (flag == 1)
			break;
		flag = 1;
	}
}

1.2.堆排序的理论讲解(小堆)

  小编学过两个版本的堆排序,下面小编就分别来讲述两个版本的理论部分,并讲述它们的缺点。

 1.2.1.版本一讲解

  先来进行理论部分的讲解,在上一篇文章中,我们已经知道了什么是堆,然后实现了堆的大部分功能,当时小编并没有给各位读者朋友去运行这些功能,其实如果把一个已有的数组进行了建堆操作以后,之后我们在取堆顶元素,然后出堆操作,我们每取堆顶元素打印一次,如果是小堆的话,最后会呈现一个降序的序列,我们可以通过每取出堆顶操作然后把它放入数组中,通过循环我们就可以让这个数组变成一个降序数组,这就是版本一的操作,版本一就是通过我们已有的堆的功能实现出堆排序,不过,版本一是有着明显的缺点的,那就是我们如果想要完成版本一的操作那么就必须要把堆写完整,因为此时我们用到了堆的功能,所以我们要写堆的结构体,写入堆,出堆,取堆顶操作,这样无疑是很麻烦的,因为我们在写算法题的时候如果这样操作的话,一是增加我们代码量,让代码显的很冗余,并且增加我们写代码的时间;二是加快出错的概率,因为这涉及到了多个函数的书写,如果其中一个函数写错的话,那么这个排序我们便运行不下去;所以小编不推荐版本一代码的书写,不过版本一可能是我们在学习完堆最容易想到的一种堆排序,所以它还是比较有学习意义的,以上便就是版本一理论部分的讲解,下面小编开始最常用的堆排序——版本二的讲解!(版本二是在版本一的基础上实现的)

 1.2.2.版本二讲解

  版本二的堆排序,是我们之后最常使用的堆排序(小编可能以后会在排序的讲解中重新讲述一遍堆排序),这个版本的堆排序其实是在版本一的基础上推出来的排序,所以小编在上面讲述过,版本一是有学习意义的,在版本一种,我们知道每次取堆顶元素然后出堆操作后,就可以实现一个降序的排序,所以我们可以依靠这个特性来帮助我们去实现堆排序,对于版本二的堆排序,其实还有两种排序方法,这两种方法的区别就是在于建堆,一个是向上调整建堆,一个是向下调整建堆,下面小编就先分别说一下这两种建堆方式的不同,然后会在代码部分给各位读者朋友去分析一下哪一种排序方法是比较好的。

1.向上调整建堆

  向上调整建堆操作就是我们在入堆操作的时候使用的建堆方法,不过此时我们是需要调整数组元素进行建堆操作,这个时候我们通过循环的方式就可以实现向上建堆操作了,因为向上调整建堆函数我们也已经知道了(不晓得可以看小编上篇文章,不过小编会在下面代码讲解部分也浅浅的说一下),此时我们建好堆以后,我们先保存好数组(堆)中最后一个元素的位置,之后我们可以先让堆头元素和堆尾元素进行交换,然后我们在进行向下调整建堆操作,这时候我们就可以把最小的元素放入到最后了,之后依次循环以后,我们就可以实现一个降序的数组了,这便是堆排序版本二其中一个写法,这个写法我们并没有使用到堆的功能,我们通过向上调整建堆和向下调整建堆便可以实现,所以可以看出版本二写法的优越性,这是第一种建堆方式,下面小编继续讲解第二种建堆方式。

 2.向下调整建堆

  向下调整建堆法我们在出堆的时候使用过,当时我们是在出堆操作的时候使用过向下调整堆的用法,其实这个用法同样也可以适用于建堆操作,此时我们并不是从头开始进行向下调整建堆了,而是在最后一个结点的父结点开始进行向下调整建堆,这么做的原因是因为此时我们整个数组都还没有被建堆,在当初我们在出堆操作的时候,除了根结点之外,其他的都已经是建好的堆了,所以我们可以从根结点开始,这就是和堆排序的不同,堆排序我们要从最后一个结点的父结点开始进行向下建堆,之后通过循环继续往前走,直到走到根结点之后,我们就完成了建堆操作,这便是向下调整建堆,之后我们还是按照上面的和上面向上调整建堆一样,我们先保存好数组(堆)中最后一个元素的位置,之后我们可以先让堆头元素和堆尾元素进行交换,然后我们在进行向下调整建堆操作,这时候我们就可以把最小的元素放入到最后了,之后依次循环以后,我们就可以实现一个降序的数组了,此时我们仅需用到两个向下调整建堆就可以完成这个操作,所以先不看时间复杂度,此时我们仅需要书写向下调整建堆的函数就可以完成堆排序了,所以这个操作方法比上面的操作方法要显得更好一点,当然时间复杂度也会比上面优化一下,时间复杂度小编会在写完代码的时候带领大家去分析一下的~下面理论部分讲完了,开始我们的代码实战喽!各位系好完全带,准备上高速喽。

1.3.堆排序的代码讲解(小堆)  

  在讲解代码之前,小编先把上一篇博客写的向下调整建堆和向上调整建堆放在这里,免得一些读者朋友会感到疑惑。

向上调整建堆:

void Adjustup(int* arr,int child)  
{
	int parents = (child - 1) / 2;
	while (child > 0)
	{
		if (arr[parents] > arr[child])
		{
			Swap(&arr[parents], &arr[child]);
			child = parents;
			parents = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

向下调整建堆: 

void Adjustdown(int* arr, int parent, int n)
{
	int child = 2 * parent + 1;
	while (child < n)
	{
		//有时候符号不要打错
		if (child + 1 < n && arr[child] > arr[child + 1])
		{
			child++;
		}
		if (arr[parent] > arr[child])
		{
			Swap(&arr[parent], &arr[child]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

 入堆操作:

void HPPush(HP* s1, HeapDateType x)
{
	assert(s1);
	if (s1->capciaty == s1->size)  // 检查一下是否要进行扩容操作
	{
		int newcapciaty = s1->capciaty == 0 ? 4 : (s1->capciaty) * 2;  //三目操作符来判断此时的空间是不是空的,如果是空的话,那么就把它先设置4个大小的空间,如果不是空的,那么直接乘2,这是个不错的扩容方法
		HeapDateType* arr1 = (HeapDateType*)realloc(s1->arr, sizeof(HeapDateType) * newcapciaty);
		assert(arr1);
		s1->arr = arr1;
		s1->capciaty = newcapciaty;
	}
	s1->arr[s1->size] = x;
	Adjustup(s1 -> arr,s1 -> size);
	s1->size++;
}

出堆操作:

void HPPop(HP* s1)
{
	assert(s1 && s1->size);
	Swap(&s1->arr[0], &s1->arr[s1->size - 1]);
	s1->size--;
	Adjustdown(s1->arr, 0, s1->size);
}

取堆顶操作:

int HPTop(HP* s1)
{
	assert(s1 && !HPEmpty(s1));
	return s1->arr[0];
}

判断堆是否为空:

bool HPEmpty(HP* s1)
{
	return s1->size == 0;
}

交换两个数:

void Swap(int* x, int* y)
{
	int m = *x;
	*x = *y;
	*y = m;
}
1.3.1.版本一 

  对于版本一的代码,小编在理论部分已经说了,此时我们需要用到入堆操作,出堆操作和取堆顶操作,所以这个方法就要让我们写一个近乎完整的堆,所以代码量是一个很庞大的,等会小编写出来各位读者朋友就知道了,不过版本一是比较容易想出来的,我们仅需先创建一个堆类型的变量,然后把数组里面的数据循环入到堆里面,然后再通过取堆顶操作再把堆里面的数据放入数组里面,然后在进行出堆操作,之后我们经过循环之后,便可以完成一个堆排序,小编这么一说各位读者朋友可能会觉着这么写有点太过于重复,我们从数组取数据入堆,还要取堆顶入数组,这样显的很冗余,但这便是版本一的代码写法,下面小编给出代码:

void Heapsort(int* arr, int n)
{
	HP x;
	for (int i = 0; i < n; i++)
	{
		HPPush(&x,arr[i]);
	}
	int i = 0;
	while (!HPEmpty(&x))
	{
		arr[i++] = HPTop(&x);
		HPPop(&x);
	}
	HPDestory(&x);
}
1.3.2.版本二 
 2.1向上调整建堆

  先来说一下向上调整建堆,向上调整建堆法是很容易想出来的算法,我们仅需在建堆的时候通过循环的方式,把数组的元素从头到尾开始进行类似入堆的操作,循环条件自然就是数组元素个数了,在我们建完堆以后就可以开始进行堆排序了,我们排序方法也是很简单,因为根结点的元素自然是最小的数据,所以我们每次先把根结点的元素和数组最后一个元素进行交换,在进行一次交换以后,我们在进行向下调整堆的操作,重新找到一个小的元素放入堆顶,之后循环进行,我们就完成了代码的书写,下面小编给出代码:

void Heapsort(int* arr, int size)
{
	//先来第一种建堆方式,向上调整建堆,时间复杂度是O(nlogn)
	for (int i = 0; i < size; i++)
	{
		Adjustup(arr, i);
	}
	int end = size - 1;
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		Adjustdown(arr, 0, end);
		end--;
	}
}
 2.2.向下调整建堆

  这个建堆方法是小编认为最有效的建堆方式,等会小编分析完时间复杂度以后各位读者朋友就知道小编为什么会这么说了,向下调整建堆小编在理论部分也说了,我们仅需先找到最后一个结点的父结点,然后从这个父结点开始向下建堆,这个结点建完堆以后,我们再往前走就好了,就可以调整另外一个子树了,循环以后,便又建成了一个新堆,之后的排序操作就和上面是一样了,小编就不重复再写了(免得被说水字数),下面给出代码:

void Heapsort(int* arr, int size)
{

	//再来一种方式,向下调整建堆
	for (int i = (size - 1 - 1) / 2; i >= 0 ; i--)
	{
		Adjustdown(arr, i, size);
	}
	int end = size - 1;
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		Adjustdown(arr, 0, end);
		end--;
	}
}

  此时小编就完成了所有的堆排序,可能有很多读者朋友觉着这么多排序方式,我应该选择哪一个呢?小编先给出答案:直接闭眼选择版本二的向下调整建堆法,这时候可能一些读者朋友会说:我凭啥去相信你?不要着急,下面小编开始进行这三个代码的时间复杂度分析。 

1.4.时间复杂度分析

 1.4.1.版本一的时间复杂度分析

  这个版本的时间复杂度算是最好分析的时间复杂度了,因为通过观看上面的代码,相信各位学过如果计算复杂度的读者朋友已经知道版本一的时间复杂度是O(N)了,乍一看,这个时间复杂度不是蛮不错的吗,这个不该是更好用的吗?确实但从时间复杂度来看,这个算法确实是不错的,但是,这个算法的前提是我们需要写一个数据结构堆,这样会是代码量非常的复杂,所以小编不推荐这个版本的堆排序,因为代码冗余。

 1.4.2.版本二的时间复杂度分析
 1.向上调整建堆

  首先我们可以看向上调整建堆这个算法的时间复杂度,下面小编先给出一个堆的照片来帮助各位读者朋友更好的理解,并且小编给出分析:

   上面便是小编对于每一层结点个数的统计,并且总结了需要向上移动几层,我们可以看出结点越多,向上移动的层数就会越多,这样无疑就会让这个代码的时间复杂度变的越多,此时我们可以计算需要向上移动结点的总的移动不是,那就是每一层的结点乘上向上移动的层数,最后一起加起来我们便可以得出我们总的移动步数,这有点考察各位读者朋友们的高中数学功底了,因为这是典型的等比数列*等差数列的问题,我记着是在数列求和方法的时候讲到过,此时我们需要用到错位相减法,这个方法各位读者朋友忘记的话可以在网上找一找,应该都是有的,小编就不在这赘述了,下面小编给出总结点数的求解方法:

  最后我们可以计算出向上调整建堆的时间复杂度应该是O(nlogn),之后我们还需要去计算之后排序的时间复杂度,它的算法图如图所示:

  通过计算我们可以计算出,此时最后算出的总移动步数的时间复杂度也应该是O(nlogn),所以最后我们可以得出此时堆排序的算法复杂度应该是O(nlogn),下面我们再来计算一下向下调整建堆的时间复杂度~

 2.向下调整建堆

  首先我们先看一下向下调整建堆的时间复杂度,下面小编同样给出一张照片来帮助各位读者朋友去了解向下调整建堆:

  通过上图我们可以看出,此时结点少的话那么此时的向下移动次数就多,结点多,向下移动次数就少,这样我们去计算每一层的结点移动步数就比较少一点,这样看来此时的时间复杂度要优于向上调整建堆的时间复杂度,不过我们仅凭感觉就得出结论这样的行为是不太好的,下面小编就阿紫计算一下总步数,计算方法同上,此时我们也需要用到错位相减的办法:

  之后我们计算向下排序建堆的时间复杂度是O(n),所以此时的向下调整建堆要优于向上调整建堆,从时间复杂度我们可以完完全全的说出这一句话,之后我们依旧是计算排序的时间复杂度,排序的时间复杂度同上,之后我们得出来版本二的时间复杂度其实均是O(nlogn),只不过其中建堆的时间复杂度有差异,小编更偏向于向下调整建堆。其实这里我们便可以总结出,堆排序的时间复杂度是O(nlogn),版本一其实不算是一个堆排序,这只不过时为了我们版本二的学习来提供一个方向的,各位读者朋友以后写排序还是多写版本二的向下调整建堆法的排序~

2.总结

  此时我们变写完了堆排序,本来堆排序小编准备在讲解一些排序方法的时候在进行讲解,不过小编想了一下,倒不如在讲完堆之后直接讲堆排序,这样的话小编还可以多牵扯到前文的内容,来帮助各位读者朋友更好的了解,所以小编的废话可能会有点多,各位读者朋友多多见谅,如果文章有差错,可以在评论区指出,小编一定会及时的回复,那么,我们下一篇文章见啦~

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值