课程设计中的一个题目,现在分享给大家。
一、 设计(实验)要求和内容
- 完成快速排序,冒泡排序,插入排序,希尔排序,归并排序,的编程实现。
- 完成几种算法的时间计算。
- 计算多次算法实现的平均值,在程序里可以自由的调用。
二、 算法原理与分析
2.1 排序算法基本思想
- 快速排序:
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
- 冒泡排序:
依次比较相邻的两个数,将小数放在前面,大数放在后面。即在第一趟:首先比较第1个和第2个数,将小数放前,大数放后。然后比较第2个数和第3个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后。冒泡排序流程至此第一趟结束,将最大的数放到了最后。在第二趟:仍从第一对数开始比较(因为可能由于第2个数和第3个数的交换,使得第1个数不再小于第2个数),将小数放前,大数放后,一直比较到倒数第二个数(倒数第一的位置上已经是最大的),第二趟结束,在倒数第二的位置上得到一个新的最大数(其实在整个数列中是第二大的数)。如此下去,重复以上过程,直至最终完成排序。
- 插入排序:
将n个元素的数列分为已有序和无序两个部分,如插入排序下所示:
{{a1},{a2,a3,a4,…,an}}
{{a1⑴,a2⑴},{a3⑴,a4⑴ …,an⑴}}
…
{{a1(n-1),a2(n-1) ,…},{an(n-1)}}
每次处理就是将无序数列的第一个元素与有序数列的元素从后往前逐个进行比较,找出插入位置,将该元素插入到有序数列的合适位置中。
- 归并排序:
首先将初始序列的n个记录看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到n/2个长度为2的有序子序列,在此基础上,再对长度为2的有序子序列进行两两归并,得到若干个长度为4的有序子序列,以此类推,直到得到一个长度为n的有序序列为止
- 希尔排序:
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
三、 排序算法分析
- 快速排序:
(1)当分区选取的基准元素为待排序元素中的最大或最小值时,为最坏的情况,时间复杂度和直接插入排序的一样,移动次数达到最大值
Cmax = 1+2+…+(n-1) = n*(n-1)/2 = O(n2) 此时最好时间复杂为O(n2)
(2)时间空间复杂度
当分区选取的基准元素为待排序元素中的"中值",为最好的情况,时间复杂度为O(nlog2n)。
快速排序的空间复杂度为O(log2n).
(3)快速排序的优点
速度很快,是目前为止最快的一种内排序算法
(4)稳定性
当待排序元素类似[6,1,3,7,3]且基准元素为6时,经过分区,形成[1,3,3,6,7],两个3的相对位置发生了改变,所是快速排序是一种不稳定排序。
- 冒泡排序:
(1)由此可见:N个数字要排序完成,总共进行N-1趟排序,每i趟的排序次数为(N-i)次,所以可以用双重循环语句,外层控制循环多少趟,内层控制每一趟的循环次数
(2)冒泡排序的优点:每进行一趟排序,就会少比较一次,因为每进行一趟排序都会找出一个较大值。如上例:第一趟比较之后,排在最后的一个数一定是最大的一个数,第二趟排序的时候,只需要比较除了最后一个数以外的其他的数,同样也能找出一个最大的数排在参与第二趟比较的数后面,第三趟比较的时候,只需要比较除了最后两个数以外的其他的数,以此类推……也就是说,没进行一趟比较,每一趟少比较一次,一定程度上减少了算法的量。
(3)时间复杂度
1.如果我们的数据正序,只需要走一趟即可完成排序。所需的比较次数C和记录移动次数M均达到最小值,即:Cmin=n-1;Mmin=0;所以,冒泡排序最好的时间复杂度为O(n)。
2.如果很不幸我们的数据是反序的,则需要进行n-1趟排序。每趟排序要进行n-i次比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:
综上所述:冒泡排序总的平均时间复杂度为:O(n2) ,时间复杂度和数据和数据状况无关、
- 插入排序:
(1)时间空间复杂度
如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数加上 (n-1)次。平均来说插入排序算法的时间复杂度为O(n^2)。
(2)稳定性
直接插入排列是基于明确的相邻位置的两个元素的比较,因此该算法是稳定的。排序过程的比较次数与待排序列的初始状态有关。每进行一趟排列并不能唯一地确定下一个元素的最终位置
- 归并排序:
(1)归并排序算法有两个基本的操作,一个是分,也就是把原数组划分成两个子数组的过程。另一个是治,它将两个有序数组合并成一个更大的有序数组。
它将数组平均分成两部分:
center = (left + right)/2,当数组分得足够小时—数组中只有一个元素时,只有一个元素的数组自然而然地就可以视为是有序的,此时就可以进行合并操作了。因此,上面讲的合并两个有序的子数组,是从
只有一个元素 的两个子数组开始合并的。
合并后的元素个数:从
1–>2–>4–>8…
比如初始数组:[24,13,26,1,2,27,38,15]
② 分成了两个大小相等的子数组:[24,13,26,1] [2,27,38,15]
②再划分成了四个大小相等的子数组:[24,13] [26,1] [2,27] [38,15]
③此时,left < right 还是成立,再分:[24] [13] [26]
[1] [2] [27] [38] [15]
此时,有8个小数组,每个数组都可以视为有序的数组了,每个数组中的left == right,从递归中返回(从19行–20行的代码中返回),故开始执行合并(第21行):
merge([24],[13]) 得到 [13,24]
merge([26],[1]) 得到[1,26]
…
最终得到 有序数组。
(2)时间空间复杂度
归并排序中,用到了一个临时数组,故空间复杂度为O(N)
由归并排序的递归公式:T(N)
= 2T(N/2) + O(N) 可知时间复杂度为O(NlogN)
数组的初始顺序会影响到排序过程中的比较次数,但是总的而言,对复杂度没有影响。平均情况 or 最坏情况下 它的复杂度都是O(NlogN)
此外,归并排序中的比较次数是所有排序中最少的。原因是,它一开始是不断地划分,比较只发生在合并各个有序的子数组时。
(3)稳定度
是稳定的排序算法,因为两两的归并,它们之间是基于相邻元素之间的比较。
- 希尔排序:
(1)优缺点
优点:极快数据移动少;
缺点:不稳定;
(2)效率分析
此排序算法的效率在序列越乱的时候,效率越高。在数据有序时,会退化成冒泡排序;
四、 程序模块结构
由课题要求可将程序划分为以下几个模块(即实现程序功能所需的函数):
生成随机数函数void
random()
主菜单函数void
main_menu()
菜单函数void
menu(int choice1)
时间计算函数double
timer(int choice)
计算平均时间函数void
caculate(double *aver)
快速排序函数int
quick(int first,int end,int L[])
冒泡排序函数void
bubble_sort(int a[])
插入排序函数void
direct(int a[])
归并排序函数void
merge(int arr[], int low, int mid, int high)
希尔排序函数void
xier(int a[])
五、 函数实现
快速排序算法步骤:
1. 先从数列中取出一个数作为基准数。
2. 分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3. 再对左右区间重复第二步,直到各区间只有一个数。
编程实现:
int quick(int first,int end,int L[])
{
int
left=first,right=end,key;key=L[first];
while(left<right)
{
while((left<right)&&(L[right]>=key))
right--;
if(left<right)
L[left++]=L[right];
while((left<right)&&(L[left]<=key))
left++;
if(left<right)L[right--]=L[left];}
L[left]=key;
return
left;
}
void quick_sort(int L[],int first,int end)
{
int
split;
if(first<end)
{
split=quick(first,end,L);
quick_sort(L,first,split-1);
quick_sort(L,split+1,end);
}
}
冒泡排序算法步骤:
1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2.对每一对相邻元素
作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
3.针对所有的元素重复以上的步骤,除了最后一个。
4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
void bubble_sort(int a[])
{
int
i,j,w;
for(i=0;i<N;i++)
for(j=0;j<N-i;j++)
if(a[j]>a[j+1])
{
w=a[j];
a[j]=a[j+1];
a[j+1]=w;
}
}
插入排序算法原理与步骤:
⒈从有序数列和无序数列{a2,a3,…,an}开始进行排序;
⒉处理第i个元素时(i=2,3,…,n),数列{a1,a2,…,ai-1}是已有序的,而数列{ai,ai+1,…,an}是无序的。用ai与ai-1,a i-2,…,a1进行比较,找出合适的位置将ai插入;
⒊重复第二步,共进行n-i次插入处理,数列全部有序。
编程实现:
void direct(int a[])
{
int
i,j,w;
for(i=0;i<N;i++)
{
for(j=i;j>=0;j--)
{
if(a[j]>=a[j+1])
{
w=a[j];
a[j]=a[j+1];
a[j+1]=w;
}
}
}
}
归并排序算法原理与步骤:
1.开辟一块空间,用于存放合并后的序列
2.设定两个指针,起始位置分别位于两个已排序序列的起始位置
3.比较两个指针所代表的元素,将更小的元素放入合并空间,并将指针后移
4.重复步骤3,直至有一个指针到达尾部
5.将另一个序列的剩余元素复制到合并序列尾
归并排序的递归公式:T(N)
= 2T(N/2) + O(N)
编程实现:
void merge(int arr[], int low, int mid, int
high){
int i, k;
int *tmp = (int *)malloc((high-low+1)*sizeof(int));
//申请空间,使其大小为两个
int left_low = low;
int left_high = mid;
int right_low = mid + 1;
int right_high = high;
for(k=0; left_low<=left_high && right_low<=right_high;
k++){ // 比较两个指针所指向的元素
if(arr[left_low]<=arr[right_low]){
tmp[k] = arr[left_low++];
}else{
tmp[k] = arr[right_low++];
}
}
if(left_low <= left_high){ //若第一个序列有剩余,直接复制出来粘到合并序列尾
//memcpy(tmp+k, arr+left_low, (left_high-left_low+l)*sizeof(int));
for(i=left_low;i<=left_high;i++)
tmp[k++] = arr[i];
}
if(right_low <= right_high){
//若第二个序列有剩余,直接复制出来粘到合并序列尾
//memcpy(tmp+k, arr+right_low, (right_high-right_low+1)*sizeof(int));
for(i=right_low; i<=right_high; i++)
tmp[k++] = arr[i];
}
for(i=0; i<high-low+1; i++)
arr[low+i] = tmp[i];
free(tmp);
return;
}
void merge_sort(int arr[], unsigned int
first, unsigned int last){
int mid = 0;
if(first<last){
mid = (first+last)/2; /* 注意防止溢出 */
/*mid = first/2 + last/2;*/
//mid = (first & last) + ((first ^ last) >> 1);
merge_sort(arr, first, mid);
merge_sort(arr, mid+1,last);
merge(arr,first,mid,last);
}
return;
}
#endif
希尔排序算法原理与步骤:
1.先取一个小于N的整数gap作为第一个增量,把文件的全部记录分成gap个组,所有距离为gap的倍数的记录放在同一个组中。
2.在各自组内进行直接插入排序
3.取第二个增量gap2<gap1重复上述的分组和排序,直至所取的增量gap=1,即所有记录放在同一组中进行直接插入排序为止
编程实现:
void xier(int a[])
{
int n = N;
int i, j, k, temp, gap;
/*希尔排序*/
for(gap=n/2; gap>=1; gap/=2)
{
for(i=0; i<gap; ++i)
{
for(j=i+gap; j<n; j+=gap)
{
if(a[j-gap] > a[j])
{
temp = a[j] ;
for(k=j-gap; k>=0
&& a[k]>temp; k-=gap)
a[k+gap] = a[k] ;
a[k+gap] = temp ;
}
}
}
}
}
随机数生成:
void random()
{
int i=0,j,b,max,min;
printf("请输入你所期望的将要生成随机数的取值范围:\n");
printf("最小值(不能为负数): ");
scanf("%d",&min);
printf("最大值(无上限) : ");
scanf("%d",&max);
srand(
(int) time(0));
for(j=0;i<N;j++)
{
b=rand();
if(b>=min&&b<=max)
{
a[i]=b;
aa[i]=b;
i++;
}
}
// printf("随机数如下: \n");
// for(i=0;i<N;i++)
// printf("%d",a[i]);
}
时间计算:
记录两个时间点,其差值即为该算法的运行时间。
double timer(int choice)
{
double t1,t2,t;
t1=(double)clock();
if(choice==1)
{
printf("\n现在使用快速排序法进行排序 :\n");
quick_sort(a,0,N-1);
}
if(choice==2)
{
printf("\n现在使用冒泡排序法进行排序 :\n");
bubble_sort(a);
}
if(choice==3)
{
printf("\n现在使用插入排序法进行排序 :\n");
direct(a);
}
if(choice==4)
{
printf("\n现在使用归并排序法进行排序 :\n");
merge_sort(a,0,N-1);
}
if(choice==5)
{
printf("\n现在使用希尔排序法进行排序 :\n");
xier(a,2);
}
t2=(double)clock();
t=difftime(t2,t1)/CLK_TCK;
// for(i=0;i<N;i++)
// printf("%d ",a[i]);
printf("\n所用排序时间为: %f 秒\n",t);
return t;
}
void caculate(double *aver) //b[]用来计数,计时
{
static
double count[5]={0};
static
double timec[5]={0};
if(choice==7)
{
int o;
printf("\n希尔排序算法性能研究:\n");
printf("\n请输入参数2的改变值:\n");
scanf("%d",&o);
double t1,t2,t;
t1=(double)clock();
xier(a,o);
printf("\n现在使用希尔排序法进行排序 :\n");
t2=(double)clock();
t=difftime(t2,t1)/CLK_TCK;
printf("\n所用排序时间为: %f 秒\n",t);
}
else
if(choice==6)
{
for(int i=0;i<5;i++)
{
count[i]++;
timec[i]=timec[i]+timer(i+1);
aver[i]=timec[i]/count[i];
for(int j=0;j<=N;j++)
a[j]=aa[j];
}
}
else
if((choice>0)&&(choice<6))
{
count[choice-1]++;
timec[choice-1]=timec[choice-1]+timer(choice);
aver[choice-1]=timec[choice-1]/count[choice-1];
}
else
{
printf("输入错误! \n请重新选择你所要使用的排序方法:\n");
main_menu();
}
}
六、 程序运行结果
本程序在VC++环境下实现,下面是对以上测试数据的运行结果:
主菜单显示如下:
结果如下:
菜单显示如下:
以上是排序算法比较的介绍。
其中程序中的N设定了样本的个数,即参与排序的数字的个数。
在输入框内输入取值的上下界,随机数将在其中产生。
附程序代码,需要自取。