插入排序(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) 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) void Merge (int A[], int p, int q, int 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) 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); } }
Ⅱ、测试程序 #include <ctime> #include <stdlib.h> #include <stdio.h> #define size 10000 #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个测试结果的截图