实验五内部排序.docx

上传人:b****5 文档编号:8027002 上传时间:2023-01-28 格式:DOCX 页数:15 大小:26.15KB
下载 相关 举报
实验五内部排序.docx_第1页
第1页 / 共15页
实验五内部排序.docx_第2页
第2页 / 共15页
实验五内部排序.docx_第3页
第3页 / 共15页
实验五内部排序.docx_第4页
第4页 / 共15页
实验五内部排序.docx_第5页
第5页 / 共15页
点击查看更多>>
下载资源
资源描述

实验五内部排序.docx

《实验五内部排序.docx》由会员分享,可在线阅读,更多相关《实验五内部排序.docx(15页珍藏版)》请在冰豆网上搜索。

实验五内部排序.docx

实验五内部排序

实验五各种内部排序算法的实现及性能比较

5.1背景知识

排序是计算机程序设计中的一种重要操作,它的功能是将一个数据元素(或记录)的任意序列,重新排列成一个按关键字有序的序列。

学习和研究各种排序方法是计算机领域的重要课题之一。

从第6章的搜索(查找)算法中可以看出,顺序表的查找时间复杂度为O(n),而建立在有序表基础上的折半查找的时间复杂度为O(log2n)。

内部排序是指待排序列完全存放在内存中所进行的排序过程,适合不太大的元素序列。

1.排序的定义

参见教科书P18710.1节基本概念。

2.排序方法

内部排序的方法很多,但就其全面性而言,很难提出一种被认为是最好的方法,每一种方法都有其各自的优缺点,适合在不同的环境(如记录的初始排列状态等)下使用。

如果按排序过程中依据的不同原则对内部排序方法进行分类,则排序大致可分为插入排序、交换排序、选择排序、归并排序、基数排序等5类;如果按内部排序过程中所需要的工作量(时间复杂度)来区分,则可分为以下3类:

1)简单的排序方法,时间复杂度为O(n2)的排序方法,例如直接插入、直接选择和冒泡排序;这些排序方法非常容易理解和实现,但是它们的效率并不高,这在数据集比较大时尤其明显。

2)先进的排序方法,时间复杂度为O(nlogn)的排序方法,例如快速排序、堆排序和归并排序。

3)基数排序:

这是一种并非基于比较的排序,它借助多关键字的排序思想,通过“分配”和“收集”的方法来完成排序,时间复杂度为O(n)。

通常(除了上面的基数排序),在排序过程中需要进行下列两种基本操作:

1)比较两个记录关键字的大小

2)将记录从一个位置移动至另一个位置。

前一个操作对大多数排序方法来说是必要的,而后一个操作可以通过改变记录的存储方式来予以避免。

可以从不同的角度描述排序算法的性能,主要有如下几个:

1)排序算法的时间复杂度和空间复杂度

2)排序算法的稳定性

3)排序算法的简单性(代码编写的难易程度)

3.排序表数据结构

排序表是待排序记录的集合。

在本次实验中,待排序的一组记录存放在地址连续的一组存储单元上。

类似于线性表的顺序存储结构。

每个排序算法的实现都采用模板函数的方式,把实验中所有的排序算法都放在SortAlgorithm.h文件中。

5.2实验目的

1)掌握常见的排序方法和实现排序的算法。

2)能根据待排序记录的关键字的初始状态选择合适的排序方法。

3)学会分析算法的基本方法,并计算算法的时间复杂度。

4)学会比较排序算法的性能。

5.3工具/准备工作

在开始实验前,请回顾教科书中有关排序的定义及常见的内部排序算法。

需要一台计算机,其中装有MicrosoftVisualC++6.0开发环境或DevCpp集成开发环境。

5.4实验内容与步骤

5.4.1基础知识

1)三种计算时间为O(n2)的排序方法是:

()、()和()。

2)对于快速排序是使用()算法设计策略开发的一种高效的交换排序。

3)()排序方法不是基于比较的排序方法。

4)目前以比较为基础的内部排序的时间复杂度T(n)的范围是(      ),其比较次数与待排序的记录的初始状态无关的是(     )。

5)对于部分有序列表,简单选择排序算法的性能比对于随机列表的性能高,这句话是否正确?

6)对于部分有序列表,直接插入排序和冒泡排序算法的性能比对于随机列表的性能高,这句话是否正确?

7)如果排序处理的是对象列表,这些对象表示包含着多个不同类型的信息项的记录,例如学生对象。

这时,排序是基于这些记录中的某个关键域进行的。

在排序过程中如果移动整个数据对象,所需要的时间将长得令人无法忍受。

对于这些由大记录或对象组成的列表,可以使用间接排序,即使用索引表来存放对象的位置,并移动对象的索引(即下标或位置)而非对象本身。

8)数组(顺序存储结构)能够用来有效地存储堆,因为堆是一种()二叉树。

9)C++标准库中的sort()函数模板使用了()排序。

10)根据数据项存放在内存还是外存,排序算法能够分为()和()两种。

5.4.2各种排序算法的实现

1、选择排序

选择排序的基本思想是对列表或者列表的一部分进行多次扫描,每次选出一个元素将其放到正确位置。

1)简单选择排序

每次选择总是从未排序序列中选出最小元素并把其放在未排序序列的起始位置上。

每选择一次会使未排序序列元素个数减一,已排序序列元素个数增一。

共需要n-1趟的选择。

初始时,未排序序列元素个数为n。

简单选择排序算法描述请参考教科书P188程序10.1。

该算法的最好、最坏和平均情况的时间复杂度为O(n2)。

且为不稳定的排序方法。

2)堆排序

教科书5.6节介绍了堆,它是实现优先权队列的有效的数据结构。

堆还可用于排序,这就是堆排序。

可以直接使用优先权队列实现排序,先将序列中的每个元素入优先权队列,之后再依次出队列,得到的结果即是有序序列。

(教科书上的优先权队列PrioQueue是小顶堆,STL的适配器类priority_queue是大顶堆。

同学们可以自己编程序进行测试。

有关STL中的栈和队列的相关内容请参考:

堆排序的基本思想如下:

第一步:

为了得到非递减的排序序列,首先构造一个大顶堆。

第二步:

for(i=n-1;i>0;i--)//每循环一次进行一个根叶交换

1)交换a[i]和a[0]

2)使用向下调整算法,把a[0]~a[i-1]重新调整成大顶堆

具体的算法实现请参考教科书P196的程序10.7和程序10.8。

该算法的最好、最坏和平均情况的时间复杂度为O(nlog2n)。

且为不稳定的排序方法。

3)树排序

我们可以利用二叉查找树完成排序,把这种排序方法称为树排序。

只需要把列表元素插入到初始为空的二叉查找树BSTree中,然后使用中序遍历来把元素复制到列表中。

算法描述如下:

template

voidTreeSort(TA[],intn)

{

BSTreetree;//构造一棵空树

for(inti=0;i

tree.Insert(A[i]);

inti=0;

tree.InOrder(int&i)//对中序遍历进行适当修改,

//对某个结点访问也就是把结点的值赋给A[i]后再i++

}

同学们可以自己编程序进行测试。

2、插入排序

1)直接插入排序

直接插入排序的思想非常简单,将序列中第一个元素作为一个有序序列,然后将剩下的n-1个元素按关键字大小依次插入该有序序列,每插入一个元素后依然保持该序列有序,经过n-1趟排序后即成为有序序列。

直接插入排序算法请参考P189的程序10.2。

直接插入排序算法的最坏情况出现在当列表按相反方向排列的时候。

时间复杂为O(n2)。

最好情况为序列已经有序,时间复杂度为O(n)。

该排序算法是稳定的排序方法。

对于短列表(包含20个左右的元素)和部分有序列表,在我们讨论的所有排序算法中,实验结果表明直接插入排序的性能最佳。

2)折半插入排序

如果我们使用折半查找而不是顺序查找来在排好序的子列表a[0],a[1],…,a[i-1]中找出下一项a[i]应该插入的位置。

那么该算法称为折半插入排序。

请编写折半插入排序算法并编程实现:

template

voidBInsertSort(intA[],intn)

{

 

}

3)Shell排序

对于短列表和部分有序列表,它使用插入排序的性能较好。

希尔排序(取名于DonaldShell)是一种插入排序,它使用直接插入排序对短列表排序,并产生更长的部分有序表。

具体地,它使用直接插入排序来对相互之间“间隔”为d的元素进行排序,首先处理a[0],a[0+d],a[0+2d],…,然后处理a[1],a[1+d],a[1+2d],…,然后处理a[2],a[2+d],a[2+2d],…,以此类推。

然后减少间隔d的值重复以上处理过程,直到间隔d变成1为止,此时执行直接插入排序之后列表就被排好了。

请编写Shell排序算法并编程实现,其中d的初值为

,且每回d的值减3,这里k是某个整数。

template

voidShellSort(intA[],intn)

{

 

}

3、交换排序

交换排序的基本思想是系统化地交换那些不符合次序的元素对,直到列表中不存在这种元素对为止,此时列表就排好了。

冒泡排序和快速排序是典型的交换排序。

1)冒泡排序

冒泡排序通过交换相邻的两个元素来实现排序。

算法请参考教科书P191的程序10.3。

注意程序中last的使用,它记录每趟过程中最后一次交换元素的位置,如果一趟起泡过程中,last仍为0,则排序算法结束。

冒泡排序最好情况(已有序)下只需要进行一趟排序,(n-1)次比较,因此最好情况下时间复杂度为O(n);最坏情况(倒序有序),第i趟比较(n-i)次,因此最坏情况下时间复杂度为O(n2)。

冒泡排序是稳定的排序方法。

2)双向冒泡排序

双向冒泡排序是冒泡排序的一个变种,它交替地从左到右和从右到左扫描未排好序列的子列表。

在从左到右扫描的时候,如果ai>ai+1,则交换ai和ai+1,使得较大的元素移向子列表的右端。

在从右到左地扫描的时候,如果ai

扫描进行到没有元素被交换为止。

编写一个算法来实现双向冒泡排序,算法写在下面,并实现该算法。

可在程序10.3的基础上修改。

template

voidBiBubbleSort(intA[],intn)//双向冒泡排序

{

 

}

3)快速排序

在冒泡排序中,每轮都比较并在需要时交换相邻的元素,这意味着有可能需要很多的交换才能把一个元素移到其正确的位置。

由C.A.R.Hoare提出并由RobertSedgewick作了明显改进的一种交换算法,就是快速排序算法,比冒泡排序的效率高很多,原因在于进行交换的元素相隔很远,使得我们需要较少的交换次数来把一个元素移到其正确位置。

快速排序采用分治策略(divide-and-conquer)。

在快速排序中,分治策略选取某个称为基准(pivot)的元素,然后进行一系列交换,使得小于或等于这个基准的所有元素都放在基准的前面,而所有大于基准的元素都放在其后边。

这就正确地定好了基准的位置,且把(子)列表分成两个更小的子列表,每个子列表都可以使用同样的方法来进行排序。

反复运用这个方法,最终得到易于排序的短列表。

容易写出快速排序算法的递归版本,算法的描述和实现请参考教科书P192的程序10.4。

我们也可以写出快速排序的非递归算法。

template

voidIterQuickSort(intA[],intn)//非递归的快速排序算法

{

 

}

快速排序的最坏情况发生在列表已经排序或者元素的排列符合反向次序的时候,时间复杂度为O(n2)。

而其平均情况下的时间复杂度为O(nlog2n)。

4)对快速排序的改进

●基准的选择

一种选择基准的通用方法是采用三数取中规则,即选取子列表的第一个、中间一个和最后一个元素的中间一个来作为基准。

事实上,许多列表都是部分有序的,此时三数取中规则很有可能选出一个与子列表的中位数相近的基准数字,而不再是“首元素”规则。

●短子列表

对于短列表(n≤20),快速排序的性能低于插入排序的性能;而递归过程中许多短列表会出现。

当元素个数n≤20时用直接插入排序,作为快速排序递归版本算法的递归出口。

在C++标准库中的sort()函数模板使用了快速排序。

大部分实现都使用了这里提到的改进方法来提高快速排序算法的性能。

请写出你自己的经过改进的快速排序递归版本的算法。

(实验必做内容)

template

voidQuickSort(intA[],intn)//非递归的快速排序算法

{

QSort(A,0,n-1);

}

template

voidQSort(TA[],intleft,intright)

{

 

}

4、归并排序

根据数据项存放在内在还是外存,排序算法分为内部排序和外部排序。

前面介绍的排序方案都基本上只用作内部排序。

它们不适用于对顺序文件的处理。

而归并排序既可用作内部排序,也可以用作外部排序。

归并排序的基本操作是归并,即把两个已经有序的列表合并成为一个新的有序列表。

将两个有序表合并成一个有序表的算法如教科书P194的程序10.5所示。

同学们可以自己研究一下,这个程序我们在上学期的课程中已经学习过。

1)折半归并排序(两路归并排序)

排序思想请阅读教科书P194。

两路归并的排序非递归版本如教科书P194的程序10-6。

也可以基于分治策略给出归并排序的递归版本的程序,程序代码如下:

template

voidMergeSort(TA[],intn)

{

MSort(A,0,n-1);

}

template

voidMSort(TA[],intlow,inthigh)

{

if(high>low){

intmid=(high+low)/2;//把序列分成两个子序列(分)

MSort(A,low,mid);//第一个子序列排序(治)

MSort(A,mid+1,high);//第二个子序列排序(治)

Merge(A,low,mid,mid+1,high);//合并两个有序表为一个有序表

}

}

请比较递归版本的归并排序和非递归版本的归并排序。

两路归并排序的时间复杂度为O(nlog2n),空间复杂度为O(n),该排序是稳定排序。

2)自然归并排序

折半归并排序要求有序子列表的长度为1,2,4,8,…,2k,其中2k≥n,因此总是经历k个拆分-归并阶段。

如果允许使用其他长度的子列表,则阶段数有可能减少。

利用了这些“自然的”有序子列表(对比于折半归并排序创建的“人工”长度)的归并排序版本被称为自然归并。

考虑如下待排序列表:

75551520853035106040502545807065100

对这个序列以一种自然的方法拆分,拆分成的有序子列表如下:

75551520853035106040502545807065100

第一趟归并:

55751520303585104050602545708065100

第二趟归并:

152030355575851025404550608065100

第三趟归并:

10152025354550556075808565100

第四趟归并:

10152025354550556065758085100

17个元素,4趟归并,如果采用2路归并,则需要5趟归并。

如果待排序序列本身已经有序,只需要一趟扫描该有序表,即可完成排序。

自然归并排序算法如下:

template

voidNaturalMergeSort(TA[],intn)

{

inti,i1,i2,j1,j2;

intSubCount=2;//有序序列的个数,设一个大小1的数,目的是while循环至少执行一次

while(SubCount>1)//如果一次扫描数组A后SubCount=1,说明已经有序,算法结束

{

i=0;

SubCount=0;//每趟归并前初始化为0

while(i

j1=i1=i;SubCount++;

while(i=A[j1]){i++;j1++;}

if(i

while(i=A[j2]){i++;j2++;}

if(SubCount%2==0)Merge(A,i1,j1,i2,j2);

i++;

}

}

}

该算法最好情况下的时间复杂度为O(n),平均和最坏情况下的时间复杂度为O(nlog2n),为稳定排序。

5、基数排序

到目前为止,讨论的排序方法都只有一个关键字,且都是基于关键字的比较和移动两种操作实现的。

而基数排序则不同与之前介绍的排序算法,它是基于多关键字的思想的。

最好的例子就是教科书中关于一副扑克牌的排序。

有几种类型的基数排序,但是它们的共同之处在于都是基于记录的关键字的某种进制下的各位上的数字来进行排序的。

算法思想如下:

初始序列为:

390058472343978420392106058674

第一趟分配:

0

1

2

3

4

5

6

7

8

9

390

420

472

392

343

674

106

058

978

058

第一趟收集:

390420472392343674106058978058

第二趟分配:

0

1

2

3

4

5

6

7

8

9

106

420

343

058

058

472

674

978

390

392

第二趟收集:

106420343058058472674978390392

第三趟分配:

0

1

2

3

4

5

6

7

8

9

058

058

106

343

390

392

420

472

674

978

第三趟收集:

058058106343390392420472674978

具体的算法描述如下:

template

voidRadixSort(TA[],intn,intd)

{

int*B=newint[n];

int*count=newint[10];//计算第i位值为j的元素个数

inti=0,k,r=10;

while(i

{//第一趟A→B,第二趟B→A

for(intj=0;j<10;j++)count[j]=0;

for(intj=0;j

{

k=(A[j]%r)/(r/10);//分离出第i位

count[k]++;

}

count[0]--;//因为数组元素的起始下标为0

for(intj=1;j<10;j++)count[j]=count[j-1]+count[j];

//计算出每趟“收集”后元素存放的位置,思想与快速转置思想相同

for(intj=n-1;j>=0;j--)//从后向前扫描元素,完成一趟过程的收集工作

{

k=(A[j]%r)/(r/10);

B[count[k]]=A[j];

count[k]--;

}

r=r*10;i++;

if(i

{

for(intj=0;j<10;j++)count[j]=0;

for(intj=0;j

{

k=(B[j]%r)/(r/10);

count[k]++;

}

count[0]--;

for(intj=1;j<10;j++)count[j]=count[j-1]+count[j];

for(intj=n-1;j>=0;j--)

{

k=(B[j]%r)/(r/10);

A[count[k]]=B[j];

count[k]--;

}

r=r*10;i++;

}

}

if(d%2==1)for(intj=0;j

}

针对上面的例子:

第一趟:

A数组:

0

1

2

3

4

5

6

7

8

9

390

058

472

343

978

420

392

106

058

674

count数组:

0

1

2

3

4

5

6

7

8

9

1

1

3

4

5

5

6

6

9

9

B数组:

0

1

2

3

4

5

6

7

8

9

390

420

472

392

343

674

106

058

978

058

第二趟:

0

1

2

3

4

5

6

7

8

9

0

0

1

1

2

4

4

7

7

9

A数组:

0

1

2

3

4

5

6

7

8

9

106

420

343

058

058

472

674

978

390

392

第三趟:

count数组:

0

1

2

3

4

5

6

7

8

9

1

2

2

5

7

7

8

8

8

9

B数组:

0

1

2

3

4

5

6

7

8

9

058

058

106

343

390

392

420

472

674

978

基数排序算法的时间复杂度为O(d*n),是线性时间,排序效率较高,该算法是稳定的排序方法。

5.4.3编程测试各种排序算法

要求:

1)使用随机数发生器产生大数据集合,运行上述各排序算法,使用系统时钟测量各算法所需要的时间,并进行比较。

2)请研究下面的程序代码,理解随机数的产生和时间函数的使用。

#include

#include

#include

//有关时间函数的内容请参考

#include"SortAlgorithm.h"//实验的各个排序算法的实现都放入此头文件中

#defineMAX_SIZE30000//排序列表规模

usingnamespacestd;

intmain(intargc,char*argv[])

{

clock_tstart,finish;

doubleduration;

inta[MAX_SIZE];

srand((unsigned)time(NULL));//随机数种子,这样每次运行序列内容不同

for(inti=0;i

a[i]=rand();

star

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 总结汇报 > 学习总结

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1