1、任务简述:
利用随机函数产生N个随机整数(N = 500,1000,1500,2000,2500,…,30000),利用直接插入排序、折半插入排序、希尔排序(对于不同数量的数据,生成不同的增量序列)、起泡排序、快速排序、选择排序、堆排序、两路归并排序(非递归)8种方法进行排序,统计每一种排序所需要的比较次数以及移动次数。
要求:
(1) 原始数据随机生成。
(2) 对于不同的数据规模,显示每种排序所需的比较次数以及移动次数。
(3) 分析每一种排序算法的特点
2、算法描述:
①直接插入排序:
首先来解释一下插入排序法的原理,它的原理是每插入一个数都要将它和之前的已经完成排序的序列进行重新排序,也就是要找到新插入的数对应原序列中的位置。那么也就是说,每次插入一个数都要对原来排序好的那部分序列进行重新的排序,时间复杂度同样为O(n²)。 这种算法是稳定的排序方法。数组下标0的位置作为哨兵,暂存待插入的元素。
实现:首先待插入元素的下标i从2开始移动,循环到数组最大长度对应的索引处L->len;然后,将待插入的元素与前一个元素即下标是i-1的元素进行比较。如果前一个元素比待插入的元素大,此时执行直接插入算法。否则不执行,i继续向后移动一个位置,重新插入一个新的元素。接着,将待插入元素暂存在哨兵L->r[0]处。再接着,将数组的元素向右移动一个位置(即增量为1,给待插入的元素留个位置,准备插入元素)。最后,将暂存在哨兵L->r[0]处的带插入元素放到由于上一步向右移动获得的空白位置处,完成整个插入操作。
②折半插入排序:
折半插入排序算法是一种稳定的排序算法,比直接插入算法明显减少了关键字之间比较的次数,因此速度比直接插入排序算法快,但记录移动的次数没有变,所以折半插入排序算法的时间复杂度仍然为O(n^2),与直接插入排序算法相同。空间复杂度为O(1)。
实现:在将一个新元素插入已排好序的数组的过程中,寻找插入点时,将待插入区域的首元素设置为a[low],末元素设置为a[high],则轮比较时将待插入元素与a[m],其中m=(low+high)/2相比较,如果比参考元素小,则选择a[low]到a[m-1]为新的插入区域(即high=m-1),否则选择a[m+1]到a[high]为新的插入区域(即low=m+1),如此直至low<=high不成立,即将此位置之后所有元素后移一位,并将新元素插入a[high+1]。
③希尔排序:
由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。效果好时,时间复杂度为:O(n^1.5);
实现:先取一个小于n的整数d作为第一个增量,把文件的全部记录分组。所有距离为d的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d’
④起泡排序:
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,是不会再交换的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。时间复杂度为O(n^2)
实现:设置标志位lastinde,lastinde为最后一次交化的位置,这样当一轮比较结束后如果lastinde=1,即:这一轮没有发生交换,说明数据的顺序已经排好,没有必要继续进行下去。
⑤快速排序:
理想的情况是,每次划分所选择的中间数恰好将当前序列几乎等分,经过log2n趟划分,便可得到长度为1的子表。这样,整个算法的时间复杂度为O(nlogn)。
最坏的情况是,每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度-1。这样,长度为n的数据表的快速排序需要经过n趟划分,使得整个排序算法的时间复杂度为O(n2)。
为改善最坏情况下的时间性能,可采用其他方法选取中间数。通常采用“三者值取中”方法,即比较low,high,mid对应的元素,取三者中关键字为中值的元素为中间数。
可以证明,快速排序的平均时间复杂度也是O(nlogn)。因此,该排序方法被认为是目前最好的一种内部排序方法。
从空间性能上看,尽管快速排序只需要一个元素的辅助空间,但快速排序需要一个栈空间来实现递归。最好的情况下,即快速排序的每一趟排序都将元素序列均匀地分割成长度相近的两个子表,所需栈的最大深度为log2(n+1);但最坏的情况下,栈的最大深度为n。这样,快速排序的空间复杂度为O(log2n))。
实现:快速排序算法通过多次比较和交换来实现排序,其排序流程如下:
(1)首先设定一个分界值,通过该分界值将数组分成左右两部分。
(2)将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
(3)然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
(4)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。
⑥选择排序:
选择排序的交换操作介于 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);选择排序是一个不稳定的排序算法
实现:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
⑦堆排序:
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的平均时间复杂度为O(nlogn),它是不稳定排序;堆是一个完全二叉树的结构,并同时满足性质:即子结点的键值或索引总是小于(或者大于)它的父节点——小(大)顶堆。
⑧两路归并排序(非递归):
首先每个元素自己为一个有序表,然后和相邻的元素组成一个新的有序表,然后以此为基础再继续组合。一直到只剩一个有序表。就完成了并归排序。
3、源代码
#include
#include
#include
#include
#include
void *getdata(int *a,int n);
void showdata(int *a,int n);
void insertsort(int *a,int n);
void binaryinsert(int *a,int n);
void shell(int *a,int n);
void shellsort(int *a,int n,int t,int *move,int *comp);
void bubble(int *a,int n);
void quick(int *a,int n);
int pivot(int *a,int low,int high,int *move,int *comp);
void quicksort(int *a,int low,int high,int *move,int *comp);
void selectsort(int *a,int n);
void heapsort(int *a,int n);
void heapadjust(int *a,int n,int index,int *move,int *comp);
void Mergesort(int *a,int n);
void Merge(int *b,int *c,int low,int mid,int high,int *move,int *comp);
main()
{
int *a,n;
system("color 1E");
printf("--------------081810221朱林昊--------------\n");
printf("\n--------------排序算法比较--------------\n\n");
printf("请输入需要的数据规模:");
scanf("%d",&n);
while(n>0)
{
a=(int *)malloc((n+1)*sizeof(int));
getdata(a,n);
insertsort(a,n);
binaryinsert(a,n);
shell(a,n);
bubble(a,n);
quick(a,n);
selectsort(a,n);
heapsort(a,n);
Mergesort(a,n);
printf("\n请输入需要的数据规模:");
scanf("%d",&n);
free(a);
}
}
void showdata(int *a,int n)
{
int i,j=0;
for(i=1;i<=n;i++)
{
printf("%-6d",a[i]);
j++;
if(j%15==0)
printf("\n");
}
printf("\n");
}
void *getdata(int *a,int n)
{
int i;
srand(time(0));
for(i=1;i<=n;i++)
a[i]=rand()%100;
}
void insertsort(int *a,int n)
{
int i,j,move=0,comp=0,*b;
b=(int *)malloc((n+1)*sizeof(int));
memcpy(b,a,(n+1)*sizeof(int));
for(i=2;i<=n;i++)
{
comp++;
if(b[i]<b[i-1])
{
b[0]=b[i];
move++;
for(j=i-1;;j--)
{
comp++;
if(b[j]>b[0])
{
b[j+1]=b[j];
move++;
}
else
break;
}
j++;
b[j]=b[0];
move++;
}
}
printf("\n直接插入排序需要%d次比较,%d次移动\n",comp,move);
free(b);
}
void binaryinsert(int *a,int n)
{
int i,j,low,high,mid,*b;
int move=0,comp=0;
b=(int *)malloc((n+1)*sizeof(int));
memcpy(b,a,(n+1)*sizeof(int));
for(i=2;i<=n;i++)
{
comp++;
if(b[i]<b[i-1])
{
move++;
b[0]=b[i];
low=1;
high=i-1;
while(low<=high)
{
comp++;
mid=(low+high)/2;
if(b[mid]==b[0])
{
low=mid;
break;
}
else if(b[mid]<b[0])
low=mid+1;
else
high=mid-1;
}
for(j=i-1;j>=low;j--)
{
b[j+1]=b[j];
move++;
}
b[low]=b[0];
move++;
}
}
printf("\n折半插入排序需要%d次比较,%d次移动\n",comp,move);
free(b);
}
void shellsort(int *a,int n,int t,int *move,int *comp)
{
int i,j;
for(i=t+1;i<=n;i++)
{
if(a[i]<a[i-t])
{
a[0]=a[i];
(*move)++;
for(j=i-t;j>=1;j=j-t)
{
(*comp)++;
if(a[j]>a[0])
{
a[j+t]=a[j];
(*move)++;
}
else
break;
}
j=j+t;
a[j]=a[0];
(*move)++;
}
}
}
void shell(int *a,int n)
{
int i,j,*b,k,t;
int move=0,comp=0;
b=(int *)malloc((n+1)*sizeof(int));
memcpy(b,a,(n+1)*sizeof(int));
for(i=1;pow(2,i)<=n;i++);
for(i=i-1;i>=1;i--)
{
t=pow(2,i)-1;
shellsort(b,n,t,&move,&comp);
}
printf("\n希尔排序需要%d次比较,%d次移动\n",comp,move);
free(b);
}
void bubble(int *a,int n)
{
int i,j,t,*b,lastindex=1;
int move=0,comp=0;
b=(int *)malloc((n+1)*sizeof(int));
memcpy(b,a,(n+1)*sizeof(int));
i=n;
while(i>1)
{
lastindex=1;
for(j=1;j<i;j++)
{
comp++;
if(b[j]>b[j+1])
{
move=move+3;
t=b[j];
b[j]=b[j+1];
b[j+1]=t;
lastindex=j;
}
}
i=lastindex;
}
printf("\n冒泡排序需要%d次比较,%d次移动\n",comp,move);
free(b);
}
void quick(int *a,int n)
{
int i,*b;
int move=0,comp=0;
b=(int *)malloc((n+1)*sizeof(int));
memcpy(b,a,(n+1)*sizeof(int));
quicksort(b,1,n,&move,&comp);
printf("\n快速排序需要%d次比较,%d次移动\n",comp,move);
free(b);
}
int pivot(int *a,int low,int high,int *move,int *comp)
{
a[0]=a[low];
(*move)++;
while(low<high)
{
while(low<high&&a[high]>=a[0])
{
(*comp)++;
high--;
}
a[low]=a[high];
(*move)++;
while(low<high&&a[low]<=a[0])
{
(*comp)++;
low++;
}
a[high]=a[low];
(*move)++;
}
a[low]=a[0];
return low;
}
void quicksort(int *a,int low,int high,int *move,int *comp)
{
int pos;
if(low<high)
{
pos=pivot(a,low,high,move,comp);
quicksort(a,low,pos-1,move,comp);
quicksort(a,pos+1,high,move,comp);
}
}
void selectsort(int *a,int n)
{
int i,j,p,t,*b;
int move=0,comp=0;
b=(int *)malloc((n+1)*sizeof(int));
memcpy(b,a,(n+1)*sizeof(int));
for(i=1;i<n;i++)
{
p=i;
for(j=i+1;j<=n;j++)
{
comp++;
if(b[j]<b[p])
p=j;
}
if(p!=i)
{
move+=3;
t=b[p];
b[p]=b[i];
b[i]=t;
}
}
printf("\n选择排序需要%d次比较,%d次移动\n",comp,move);
free(b);
}
void heapsort(int *a,int n)
{
int *b,i;
int move=0,comp=0;
b=(int *)malloc((n+1)*sizeof(int));
memcpy(b,a,(n+1)*sizeof(int));
for(i=n/2;i>=1;i--)
heapadjust(b,n,i,&move,&comp);
for(i=1;i<n;i++)
{
b[0]=b[1];
b[1]=b[n-i+1];
b[n-i+1]=b[0];
heapadjust(b,n-i,1,&move,&comp);
}
printf("\n堆排序比较%d次 移动%d次\n",comp,move);
free(b);
}
void heapadjust(int *a,int n,int index,int *move,int *comp)
{
int i,j;
i=index;
j=2*i;
a[0]=a[i];
(*move)++;
while(j<=n)
{
(*comp)++;
if(j<n&&a[j+1]>a[j])
j++;
(*comp)++;
if(a[j]>a[0])
{
(*move)++;
a[i]=a[j];
i=j;
j*=2;
}
else
break;
}
a[i]=a[0];
(*move)++;
}
void Merge(int *b,int *c,int low,int mid,int high,int *move,int *comp)
{
int i=low,j=mid+1,k=low;
while(i<=mid && j<=high)
{
(*comp)++;
if(b[i]<=b[j])
c[k++]=b[i++];
else
c[k++]=b[j++];
(*move)++;
}
while(i<=mid)
{
c[k++]=b[i++];
(*move)++;
}
while(j<=high)
{
c[k++]=b[j++];
(*move)++;
}
}
void Mergesort(int *a,int n)
{
int *b=(int *)malloc(sizeof(int)*(n+1));
int *c=(int *)malloc(sizeof(int)*(n+1));
memcpy(b,a,sizeof(int)*(n+1));
int i,j,k,low,top,mid,len=1,move=0,comp=0;
while(len<n)
{
for(j=1;j<=(n-2*len+1);j+=2*len)
Merge(b,c,j,j+len-1,j+2*len-1,&move,&comp);
if((n+1-j)>len)
Merge(b,c,j,j+len-1,n,&move,&comp);
else
for(k=j;k<=n;k++)
c[k]=b[k];
memcpy(b,c,sizeof(int)*(n+1));
len*=2;
}
printf("\n归并排序比较%d次 移动%d次\n",comp,move);
free(b);
free(c);
}
4、运行结果
1.数据顺序有序:
2.数据逆序有序:
3.数据无序,随机:
5、总结
运行正确,对于顺序,逆序,无序的结果都可以排序成功,里面有showdata函数,可以可视化排序后的数组,当然,因为本题的侧重点是比较这些排序算法的性能,因此,没有可视化排序后的结果。
我们可以发现,当数据本身大致有序,或者完全有序时,那些经典的算法,比较和移动的次数是最少的,甚至完全有序时,比较次数只有n-1,移动次数只有0,但是当数据无序时,那些改进的算法就很有优势了,他们要么移动次数减少,要么比较次数减少,要么都有所减少。
性能分析:
1.直接插入:
数据敏感性:
最好的情况下(有序):比较n-1次,移动0次
最坏的情况下(逆序):比较2+3+…+n=(n+2)*(n-1)/2次 ,移动(1+1+1)+(1+1+2)+…+(1+1+n-1)=(n+4)(n-1)/2 (1+1+x)代表一次移入哨兵位一次移出哨兵位,和插入引起的数据移动。
可以看出对数据是敏感的
时间复杂度:O(n^2)
空间复杂度:O©
稳定性:稳定(相邻元素比较)
2.折半插入
只改变了比较次数,未改变移动次数,时间复杂度仍为O(n^2),空间复杂度为O©,对数据依然敏感,但不再稳定
3.希尔排序
时间复杂度:与增量序列的取值有关,一般达到的较好情况为O(n^1.5)
空间复杂度:本地实现O©
稳定性:不稳定(涉及非相邻元素的比较)
数据敏感性:对数据敏感
4.冒泡排序
时间复杂度:O(n^2)
比较次数:1+…+(n-1)=n(n-1)/2
移动次数:最差情况下为3n(n-1)/2
空间复杂度:O©
稳定性:是稳定的(相邻元素比较)
5.冒泡排序改进
最好情况下(原数组有序):比较次数为 n-1,移动次数为 0
最坏情况下(原数组逆向有序):比较次数为 n(n-1)/2,移动次数为 3n(n-1)/2
改进了算法的执行效率,但没有改变时间复杂度O(n^2)
空间复杂度 O©
稳定性:是稳定的(相邻元素比较)
6.快速排序
对数据较敏感
时间复杂度:
最坏的情况下:即原序列有序,第一次从a[high]向前找将会比较n-1次,之后对除第一个元素外的序列操作,同样这个序列也是有序的,需要比较n-2次,一直这样进行下去,那么将退化为冒泡排序,比较n(n-1)/2次,移动n-1次。平均情况下时间复杂度为O(nlogn)
空间复杂度:
虽然是本地实现的,但递归是需要用到zhai的,存在隐含的空间复杂度。相当于求点的个数一定的二叉树的深度。最好的情况便是完全二叉树,zhai的最大深度为【logn】+1,最坏的情况为原序列有序,zhai的深度为n。
如果每次快排后先对长度较短的子序列坐快排,那么最大深度可降为logn。这样是因为短的区间在排几次之后就会变得有序,不会有很多元素入zhai,对zhai的需求较低,如果先做长的区间的话,首先原来的短区间还在zhai里,其次如果每次都先做长区间,那么就会有短去见一直积累在zhai中等待处理,只有长区间处理完毕才会去处理那些短区间,因此对zhai的需求就特别大。
稳定性:不稳定(涉及到非相邻元素的比较与移动)
7.选择排序
时间复杂度:O(n^2)
比较次数:n-1+n-2+…+1=n(n-1)/2
移动次数:min(有序):0 -> max(例如51234): 3(n-1)
空间复杂度:O(1)
稳定性:不稳定(涉及非相邻元素的比较和移动)
对数据不敏感
8.堆排序
时间复杂度:
运行时间主要集中在构建初始堆和删除根节点后的筛选上了。每进行一次筛选的时间复杂度为O(logn)(因为完全二叉树的最大深度为[logn]+1),因此所有的筛选和建堆操作的时间复杂度为O(nlogn)
空间复杂度:O©
稳定性:不稳定
对数据不敏感