算法设计与分析实验报告——排序
一、实验步骤和要求
1. 算法和代码 的设计与 实现分别设计并实现插入排序、合并排序、快速排序的算法;
2. 测试: 设计 测试数据集, 编写测试程序, 用于测试
a) 正确性: 所实现的 三种 算法的正确性;
b) 算法复杂性: 三种排序算法中,设计测试数据集,评价各个算法在算法
复杂性上的表现;(最好情况、最差情况、平均情况)
c) 效率: 在三种排序算法中,设计测试数据集,评价各个算法中比较的频
率,腾挪的频率。
3. 撰写 评价 报告
a) 结合第二步的测试和实验 结果,在理论 上给予总结和评价三种排序算法
在算法复杂性和效率上的表现。形成电子版实验报告。
二、实验源代码
1、插入排序
void insert_sort(int arr[])
{
int i,j,temp;
for(i=2;i<=n;i++)
{
temp=arr[i];
moveNum++; //首先把标杆元素挪动到一个交换空间,挪动一次
j=i-1;
while( j>=0 && temp<arr[j] )
{
moveNum++;
cmpNum+=2;
arr[j+1] = arr[j];
j--;
}
cmpNum++; //最后跳出循环的比较
arr[j+1]=temp;
moveNum++;
}
}
2、归并排序——递归
void Merge(int arr[],int l,int m,int r) //合并操作
{
int i,j,k;
i=l,j=m+1,k=0;
while(i<=m&&j<=r) //归并的过程
{
cmpNum+=2; //进来就完成一次比较
if(arr[i]<=arr[j])
A[k++]=arr[i++];
else
A[k++]=arr[j++];
cmpNum++;
}
cmpNum++;
if(i<=m)
{
while(i<=m)
{
A[k++]=arr[i++];
cmpNum++;
}
}
else{
while(j<=r)
{
A[k++]=arr[j++];
cmpNum++;
}
}
cmpNum++;
moveNum += r-l+1; //挪腾次数就是需要合并的大小
for(i=l,k=0;i<=r;i++,k++)
arr[i]=A[k];
cmpNum += r-l+1;
moveNum += r-l+1; //挪腾次数就是需要合并的大小
}
void mergeSort(int arr[],int l,int r) //递归合并排序
{
if(l<r)
{
int m=(l+r)/2;
mergeSort(arr,l,m);
mergeSort(arr,m+1,r);
Merge(arr,l,m,r);
}
cmpNum++;
}
3、归并排序——非递归
void Merge(int c[],int d[],int l,int m,int r)
{
int i=l,j=m+1,k=l;
while( i<=m && j<=r )
{
cmpNum += 2;
if(c[i]<=c[j])
d[k++]=c[i++];
else
d[k++]=c[j++];
cmpNum++;
}
cmpNum++;
if(i>m){
for(int q=j;q<=r;q++)
d[k++]=c[q];
}
else{
for(int q=i;q<=m;q++)
d[k++]=c[q];
}
cmpNum++;
moveNum += r-l+1;
}
void MergePass(int x[],int y[],int s,int r) //合并大小为s的相邻子数组
{
int i=1;
while(i+2*s-1<=r)
{
cmpNum++;
Merge(x,y,i,i+s-1,i+2*s-1);
i=i+2*s;
}
cmpNum++;
if(i+s-1<r) //剩余的超过一半
Merge(x,y,i,i+s-1,r);
else{
for(int j=i;j<=r;j++)
y[j]=x[j];
cmpNum += r-i;
moveNum += r-i;
}
cmpNum++;
}
void mergeSort_n(int arr[],int r) //非递归的归并排序
{
int s=1;
while(s<r){
cmpNum++;
MergePass(arr,b,s,r);
s += s;
MergePass(b,arr,s,r);
s += s;
}
cmpNum++;
}
4、快速排序
int Partition(int arr[],int left,int right)
{
int pivot = arr[left];
moveNum++;
while(left<right) //只要left与right不相遇
{
cmpNum++;
while(left<right&&arr[right]>pivot)
{
cmpNum+=2; //每次进行了两个比较
right--;
}
arr[left]=arr[right];
moveNum++;
while(left<right&&arr[left]<=pivot)
{
cmpNum+=2;
left++;
}
arr[right]=arr[left];
moveNum++;
}
cmpNum++;
arr[left]=pivot;
moveNum++;
return left;
}
void quickSort(int arr[],int l,int r)
{
if(l<r)
{
int i = Partition(arr,l,r);
quickSort(arr,l,i-1);
quickSort(arr,i+1,r);
}
cmpNum++;
}
三、算法正确性确定
使用函数 isCorrect(),判断排序后的数组是否是非降序来确定算法的正确性。
void isCorrect()
{
int i=0;
for(i=2;i<=n;i++)
if(medium[i]<medium[i-1])
{
printf("error\n");
return;
}
printf("correct\n");
}
经验证,算法正确性可以保证。
以上是简单的方法,我觉得不是很够严谨,有可能在排序的过程中存在一些丢失,因此有以下第二种方法。
使用c++提供的库函数sort,对初始的数列进行排序,获得一个标准的模板数组,然后每次将调用的结果和这个数组对比,可以确保算法正确性,代码如下,时间复杂度上仅比上式多了复制函数和sort函数的时间,isCorrect()函数内的时间是一样的。
#include<algoritm>
int corr_arr[maxn];
sort(corr_arr+1,corr_arr+n+1); //使用库函数对arr进行排序,然后得到标准的模板
void isCorrect()
{
int i=1;
for(i=1;i<=n;i++)
if(medium[i]!=corr_arr[i])
{
printf("error\n");
return;
}
printf("correct\n");
}
四、测试数据集构造
归并排序三种情况合一,因为使用的是一个辅助空间,比较次数和移动次数都不会改变。
平均情况下不需要构建数据集,直接使用srand()函数,随机生成待排数组就可以。
最优情况下,快速排序,每次找到中枢结点,左右部分都是平均分配,可以使用一个算法,实现数据集构造,算法如下:
void fastSet(int arr[],int left,int right,int low,int high)
// left,right指插入到数组中的位置,low和high指插入数的区间,最小值和最大值
{
if(left<=right)
{
int mid=(left+right)/2; //这个位置放置arr[left]
int mid_num=(low+high)/2;
arr[left]=mid_num;
fastSet(arr,left+1,mid,low,mid_num-1);
fastSet(arr,mid+1,right,mid_num+1,high);
}
}
最优情况下,插入排序的数据集就是有序的,也不需要特别构造。
最坏情况下,快速排序集是有序的。
最坏情况下,插入排序的数据集是倒序的。
因此数据集,除了快速排序的最优集需要使用算法构造外,均不需要特别构造。
五、算法复杂性
计算程序运行时间函数
double run_time;
_LARGE_INTEGER time_start; //开始时间
_LARGE_INTEGER time_over; //结束时间
double dqFreq; //计时器频率
LARGE_INTEGER f; //计时器频率
QueryPerformanceFrequency(&f);
dqFreq=(double)f.QuadPart;
QueryPerformanceCounter(&time_start); //计时开始
// do something
QueryPerformanceCounter(&time_over); //计时结束
isCorrect();
run_time=1000000*(time_over.QuadPart-time_start.QuadPart)/dqFreq;
平均效率
计算100次求均值
计算规模\算法策略(us) | 简单插入排序 | 递归-归并排序 | 非递归-归并排序 | 快速排序 |
---|---|---|---|---|
10 | 0.164 | 0.369 | 0.311 | 0.160 |
100 | 11.820 | 6.771 | 4.972 | 8.465 |
1000 | 843.611 | 134.320 | 94.833 | 73.213 |
5000 | 21766.059 | 793.927 | 621.811 | 613.983 |
10000 | - | 1856.142 | 1316.877 | 1324.300 |
100000 | - | 16315.375 | 21803.295 | 20831.238 |
- | 45007.401 | 34868.027 | 34002.188 |
最优效率
计算规模\算法策略(us) | 简单插入排序 | 快速排序 |
---|---|---|
10 | 0.008 | 0.130 |
100 | 0.103 | 2.541 |
1000 | 5.251 | 70.514 |
5000 | 102.245 | 489.251 |
10000 | 15025.15 | 902.152 |
100000 | - | 15015.255 |
最低效率
计算规模\算法策略(us) | 简单插入排序 | 快速排序 |
---|---|---|
10 | 1.250 | 1.632 |
100 | 12.256 | 15.241 |
1000 | 902.155 | 1020.241 |
5000 | 1952.451 | 2041.514 |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ehswrSEi-1572329458711)(C:\Users\XiaoLin\AppData\Roaming\Typora\typora-user-images\image-20191028131306945.png)]
六、效率(比较频数,腾挪频数)
平均效率
计算规模\算法策略 | 简单插入排序 | 递归-归并排序 | 非递归-归并排序 | 快速排序 |
---|---|---|---|---|
10 | 34、41 | 68、143 | 37、113 | 28、67 |
100 | 2482、4667 | 1344、2453 | 794、2120 | 450 1620 |
1000 | 256566、510135 | 19952、34037 | 9995、29228 | 6194、25740 |
5000 | 6349483、12683969 | 123616 207621 | 69990 192244 | 36084 163225 |
10000 | - | 267232 547965 | 139990 404523 | 76522 354090 |
100000 | - | 3337856 5445661 | 1799987 5106689 | 939116 4633551 |
- | 7075712 14420653 | 3599987 10613257 | 2014658 10111398 |
最优效率
计算规模\算法策略 | 简单插入排序 | 快速排序 |
---|---|---|
10 | 15、21 | 15、43 |
100 | 1025、2515 | 205、1511 |
1000 | 1506211、321551 | 4152、18521 |
5000 | 5415262、10235521 | 28654、105256 |
10000 | - | 52652、285256 |
最低效率
计算规模\算法策略 | 简单插入排序 | 快速排序 |
---|---|---|
10 | 60、105 | 48、95 |
100 | 4152、7511 | 3510、6520 |
1000 | 415202、658014 | 315452、523651 |
5000 | 8514256、15235214 | 3056585、10556254 |
七、算法评价
插入排序的时间复杂度最大,比其他三个算法多了许多,其他三个理论实践复杂度都是O(NlogN),具体运行环境中,数量级基本一致,但是仍然有差别,在数据量较小的情况下差别不大,我在网上搜索得知当数据足够大的情况下,快速排序的效率是归并排序的两倍。值得注意的是,当数据量很大的时候,调用递归归并排序,会栈溢出。归并排序需要使用大小为n的物理空间,需要比较和移动的次数都明显多于快速排序。快速排序,速度最快,但是缺点是不是稳定的排序,并且在最坏情况下,退化为O(NlogN)。
在使用辅助空间的时候,需要在堆区声明一个空间,如果在子函数内,即栈区声明超过100,000以上的数组,就会出现爆栈的风险,影响程序的速度和正常运行。
在总体上不要求排序稳定性的条件下,可以优先考虑快速排序。当然即使考虑稳定性,也可使用快速排序,增加第二键值即可。