编程实现希尔、快速、堆排序、归并排序算法。要求首先随机产生10000个数据存入磁盘文件,然后读入数据文件,分别采用不同的排序方法进行排序并将结果存入文件中。
编程实现希尔、快速、堆排序、归并排序算法。要求首先随机产生10000个数据存入磁盘文件,然后读入数据文件,分别采用不同的排序方法进行排序并将结果存入文件中。
一、算法思想描述(用一个长度为10的序列进行模拟)
1.希尔排序
希尔排序是对直接插入排序的改进,它利用了直接插入排序在序列个数少且基本有序的情况下排序效率较高的性质。
首先获取整数d,将序列分出d个长度为d的子序列,选取中间(或中间靠后)的位置作为被插入位置,并用temp存储该位置的数据。从第一个序列的第一个数据开始与temp比较,若比temp大则将该数据插入到temp的位置,实际上就是每个小组内的排序,而这个比较序列里相邻数据的位置增量为d,即每次从不同序列中选取序列中相同位置的数据与temp比较。所有序列的第一个位置的数据比较完则完成了一个for循环,将初始temp的位置向后挪一个得到新的temp,再进行下一轮比较,当前增量的比较完成后将d折半再进行比较。
图1.希尔排序示意流程
2.快速排序
快速排序是冒泡排序的改进,同样是一次比较两个数据的大小,但增大了冒泡排序中每次比较后数据的移动位置。
首先选取第一个数据为基准,将数据存入temp中,该位置即为当前被插入位置。同时需要两个变量i和j分别指向序列的最左边和最右边。然后先用j从右往左遍历寻找比基准值小的数据,找到了就插入当前的被插入位置,并将找到的数作为下一个基准值,并反过来用i从左往右遍历比基准值大的数据。如此左右循环,循环的判断条件是i < j,需要注意的是在执行每一步操作的时候都要判断是否i < j。
这样执行完一次循环就完成了一次划分,循环最后i和j的落点是一次划分的枢纽,枢纽左边的值均比它小,右边的值均比它大。然后对左右两边递归划分即可得到有序序列。
图2.图3.快速排序示意流程
3.堆排序
堆排序是利用完全二叉树的性质的排序方法。在逻辑上将序列看成一棵树,由于需要利用结点坐标之间的乘除特点,故不用序列的零位置。
首先在逻辑上将序列构造成一个堆,然后通过层层筛选的方法将其改造为一个大顶堆,接着每次将大顶堆的最大值(即顶点)与序列的最后一个交换,这样最大值就找到了自己的位置,而交换后的堆又需要重新筛成大顶堆。不断重复筛选过程即可得到有序序列。
筛选是从最后一个分支结点开始,通过比较筛选确定该位置的结点数值比它的左右孩子都要大,然后从后往前筛选每一个分支结点,最终即可得到大顶堆。
图4.堆排序示意图
4. 归并排序
归并排序的主要思想是将若干个有序序列逐步归并得到一个有序序列。为了方便这里通常也不考虑有序序列的零位置。
从序列的第一个元素往后开始构造子序列,首先将子序列的长度h设置为1,一次构造两个子序列进行归并,一趟结束后即得到一条新的序列,将归并后的新的子序列再次依次归并到,直到子序列的长度等于序列长度。
在每一趟的归并中,需要考虑当前记录中子序列的情况。如果至少有两个长度为h的子序列即可循环归并;如果只有一条长为h的子序列而另一条不足h,即按照相应的信息进行最后一次归并;如果只剩下一条序列,则直接纳入新的序列。
每次归并即将两个序列从头开始一次比较,将较小者先填入新的序列。
图5.归并排序示意流程
二、程序结构
首先随机产生数据,写入磁盘文件,再读取文件,然后用switch结构选择不同的排序方法对数据进行排序,在每一个排序的算法的前后设置好时间变量以显示算法运行时间,一次排序后将结果存入相应的文件(这里设置了五个磁盘文件分别存储原文件信息和四种不同算法的排序结果),由于此时的序列已经是有序的,需要重新读取原文件进行下一次选择。
图6.程序结构图
三、实验过程
1. 设置不同的数据总量1 0000、5 0000、10 0000、100 0000、1000 0000,多次测试并记录不同排序的用时;
2. 用每种算法分别读取有序与无序的数据序列,记录排序用时;
3. 选择合适的实验数据绘制为图表并进行分析。
四、测试结果
1.测试结果
长度为10000的无序序列数据排序的运行结果:
图7.运行结果
图表记录:
表1.排序用时记录
数据总量 | 是否有序 | 希尔排序 | 快速排序 | 堆排序 | 归并排序 |
1 0000 | 无序 | 0.003s | 0.002s | 0.003s | 0.002s |
有序 | 0.001s | 0.134s | 0.002s | 0.001s | |
50000 | 无序 | 0.017s | 0.009s | 0.015s | 0.01s |
有序 | 0.005s | 3.325s | 0.012s | 0.006s | |
10 0000 | 无序 | 0.038s | 0.019s | 0.033s | 0.022s |
有序 | 0.011s | 13.427s | 0.025s | 0.013s | |
100 0000 | 无序 | 0.48s | 0.244s | 0.441s | 0.225s |
有序 | 0.125s | 1360.07s | 0.296s | 0.122s | |
1000 0000 | 无序 | 6.651s | 5.502s | 6.155s | 2.504s |
有序 | \ | \ | \ | \ |
2.数据分析
图8
图9
图10
由于测试数据较少,从以上图表以及多次实验(图表只绘制了一次)中可以初步看出几个现象:
快速排序在有序的情况下时间复杂度最差;
数据总量很大时,归并排序时间复杂度最好;
在无序情况下,快速排序和归并排序时间复杂度较好;
在有序情况下,归并排序和希尔排序时间复杂度较好。
五、问题与讨论
1.实验中的问题与讨论
(1) 栈溢出问题
在测试过程中最突出的问题就是在开始测试10000个随机数排序时发现快速排序单独运行一次后再次运行任何排序程序都会崩溃,调试结果显示快速排序只运行到大约4400左右的数据,查阅相关资料后了解到是产生了栈溢出(Stack OverFlow),由于快速排序运用了递归算法(这里的归并排序我选用了非递归算法),需要栈来存放每一层递归调用的信息,其最大容量应与递归调用的深度一致,而当数据量越大,快排的递归深度也逐渐加深,故产生了栈溢出,可以在项目属性中找到堆栈保留大小并进行调整,堆栈的默认大小一般为1 M = 1048576 B,默认的单位为B。
(2) 不同排序函数的参数比较
不同排序函数的参数不同需要注意。x是相同的参数,为待排序列:
void ShellSort(intx[], intn);//希尔排序
参数n为元素个数
void QuickSort(intx[], inti, intj);//快速排序
参数i和j分别为元素的是始终序号
void HeapSort(intx[], intn);//堆排序
参数为出去首位元素(序列零位置的元素)的元素个数
void MergeSort1(intx[], intx1[], intn);//归并排序(非递归算法)
参数n为除去首位的元素个数,x1是一个辅助数组
观察以上算法中的整形参数,我们可以发现,希尔排序和快速排序均可以从0位置的元素开始排序,而堆排序和归并排序通常从1位置开始排序。这是因为堆排按照完全二叉树的原理需要通过元素下标的乘除变换来进行查找遍历,所以需要除去特殊数字0,而归并排序中依次比较一定下标增量的元素,为了方便变换需要将第一位元素的下标设置为1。
在测试中为了统一元素个数,序列元素均从下标1开始计数。
此外,还可以发现归并排序中用到了一个辅助数组。该算法在排序时将自身分为多个子序列,归并到为一个新的序列,故需要两个序列空间来存储。而在具体算法中,此处也是一个需要注意的地方,为了保证最后的序列存储在原序列中,在依次循环中需要先将x归并到x1,再将x1归并到x,两次操作相同,数组参数位置相反。
2.算法性能分析
表2.算法性能分析
| 希尔排序 | 快速排序 | 堆排序 | 归并排序 |
时间复杂度 | 希尔排序算法是它所取增量的函数,时间性能的分析比较复杂 | 对于无序序列快速排序时间性能较好;而对于有序序列,快速排序是最坏的排序情况 | 堆排序的运行时间主要消耗在初始建堆和重建堆时的反复筛选上。堆排序的时间复杂性比较稳定,对原始数据的排序状态不敏感。 | 归并排序对原始数据的排序状态不敏感,在数据总量较大时最能体现出它的时间性能优势 |
空间复杂度 | 只需要一个暂存被插入记录位置的辅助空间 | 由于快速排序时递归的,需要一个栈来存放每一层递归调用的必要信息 | 需要一个用于交换的暂存单元 | 归并排序中又新序列和原序列,故需要两个序列空间 |
稳定性 | 由于这里的增量不同于插入排序的1,需要跳过很多元素进行插入操作,所以希尔排序是不稳定的排序方法。 | 快速排序跳过了中间的数据直接从首位开始交换,所以是不稳定的排序方法 | 由于在逻辑上堆排序把序列看成是树的构造,不考虑序列的顺序,故堆排序是不稳定的排序方法。 | 无论是一次归并还是一趟归并都是依次进行的,故归并排序是一种稳定的排序方法。 |