发布于 

插入排序(Insert Sort)、归并排序(Merge Sort)和快速排序(Quick Sort)

一、算法简介

1.插入排序算法(Insert Sort Algorithm)

  • 直接插入排序(Straight Insertion Sort)的基本思想是
    把n个待排序的元素看成为一个有序表和一个无序表。开始时有序表中只包含1个元素,无序表中包含有n-1个元素,排序过程中每次==从无序表中取==出第一个元素,将它==插入到有序表中==的适当位置,使之成为新的有序表,重复n-1次可完成排序过程。
  • 我们需要做的工作只有两个:
    • 取出无序区中的第1个数,并找出它在有序区对应的位置。
    • 将无序区的数据插入到有序区;若有必要的话,则对有序区中的相关数据进行移位。

2.归并排序算法(Merge Sort Algorithm)

  • 归并排序的操作:
    将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
  • 归并排序的应用场景:
    速度仅次于快速排序,为稳定排序算法,一般用于==对总体无序,但是各子项相对有序的数列==

    3.快速排序算法(Quick Sort Algorithm)

  • 快速排序的思想:
    通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
  • 适用场景:
    快速排序是==不稳定的==。它不需要额外的存储空间。它的应用场景是大规模的数据排序,并且实际性能要好于归并排序。

二、程序

Ⅰ、算法程序

1.插入排序算法(Insert Sort Algorithm)

/*Insertion Sorting*/
/*每次从无序的队列中选择一个插入,
*直到所有元素都排序完成。*/
void InsertSortUp(int A[], int n)
{
for (int i = 1; i < n; i++) {
int swap = A[i];
int j;
for (j = i-1 ; A[j] > swap && j >= 0; j--)
A[j+1] = A[j];
A[j+1] = swap;
}
}

2.归并排序算法(Merge Sort Algorithm)

/*MergeSort*/

void Merge(int A[], int p, int q, int r)
{
//创造左右两个数组L、R,将已经排好的两部分放进去
int n1 = q - p + 1, n2 = r - q;
int L[n1], R[n2];
int i,j;
for (i = 0; i < n1; i++)
L[i] = A[p + i];
for (j = 0; j < n2; j++)
R[j] = A[q + 1 + j];

	//开始对两个数组进行归并 
i = 0; j = 0;
for (int k = p; k <= r; k++) {
if(i == n1){//如果左边数组已经全放进去
while(j < n2) A[k++] = R[j++];
} else if ( j == n2){//如果右边数组已经全放进去
while(i < n1) A[k++] = L[i++];
} else{//比较两个数组,把小的数放进A数组,指针后移
if (L[i] <= R[j]) {
A[k] = L[i];
i++;
}
else {
A[k] = R[j];
j++;
}
}

}
}
void MergeSortUp(int A[], int p,int r)
{
if (p < r) {
int q = (p + r) / 2;
MergeSortUp(A, p, q);
MergeSortUp(A, q + 1, r);
Merge(A, p, q, r);
}
}

3.快速排序算法(Quick Sort Algorithm)

/*QuickSort*/
void swap(int A[], int i, int j)
{
int x = A[i];
A[i] = A[j];
A[j] = x;
}

int partition(int A[], int p, int r)
{
int x = A[p];
int i = p;
for (int j = p + 1; j <= r; j++) {
if (A[j] < x) {
i = i + 1;
swap(A, i, j);
}
}
swap(A, p, i);
return i;
}
void QuickSortUp(int A[], int p, int r)
{
if (p < r) {
int q = partition(A, p, r);
QuickSortUp(A, p, q - 1);
QuickSortUp(A, q + 1, r);
}
}

Ⅱ、测试程序

/*Test*/
#include<ctime>
#include<stdlib.h>
#include<stdio.h>
#define size 10000 //size为数据规模
#include"InsertSort.cpp"
#include"MergeSort.cpp"
#include"QuickSort.cpp"

int A[size];
clock_t start,finish;
double runtime_insert,runtime_merge,runtime_quick;
/*Generating test data set*/
void tstdata(int n)
{
FILE *fp;
if((fp = fopen("tstdata.txt","w+"))== NULL)
printf("cant open the file.\n");
else{
srand(time(NULL));
for (int i = 1; i <= n; i++) {
if(i!=n) fprintf(fp,"%d ",rand());
else fprintf(fp,"%d\n",rand());
}
fclose(fp);
}

}
/*output function*/
int output(char *filename,int A[])
{
FILE * fp;
if((fp = fopen(filename,"w+"))==NULL){
printf("cant open the file.\n");
}
else{
for(int i=0;i<size;i++){
if(i!=size-1) fprintf(fp,"%d ",A[i]);
else fprintf(fp,"%d\n",A[i]);
}
fclose(fp);
}

return 0;
}
main()
{
int i=0;

/*generating test data*/
/*tstdata(size);
printf("Data set has been created.\n");*/


/*get the test data*/
FILE *fp;
if((fp = fopen("tstdata.txt","r"))==NULL){
printf("cant open the file.\n");
}
while(fscanf(fp, "%d", &A[i]) != EOF)
i++;
fclose(fp);
for(i=0;i<size;i++){
printf("%d ",A[i]);
}
printf("\n");
printf("Array has been created.\n");


/*copy the test data set*/
int A1[size],A2[size],A3[size];
for(int i=0;i<size;i++){
A1[i]=A[i];
A2[i]=A[i];
A3[i]=A[i];
}

    /*Insert Sorting*/
printf("Insert Sorting...\n");

start = clock();
InsertSortUp(A1,size);
finish = clock();

output("InsertSortUp.txt",A1);
runtime_insert = (double)(finish - start)/CLOCKS_PER_SEC;
printf("Insert Sort has been finished.\nTime Cost:%lf\n",runtime_insert);


/*Merge Sorting*/
printf("Merge Sorting...\n");

start = clock();
MergeSortUp(A2,0,size-1);
finish = clock();

output("MergeSortUp.txt",A2);
runtime_merge = (double)(finish-start)/CLOCKS_PER_SEC;
printf("Merge Sort has been finished.\nTime Cost:%lf\n",runtime_merge);

/*Quick Sorting*/
printf("Quick Sorting...\n");

start = clock();
QuickSortUp(A3,0,size-1);
finish = clock();

output("QuickSortUp.txt",A3);
runtime_quick = (double)(finish-start)/CLOCKS_PER_SEC;
printf("Quick Sort has been finished.\nTime Cost:%lf\n",runtime_quick);


}

三、测试数据集生成及测试

  • 我为算法的测试准备了12个测试数据集,其中数据量分别为10,000\50,000\100,000
  • 每个数据量下有==随机生成数据集(用于测试平均复杂度)==和==逆序数据集(用于测试最坏情况)==
  • 为了减小误差,每个类型的数据集都准备了两个,这样便生成了3x2x2=12个数据集
  • 算法的正确性测试在数据量很小的时候进行了手动验证,所以这里我们仅着重比较时间复杂度

Ⅰ、测试数据集生成

通过main函数中的/generating test data/部分生成“随机生成测试集”,详细测试数据见附件。
通过快速排序算法生成相应的“逆序数据集”进行排序算法的时间复杂度测试,详细数据见附件。

Ⅱ、测试过程

测试结果如下表:(详见附件)

data set Insert Sort(s) Merge Sort(s) Quick Sort(s)
S1:10,000\rand array(average condition) 0.082 0.001 0.002
S2:10,000\rand array(average condition) 0.069 0.002 0.001
S3:10,000\reserve array(worst condition) 0.469 0.003 0.554
S4:10,000\reserve array(worst condition) 0.463 0.003 0.546
S5:50,000\rand array(average condition) 1.718 0.009 0.009
S6:50,000\rand array(average condition) 1.881 0.01 0.009
S7:50,000\reserve array(worst condition) 8.54 0.015 /
S8:50,000\reserve array(worst condition) 7.956 0.015 /
S9:100,000\rand array(average condition) 7.023 0.022 0.017
S10:100,000\rand array(average condition) 6.86 0.02 0.018
S11:100,000\reserve array(worst condition) 31.549 0.027 /
S12:100,000\reserve array(worst condition) 30.989 0.029 /

注:50,000和100,000数据量下,最坏情况下快速排序算法程序没法完成排序

四、算法复杂度分析

1.插入排序算法(Insert Sort Algorithm)

  • 最优情况:
    最少比较一次,移动两次。
    Cmin = n-1;Mmin=(n-1)×2;
  • 最坏情况:
    最多比较i次,移动i+2次(逆序)(i=1,2,…,n-1)
    Cmax=1+2+…+(n-1)=(n2-n)/2
    M max=3+4+…+(n+1)=(n2+3n-4)/2
    Cave=(n2+n-2)/4
    M ave=(n2+7n-8)/4
  • 故直接插入排序的时间复杂度为O(n2),它的时间复杂度和待排序列的顺序有关。

2.归并排序算法(Merge Sort Algorithm)

通过迭代作图法可知,归并算法的算法复杂度为O(nlogn),它的时间复杂度和待排序列的顺序无关。

3.快速排序算法(Quick Sort Algorithm)

  • 最坏情况:
    顺序或逆序时,一次partition只能解决一个元素的位置 排列,所以最坏情况下的时间复杂度为O(n^2)
  • 平均情况:
    O(logn),枢轴元素两边的待排序列分的越平均,时间复杂度越小。

五、算法优化

1.归并排序的“哨兵”

在归并排序中,将两个已经排号的序列整合在一起时,之前我们是这样做的:

if(i == n1){//如果左边数组已经全放进去 
while(j < n2) A[k++] = R[j++];
} else if ( j == n2){//如果右边数组已经全放进去
while(i < n1) A[k++] = L[i++];
}

如果在待排的两个序列的最右端添加一个==哨兵==,即最大值MAX,就不用判断有序列已经选完了的问题,能够有效的减少判断的次数。

2.快速排序枢轴元素pivot的选取

pivot的选择对于快速排序时间复杂度的影响十分的大。从上面的“逆序测试数据”可知,如果每次选择的pivot都是最大/最小值,快速排序的复杂度可能会达到O(n^2)。
每次运行过程中,随机选取pivot, 通常能得到比较好的结果。
我采用了一种==三者取中==的方法,即选取第一个、最后一个以及中间的元素的中位数作为pivot,这样能够有效的避免“worst condition”的出现。
代码如下:(摘自csdn博客)

//median-of-three pivot rule
private static int choosePivotMedianOfThree(int[] a, int l, int r) {
int mid = 0;
if ((r-l+1) % 2 == 0) {
mid = l + (r-l+1)/2 - 1;
} else {
mid = l + (r-l+1)/2;
}

//只需要找出中位数即可,不需要交换
//有的版本也可以进行交换
if (((a[l]-a[mid]) * (a[l]-a[r])) <= 0) {
return l;
} else if (((a[mid]-a[l]) * (a[mid]-a[r])) <= 0) {
return mid;
} else {
return r;
}
}
---------------------
/*作者:xinyuexy
*来源:CSDN
*原文:https://blog.csdn.net/qq_31903733/article/details/82945605
*版权声明:本文为博主原创文章,转载请附上博文链接!*/

再将选取的pivot与队列第一个元素交换即可。

3.快速排序稳定性的改善

快速排序是“不稳定”的原因在于,partition的最后一步,pivot和指针i位置的元素交换。
举例:

  • 3 1 3’ 5 2 6 1’(大小相同的元素用’区分)
  • 3 1 2 5 3’ 6 1’
  • 3 1 2 1’ 3’ 6 5
    (这时候还没问题)
  • 1’ 1 2 3 3’ 6 5
    (最后一步1’和1的顺序发生变化)

解决方法:每次partition的最后一步时,遍历待排数组A[i]之前的部分,将与A[i]大小相同的元素整体后移。

六、实验心得

  • 本次实验我学习了三种重要算法:插入排序算法、归并排序算法和快速排序算法,了解了它们的原理和适用的情景。
  • 我认为相对于快速排序,归并排序更具有健壮性,它不会因为序列的顺序影响时间复杂度,而且它是一个稳定的排序。可能由于数据集不够大,我还没能充分体会到快速排序在时间上的优势。
  • 最坏情况下的时间复杂度和平均时间复杂度相差非常大,在以后分析算法时要兼顾两者。

七、附录大纲

  • Test.cpp
    测试程序,包括数据集生成,待排序列输出等等。
  • InsertSort.cpp
    插入排序算法
  • MergeSort.cpp
    归并排序算法
  • QuickSort.cpp
    快速排序算法
  • 12个测试数据集txt
  • 12个测试结果的截图