内部排序实验报告

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

数据结构实验报告
内部排序
班级:13 软工一班
学号:13131113 姓名:吕蒙学号:13131116 姓名:钱剑滨学号:13131142 姓名:孔亚亚学号:13131144 姓名:邱佃雨
13软工转本1 钱剑滨实验报告
内部排序实验报告
信息工程系 13软工转本1 日期 2016年06月07日一、实验内容
编写程序实现各种内部排序(包括:冒泡排序、插入排序、选择排序、希尔排序、快速排序、堆排序、归并排序、基数排序),并运用这些排序对一组随机生成的数组进行排序。

二、时间复杂度
1.冒泡排序(O(n^2))
若文件的初始状态是正序的,一趟扫描即可完成排序。

所需的关键字比较次数和
记录移动次数均达到最小值:,。

所以,冒泡排序最好
的时间复杂度为。

若初始文件是反序的,需要进行趟排序。

每趟排序要进
行次关键字的比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。

在这种情况下,比较和移动次数均达到最大值:
冒泡排序的最坏时间复杂度为。

综上,因此冒泡
排序总的平均时间复杂度为。

2.插入排序(O(n^2))
如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。

最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。

最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。

插入排序的赋值操作是比较操作的次数加上(n-1)次。

平均来说插入排序算法的时间复杂度为O(n^2)。

因而,插入排序不适合对于数据量比较大的排序应用。

但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。

3.选择排序(O(n^2))
选择排序的交换操作介于0 和(n - 1)次之间。

选择排序的比较操作为n (n - 1)/ 2 次之间。

选择排序的赋值操作介于0 和3 (n - 1)次之间。

比较次数O(n^2),比较次数与关键字的初始状态无关,总的比较次数N=(n-1)+(n-2)+...+1=n*(n-1)/2。

交换次数O(n),最好情况是,已经有序,交换0次;最坏情况交换n-1次,逆序交换n/2次。

交换次数比冒泡排序少多了,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。

4.希尔排序
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。

所以,希尔排序的时间复杂度会比o(n^2)好一些。

5.快速排序(O(n^2))
无论适用哪一种方法来选择pivot,由于我们不知道各个元素间的相对大小关系(若知道就已经排好序了),所以我们无法确定pivot的选择对划分造成的影响。

因此对各种pivot选择法而言,最坏情况和最好情况都是相同的。

我们从直觉上可以判断出最坏情况发生在每次划分过程产生的两个区间分别包含
n-1个元素和1个元素的时候(设输入的表有n个元素)。

下面我们暂时认为该猜测正确,在后文我们再详细证明该猜测。

对于有n个元素的表L[p..r],由于函数Partition的计算时间为θ(n),所以快速排序在序坏情况下的复杂性有递归式如下:T(1)=θ(1),T(n)=T(n-1)+T(1)+θ(n)
(1)用迭代法可以解出上式的解为T(n)=θ(n2)。

这个最坏情况运行时间与插入排序是一样的。

下面我们来证明这种每次划分过程产生的两个区间分别包含n-1个元素和1个元素的情况就是最坏情况。

设T(n)是过程Quick_Sort作用于规模为n的输入上的最坏情况的时间,则T(n)=max(T(q)+T(n-q))+θ(n),其中1≤q≤n-1
(2)我们假设对于任何k<n,总有T(k)≤ck,其中c为常数;显然当k=1时是成立的。

将归纳假设代入(2),得到:T(n)≤max(cq2+c(n-q)2)+θ(n)=c*max(q2+(n-q)2)+θ(n)因为在[1,n-1]上q2+(n-q)2关于q递减,所以当q=1时q2+(n-q)2有最大值
n2-2(n-1)。

于是有:T(n)≤cn2-2c(n-1)+θ(n)≤cn2只要c足够大,上面的第二个小于等于号就可以成立。

于是对于所有的n都有T(n)≤cn。

这样,排序算法的最坏情况运行时间为θ(n2),且最坏情况发生在每次划分过程产生的两个区间分别包含n-1个元素和1个元素的时候。

时间复杂度为o(n2)。

6.堆排序(O(nlgn))
堆排序过程的时间复杂度是O(nlgn)。

因为建堆的时间复杂度是O(n)(调用一次);调整堆的时间复杂度是lgn,调用了n-1次,所以堆排序的时间复杂度是O(nlgn)
7.归并排序(O(nlgn))
时间复杂度为O(nlogn) 这是该算法中最好、最坏和平均的时间性能。

空间复杂度为O(n)比较操作的次数介于(nlogn) / 2和nlogn - n + 1。

赋值操作的次数是(2nlogn)。

归并算法的空间复杂度为:0 (n)归并排序比较占用内存,但却是一种效率高且稳定的算法。

8.基数排序(O(d(n+radix)))
时间效率:设待排序列为n个记录,d个关键码,关键码的取值范围为radix,则进行链式基数排序的时间复杂度为O(d(n+radix)),其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(radix),共进行d趟分配和收集。

空间效率:需要2*radix 个指向队列的辅助空间,以及用于静态链表的n个指针。

三、算法稳定性
1.冒泡排序(稳定)
冒泡排序就是把小的元素往前调或者把大的元素往后调。

比较是相邻的两个元素比较,交换也发生在这两个元素之间。

所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。

2.插入排序(稳定)
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。

当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。

比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。

如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。

所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

3.选择排序(不稳定)
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。

那么,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。

比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法。

4.希尔排序(不稳定)
由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。

5.快速排序(不稳定)
快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。

而右边的j下标一直往左走,当a[j] > a[center_index]。

如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。

交换a[j]和a[center_index],完成一趟快速排序。

在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列5 3 3 4 3 8 9 10 11,现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j]交换的时刻。

6.堆排序(不稳定)
堆排序的时间,主要由建立初始堆和反复重建堆这两部分的时间开销构成,它们均是通过调用Heapify实现的。

堆排序的最坏时间复杂度为O(nlgn)。

堆排序的平均性能
较接近于最坏性能。

由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。

堆排序是就地排序,辅助空间为O(1),它是不稳定的排序方法。

7.归并排序(稳定)
归并排序是一种稳定的排序
8.基数排序(稳定)
基数排序是一种稳定的排序
四、各排序介绍和算法实现
1.冒泡排序
冒泡排序算法的运作如下:(从后往前)
(1)比较相邻的元素。

如果第一个比第二个大,就交换他们两个。

(2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。

在这一点,最后的元素应该会是最大的数。

(3)针对所有的元素重复以上的步骤,除了最后一个。

(4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

代码:
void maopao(int numberN[MAX])
{
int temp, i, j, t;
yuan(numberN);
int numberY[MAX];
for(t = 0; t < N; t++)
{
numberY[t] = numberN[t];
}
for(i = 0; i < N - 1; i++)
for(j = 0; j < N - (i + 1); j++)
if(numberY[j] > numberY[j + 1])
{
temp = numberY[j];
numberY[j] = numberY[j + 1];
numberY[j + 1] = temp;
}
sheng(numberY);
}
一般来说,插入排序都采用in-place在数组上实现。

具体算法描述如下:(1)从第一个元素开始,该元素可以认为已经被排序
(2)取出下一个元素,在已经排序的元素序列中从后向前扫描
(3)如果该元素(已排序)大于新元素,将该元素移到下一位置
(4)重复步骤3,直到找到已排序的元素小于或者等于新元素的位置(5)将新元素插入到下一位置中
(6)重复步骤2~5
代码:
void charu(int numberN[MAX])
{
int i,j,t;
int temp;
int numberY[MAX];
yuan(numberN);
for(t = 0; t < N; t++)
{
numberY[t] = numberN[t];
}
for(i=1;i<N;i++)
{
temp=numberY[i];
for(j=i;j>0&&numberY[j-1]>temp;j--)
{
numberY[j]=numberY[j-1];
}
numberY[j]=temp;
}
sheng(numberY);
}
对比数组中前一个元素跟后一个元素的大小,如果后面的元素比前面的元素小则用一个变量k来记住他的位置,接着第二次比较,前面“后一个元素”现变成了“前一个元素”,继续跟他的“后一个元素”进行比较如果后面的元素比他要小则用变量k记住它在数组中的位置(下标),等到循环结束的时候,我们应该找到了最小的那个数的下标了,然后进行判断,如果这个元素的下标不是第一个元素的下标,就让第一个元素跟他交换一下值,这样就找到整个数组中最小的数了。

然后找到数组中第二小的数,让他跟数组中第二个元素交换一下值,以此类推。

代码:
void xuanze(int numberN[MAX])
{
int temp, i, j, t, min;
int numberY[MAX];
yuan(numberN);
for(t = 0; t < N; t++)
{
numberY[t] = numberN[t];
}
for(i = 0; i < N - 1; i++)
{
min = i;
for(j = i + 1; j < N; j++)
{
if(numberY[min] > numberY[j])
{
min = j;
}
}
temp = numberY[i];
numberY[i] = numberY[min];
numberY[min] = temp;
}
sheng(numberY);
}
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。

所有距离为d1的倍数的记录放在同一个组中。

先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。

代码:
void xier(int numberN[MAX])
{
int d, i, j, t, temp;
int numberY[MAX];
yuan(numberN);
for(t = 0; t < N; t++)
{
numberY[t] = numberN[t];
}
for(d = N/2;d >= 1;d = d/2)
{
for(i = d; i < N;i++)
{
temp = numberY[i];
for(j = i - d;(j >= 0) && (numberY[j] > temp);j = j-d)
{
numberY[j + d] = numberY[j];
}
numberY[j + d] = temp;
}
}
sheng(numberY);
}
设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。

值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。

一趟快速排序的算法是:
(1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
(2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
(3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]互换;
(4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
(5)重复第3、4步,直到i=j;(3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。

找到符合条件的值,进行交换的时候i,j指针位置不变。

另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。

代码:
int partions(int numberY[MAX],int low,int high)
{
int prvotkey=numberY[low];
while (low<high)
{
while (low<high&&numberY[high]>=prvotkey)
--high;
numberY[low]=numberY[high];
while (low<high&&numberY[low]<=prvotkey)
++low;
numberY[high]=numberY[low];
}
numberY[low]=prvotkey;
return low;
}
void quicksort(int numberY[MAX],int low,int high)
{
int prvotloc;
if(low<high)
{
prvotloc=partions(numberY,low,high); //将第一次排序的结果作为枢轴
quicksort(numberY,low,prvotloc-1); //递归调用排序由low 到prvotloc-1
quicksort(numberY,prvotloc+1,high); //递归调用排序由prvotloc+1到high
}
}
void kuaisu(int numberN[MAX]) {
int i;
int numberY[MAX];
yuan(numberN);
for(i = 0; i < N; i++)
{
numberY[i] = numberN[i];
}
quicksort(numberY, 0, N-1);
sheng(numberY);
}
6.堆排序
大根堆排序算法的基本操作:
(1)建堆,建堆是不断调整堆的过程,从len/2处开始调整,一直到第一个节点,此处len是堆中元素的个数。

建堆的过程是线性的过程,从len/2到0处一直调用调整堆的过程,相当于o(h1)+o(h2)…+o(hlen/2) 其中h表示节点的深度,len/2表示节点的个数,这是一个求和的过程,结果是线性的O(n)。

(2)调整堆:调整堆在构建堆的过程中会用到,而且在堆排序过程中也会用到。

利用的思想是比较节点i和它的孩子节点left(i),right(i),选出三者最大(或者最小)者,如果最大(小)值不是节点i而是它的一个孩子节点,那边交互节点i和该节点,然后再调用调整堆过程,这是一个递归的过程。

调整堆的过程时间复杂度与堆的深度有关系,是lgn 的操作,因为是沿着深度方向进行调整的。

(3)堆排序:堆排序是利用上面的两个过程来进行的。

首先是根据元素构建堆。

然后将堆的根节点取出(一般是与最后一个节点进行交换),将前面len-1个节点继续进行堆调整的过程,然后再将根节点取出,这样一直到所有节点都取出。

代码:
// array是待调整的堆数组,i是待调整的数组元素的位置,nlength是数组的长度
//本函数功能是:根据数组array构建大根堆
代码:
// array是待调整的堆数组,i是待调整的数组元素的位置,nlength是数组的长度
//本函数功能是:根据数组array构建大根堆
代码:
void HeapAdjust(int array[], int i, int nLength)
{
int nChild;
int nTemp;
for (nTemp = array[i]; 2 * i + 1 < nLength; i = nChild)
{
// 子结点的位置=2*(父结点位置)+ 1
nChild = 2 * i + 1;
// 得到子结点中较大的结点
if ( nChild < nLength-1 && array[nChild + 1] > array[nChild])
++nChild;
// 如果较大的子结点大于父结点那么把较大的子结点往上移动,替换它的父结点
if (nTemp < array[nChild])
{
array[i] = array[nChild];
array[nChild]= nTemp;
}
else
// 否则退出循环
break;
}
}
// 堆排序算法
void dui(int numberN[MAX])
{
int tmp, t, i;
int numberY[MAX];
yuan(numberN);
for(t = 0; t < N; t++)
{
numberY[t] = numberN[t];
}
// 调整序列的前半部分元素,调整完之后第一个元素是序列的最大的元素
//length/2-1是第一个非叶节点,此处"/"为整除
for (i = floor(N -1)/ 2 ; i >= 0; --i)
HeapAdjust(numberY, i, N);
// 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素for (i = N - 1; i > 0; --i)
{
// 把第一个元素和当前的最后一个元素交换,
// 保证当前的最后一个位置的元素都是在现在的这个序列之中最大的
/// Swap(&array[0], &array[i]);
tmp = numberY[i];
numberY[i] = numberY[0];
numberY[0] = tmp;
// 不断缩小调整heap的范围,每一次调整完毕保证第一个元素是当前序列的最大值
HeapAdjust(numberY, 0, i);
}
sheng(numberY);
}
7.归并排序
归并操作的工作原理如下:
(1)申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列(2)设定两个指针,最初位置分别为两个已经排序序列的起始位置
(3)比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
(4)重复步骤(3)直到某一指针超出序列尾
(5)将另一序列剩下的所有元素直接复制到合并序列尾
代码:
//归并操作
void Merge(int sourceArr[], int targetArr[], int startIndex, int midIndex, int endIndex) {
int i, j, k;
for(i = midIndex+1, j = startIndex; startIndex <= midIndex && i <= endIndex; j++) {
if(sourceArr[startIndex] < sourceArr[i])
{
targetArr[j] = sourceArr[startIndex++];
}
else
{
targetArr[j] = sourceArr[i++];
}
}
if(startIndex <= midIndex)
{
for(k = 0; k <= midIndex-startIndex; k++)
{
targetArr[j+k] = sourceArr[startIndex+k];
}
}
if(i <= endIndex)
{
for(k = 0; k <= endIndex- i; k++)
{
targetArr[j+k] = sourceArr[i+k];
}
}
}
//内部使用递归,空间复杂度为n+logn
void MergeSort(int sourceArr[], int targetArr[], int startIndex, int endIndex)
{
int midIndex;
int tempArr[MAX]; //此处大小依需求更改
if(startIndex == endIndex)
{
targetArr[startIndex] = sourceArr[startIndex];
}
else
{
midIndex = (startIndex + endIndex)/2;
MergeSort(sourceArr, tempArr, startIndex, midIndex);
MergeSort(sourceArr, tempArr, midIndex+1, endIndex);
Merge(tempArr, targetArr,startIndex, midIndex, endIndex);
}
}
//调用
void guibing(int numberN[MAX])
{
int numberY[MAX];
yuan(numberN);
MergeSort(numberN, numberY, 0, N-1);
sheng(numberY);
}
8.基数排序
最高位优先(Most Significant Digit first)法,简称MSD法:先按k1排序分组,同一组中记录,关键码k1相等,再对各组按k2排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd对各子组排序后。

再将各组连接起来,便得到一个有序序列。

最低位优先(Least Significant Digit first)法,简称LSD法:先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列。

代码:
void jishu(int numberN[MAX])
{
int temp[10][20]={0}; //第一个10表示0~9,第二个20表示a的size
int order[10]={0};
int i,j,k,t,p,n=1; //k表示当前比较的那一位上的具体数字n=1,10,100,1000...取决于a中的最大的数
int numberY[MAX];
yuan(numberN);
for(t = 0; t < N; t++)
{ numberY[t] = numberN[t]; }
while(n <= 100)
{
for(i=0;i<N;i++)
{
k = (numberY[i]/n) % 10;
temp[k][order[k]] = numberY[i];
order[k]++;
}
p=0; //p为一次排序过程中,a的脚标
for(i=0;i<10;i++)
{
if(order[i] != 0)
{
for(j=0;j<order[i];j++)
{
numberY[p] = temp[i][j];
p++;
}
order[i] = 0;
}
}
n *= 10;
}
sheng(numberY);
}
五、实验测试
初始界面
各排序结果(相同)。

相关文档
最新文档