前言:
排序算法在本科的时候就学习过冒泡法,也没有想过如何去计算算法的复杂度,现在想想之前所用的排序算法实在是太小儿科了。当时课本上最先进的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);
}
总结:
排序算法我是提前学习了,隔了两个星期之后才来亲手实现。最基本的排序算法,插入,希尔,以及堆排序,由于方法简单,没有再参考课本就写了出来。归并排序稍微复习了一下原理以及伪代码,也写了出来。最后的快排真是一点都写不出来了。因为它的思想虽然简单,实现上的细节确实不容易,只能在重新学习一遍之后,才写出了快排到算法。
核心还是要理解这些算法,只有在理解了算法之后,才能做到自己想什么时候写出来就能写出来。而不是简单的默写。