前言:
排序算法在本科的时候就学习过冒泡法,也没有想过如何去计算算法的复杂度,现在想想之前所用的排序算法实在是太小儿科了。当时课本上最先进的2路插入排序也是O(n^2)的复杂度,当时还觉得特别麻烦。就是想的太多,做的太少。
在这里所列出来的排序算法都是内部排序算法,并且不包括
我的github:
我实现的代码全部贴在我的github中,欢迎大家去参观。
https://github.com/YinWenAtBIT
插入排序:
思想:
从小到大:
插入第P个数字,假设前面第0至第P-1个数已经是按照从小到大排序。
那么只要找到第P个数字在前P-1个数字中的位置即可。
实现的方式是从P-1个数字的最后一个数字开始寻找,如果P小于最后一个数字,则最后一个数字往后挪一位。
直到找到P的位置,再讲P放入空出来的位置上。
复杂度与稳定性
由于嵌套循环的每一个都要发生N次迭代,因此时间复杂度为O(N^2)。最好的情况下,如果输入数据已经是预排序的,那么每一次循环只需要一次对比,那么复杂度是O(N)。插入排序是稳定的排序。
编码实现:
/*插入排序*/ void InsertionSort(ElementType A[], int N) { int i,j; ElementType temp; for(i=1; i<N; i++) { temp = A[i]; for(j=i; j>0; j--) { if(temp <A[j-1]) A[j] = A[j-1]; else break; } A[j] = temp; } }
希尔排序:
思想:
从小到大:
希尔排序的原理与插入排序基本相同,不同之处在于希尔排序使用的比较间隔不同与插入排序,插入排序只使用1作为比较间隔。
希尔排序的比较间隔从大到小,最后为1。
希尔排序的比较间隔有许多的设置方法,发明人使用的为1,2,4,8这样两倍的间隔。现在最好的间隔为Sedgewick提出的增量序列1,5,9,41,109这样的序列。在这里我的实现就使用的这样的序列。
复杂度与稳定性
使用希尔排序最坏情况为O(N^2),Sedgewick的增量序列的下界为O(N^(4/3)),平均时间为O(N^(7/6))。希尔排序不是稳定的排序。
编码实现:
/*希尔排序*/ void ShellSort(ElementType A[], int N) { /*Sedgewick序列*/ int Sedgewick[5] = {109, 41, 19, 5, 1}; int i, j; int Index, Increament; ElementType temp; for(Index=0; Index<5; Index++) if(Sedgewick[Index] < N) break; if(Index == 5) return; for(; Index<5; Index++) { Increament = Sedgewick[Index]; for(i=Increament; i<N; i++) { temp = A[i]; for(j = i; j>Increament-1; j--) { if(temp < A[j-Increament]) A[j] = A[j-Increament]; else break; } A[j] = temp; } } }
堆排序:
思想:
从小到大:
将数据构建成二叉堆,然后依次删除最小值,就可以得到排序的结果。缺点是需要使用额外的空间。
复杂度与稳定性
构建堆花费时间O(N),每次删除使用O(LogN)时间。一共N次,所以时间复杂度为O(N LogN)。堆排序也是不稳定排序。
编码实现:
/*堆排序,直接调用了之前编写的二叉堆代码*/ void HeapSort(ElementType A[], int N) { int i; PriorityQueue H = BuildHeap(A, N); for(i=0; i<N; i++) A[i] = DeleteMin(H); Destroy(H); }
归并排序:
思想:
从小到大:
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。首先考虑下如何将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在新的数列里放入这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。
解决了上面的合并有序数列问题,再来看归并排序,其的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。如何让这二组组内数据有序了?
可以将A,B组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。
复杂度与稳定性
它的最坏情况下时间复杂度为O(N LogN),属于速度很快的排序了,但是缺点是也需要一个同样大小的辅助数组。归并排序是稳定的排序算法。
编码实现: 在这里我把归并排序分成了3个部分,启动部分建立一个辅助的数组,然后启动排序的真正部分Msort,并且提取出了Merge函数。/*归并排序*/ void Merge(ElementType A[], ElementType TmpArray[], int Lpos, int Rpos, int RightEnd) { int i, LeftEnd, Num, TmpPos; LeftEnd = Rpos -1; Num = RightEnd - Lpos +1; TmpPos = Lpos; while(Lpos <= LeftEnd && Rpos <= RightEnd) { if(A[Lpos] < A[Rpos]) TmpArray[TmpPos++] = A[Lpos++]; else TmpArray[TmpPos++] = A[Rpos++]; } /*复制剩下的数据*/ while(Lpos <= LeftEnd) TmpArray[TmpPos++] = A[Lpos++]; while(Rpos <= RightEnd) TmpArray[TmpPos++] = A[Rpos++]; /*拷贝回原来的数组*/ for(i =0; i<Num; i++, RightEnd--) { A[RightEnd] = TmpArray[RightEnd]; } } void Msort(ElementType A[], ElementType TmpArray[], int Left, int Right) { int Center; if(Left < Right) { Center = (Left+Right)/2; Msort(A, TmpArray, Left, Center); Msort(A, TmpArray, Center+1, Right); Merge(A, TmpArray, Left, Center+1, Right); } } void MergeSort(ElementType A[], int N) { ElementType * TmpArray = (ElementType *)malloc(N*sizeof(ElementType)); if(TmpArray == NULL) { fprintf(stderr, "not enough memory\n"); exit(1); } Msort(A, TmpArray, 0, N-1); free(TmpArray); }
快速排序:
思想:
从小到大:
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。
该方法的基本思想是:
1.先从数列中取出一个数作为基准数。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数(这一步可以提前停止,在只有3-5个数的时候使用插入排序更快)。
虽然算法描述起来挺简单的,但是里面有许许多多的细节,要解决这些坑,还是得花不少的功夫。
1.选择哪一个数作为基准
书上提供的方法我觉得基本上时最优了。为了避免遇上已经排序的序列,选择第一个数是肯定不可取的(这样导致所有的数据都分在一边,另一边没有数,时间复杂度变成O(N^2))。然后直接选择中间的数?这样同样难免遇上最大值或者最小值。由于在选择哪一个数作为基准并不花费太多的功夫,并且可以大大改善算法时间的稳定性,所以在这里可以选择复杂一点的做法。
书上提供的做法是,提取第一个,最后一个,已经正中间的数,让它们从小到大排列,然后选择中间的数作为基准。然后,把中间的基准数和倒数第二个数交换,这样做的目的是为了第二步的方便。接下来的叙述就会说明这一点。
2.如何交换:
交换的方式为:
使用两个指针,一个指向数组开始处,另一个指向末尾,然后两个指针开始运动,指向开头的指针在遇到比基准大的数时停下来。指向末尾的指针在遇到比基准小的数停下来,然后交换所指的数,重复这个过程,直到最初指向末尾的指针跑到了指向开始指针的前面为止。这样就完成了将数据划分两部分的工作。最后再把基准(倒数第二个数)与指向开头指针所指的数交换(这个数大于基准,它前面的数都小于基准)。这样基准就处于中间。然后可以对左右两部分再进行快排。这里把基准放在末尾的原因是,作为指向开头指针的提醒处,避免开头指针一直滑动以至于超过数组界限。(这里对于等于基准的数,两个指针都停下来)。
复杂度与稳定性
它的最坏情况下时间复杂度为O(N^2),但是优化了选择基准的方式,一般难以遇上。平均速度为O(N LogN)属于速度很快的排序了,并且不需要额外的空间。快速排序是不稳定的排序算法。
编码实现: 在这里我把快速排序分成了4个部分,启动部分启动快排,然后选择基准部分提取出来作为单独的函数,交换作为单独的函数。以及最为核心的滑动交换,并且对子数组进行快排作为一部分。/*快速排序*/ void Swap(ElementType *A, ElementType *B) { ElementType temp; temp = *A; *A = *B; *B = temp; } ElementType Median3(ElementType A[], int Left, int Right) { int Center = (Left + Right) / 2; /*从左到右,从小到大排列*/ if(A[Left] > A[Center]) Swap(&A[Left], &A[Center]); if(A[Left] > A[Right]) Swap(&A[Left], &A[Right]); if(A[Center] > A[Right]) Swap(&A[Center], &A[Right]); Swap(&A[Center], &A[Right-1]); return A[Right-1]; } #define Cutoff (3) void Qsort(ElementType A[], int Left, int Right) { int i,j; ElementType Pivot; Pivot = Median3(A, Left, Right); if(Left + Cutoff <Right) { i = Left; j = Right-1; while(1) { while(A[++i] < Pivot); while(A[--j] > Pivot); if(i < j) Swap(&A[i], &A[j]); else break; } /*将枢纽元放回中间,此时枢纽元左边的数据都比它小 右边的数据都比它大,再对左右数据排序即可*/ Swap(&A[i], &A[Right-1]); Qsort(A, Left, i-1); Qsort(A, i+1, Right); } else /*少于3个数据就直接使用插入排序更快*/ InsertionSort(A+Left, Right-Left+1); } void QuickSort(ElementType A[], int N) { Qsort(A, 0, N-1); }
总结:
排序算法我是提前学习了,隔了两个星期之后才来亲手实现。最基本的排序算法,插入,希尔,以及堆排序,由于方法简单,没有再参考课本就写了出来。归并排序稍微复习了一下原理以及伪代码,也写了出来。最后的快排真是一点都写不出来了。因为它的思想虽然简单,实现上的细节确实不容易,只能在重新学习一遍之后,才写出了快排到算法。
核心还是要理解这些算法,只有在理解了算法之后,才能做到自己想什么时候写出来就能写出来。而不是简单的默写。