今天为大家带来的是数据结构中的七大排序算法的其中两种:希尔排序和堆排序。本次的代码实现还是使用的C语言,编译器还是vs2013版本,希望接下来的内容可以帮助大家理解这两种排序。
希尔排序
首先,对于一组待排数据,无论是什么方法,最重要的就是排序的速度,占用空间和准确性。所以,前人一直在寻找可以提升排序速度的算法。现在,我就为大家带来排序算法里程碑的算法:希尔排序算法。
对于我们之前提到的直接插入算法,如果一个待排数据基本有序或者操作数较少时,直接插入法的优势就显得非常突出。而对于我们一般的数据,基本都是大量的,无序的,这时就有人会想到,我们可以先把这些数据拆分成很多小的数据组,在使用直接插入算法,那么会不会大量节约时间呢??
很明显,是可以的,但也不完全正确,我来给大家举一个例子。比如我们现在有序列{},我们将这组待排数据分成三组:{},{},{7,4,6},即使我们将这些数组各自排行好了顺序,变成{},{},{},再合并数组,此时的数据也并不是基本有序的数据。那么这样就毫无作用吗??
很明显,出现这样的问题就在于我们分割待排序数据的目的是为了减少待排序数据的数目,并使整个数据呈现基本有序的序列。如今达不到我们的预期想法,因此我们需要采取跳跃分割的办法:将相距某个增量的数据记录成一组数据,再根据关键字去调整这组数据的排序。这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而且是局部有序的。这也就是希尔排序的基本思想。
希尔排序:就是把序列按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量的逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个序列恰好被分为一组,算法便终止。
接下来我将使用画图的方式为大家讲解希尔排序的具体步骤,这里我们假设将待排序列排为增序列
接下来在进行第二步,改变增量的值,重复上述步骤
最后在令的值变为1,交换相邻的元素,得到最终结果
根据上图的表述,我们可以知道希尔排序算法的思想,现在我们来实现下代码:
void ShellSort(Sqlist *l)//对顺序表l进行增排序
{
int tmp=l.length;
while(tmp>1)
{
tmp=tmp/3+1;//tmp初始值
for(int i=tmp+1;i<l.length;i++)
{
if(l.val[i]<l.val[i-tmp])
{
l.val[0]=l.val[i];//0号下标元素开始为空,以便交换数据
for(int j=i-tmp;j>0&&l.val[0]<l.val[j];j-=tmp)
{//判断一个组后续元素
l.val[j+tmp]=l.val[j];//交换数据
}
l.val[j+tmp]=l.val[0];//将关键字插入
}
}
}
}
从上面的代码看,还是很难理解的,所以我们先给出了图片辅助代码理解;希望大家可以根据代码写一遍思路,以便我们牢记该方法的使用。
现在,我们来分析一下希尔排序的复杂度。首先是空间复杂度,我们需要使用一个辅助空间,存储每组待排序组的关键词,所以空间复杂程度为。接着我们来看空间复杂程度,对于希尔排序,最重要的就是增量的选取,目前我们还没有一个很好的解决方法求取最合适的增量。不过,根据大量数据研究,当序列的增量为时,可以获取最佳效果,时间复杂度为,要好于直接排序的时间复杂度。但需要注意的是:增量序列的最后一个增量值必须等于1才行。此外,希尔排序还是一个不稳定的排序算法,须谨慎使用。
堆排序
对于希尔排序,我们需要掌握这种算法思想,将之合理运用到以后的排序中。接下来我们要讲的排序可以说是简单选择排序的一种优化,并且这种优化的效果十分明显。
在介绍堆排序之前,我们需要了解什么是堆,堆的定义是什么??
我们先看上面两幅图,左边为大堆,右边为小堆;仔细观察,我们就会发现,左图中每个父节点都大于子节点,右边每个父节点都小于子节点。这就是我们要讲的堆结构。
堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子节点的值,称为大顶堆;或者每个节点的值都小于或等于其左右孩子节点的值,称为小顶堆。
所以,这里我们根据堆的定义。如果按照层序便利的方式给节点从1开始编号,则节点之间满足如下关系:&&或者&&其中,这里的范围是由于二叉树的性质5所得到的。
现在我们来正式介绍堆排序算法的定义。
堆排序就是利用堆(假设为大堆)进行排序的算法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将它一走,然后对剩余的n-1个序列重新构造一个堆,这样就会得到n个元素的次小值。如此反复操作,便能得到一个有序序列。
思路其实很好理解,这里我们直接看代码,在代码中解析每一步操作。这里我们将待排序数组构建成递增顺序,首先是堆排序的整体框架
void HeapSort(Sqlist *l)
{
for(int i=0;i<l.length;i++)
{
HeapAdjust(l,i,l.length);//将l中的无序元素构建成大堆
}
for(int i=l.length;i>1;i--)
{
swap(l,1,i);//将堆顶元素和待排序元素的最后一个待排序位交换
HeapAdjust(l,1,i-1);//再次恢复到大堆
}
}
上面的代码都很好理解,主要就在于关键的函数内部的实现,接下来我们来细说这个函数的作用,我们还是先以画图的形式便于大家理解。
以上就是构建一次大堆的过程,之后的步骤都和以上步骤相仿,现在我们来编写相关代码。
void HeapAdjust(Sqlist *l,int m,int n)
{
int tmp=l.val[m];//保存我们标记节点的大小
for(int i=2*m;i<=n;i*=2)//标记子节点
{
if(i<m&&l.val[i]<l.val[i+1];//找到最大子节点
{
++i;
}
if(tmp>=l.val[i])//最大子节点和父节点相比较
{
break;
}
l.val[m]=l.val[i];//赋值,最大子节点
m=i;//更新标记
}
l.val[m]=tmp;
}
以上代码就是我们上图的代码呈现,其实还是蛮简单的。如果还是不明白,可以结合我们画的图来一步步操作,加深理解。
现在,我们来分析一下堆排序的复杂度。先来看空间复杂程度,由于每次对堆操作都会使用1个临时空间,所以空间复杂度为。堆排序的主要时间消耗就是在初始构建堆和反复重建大堆上。在构建过程中,由于我们是从完全二叉树的最下层的最右端开始构建的,将他与其孩子节点进行交换。对于每个非终端节点,其实最多可以交换两次,所以构建堆的时间复杂程度就是。
而在正式排序中,第i次取堆顶节点重建堆需要用到,总共需要取次,所以重建堆的时间复杂度为。总体上看,堆排序的时间复杂程度为。
标签:tmp,排序,val,七大,我们,算法,数据结构,节点 From: https://blog.51cto.com/u_15209404/5815063