写在前面:
排序是计算机程序设计中的一种重要操作,它的功能是将一个数据元素的随意序列,又一次排列成一个按keyword有序的序列。因此排序掌握各种排序算法很重要。
对以下介绍的各个排序,我们假定全部排序的keyword都是整数、对传入函数的參数默认是已经检查好了的。仅仅是简单的描写叙述各个算法并给出了详细实现代码。并未做其它深究探讨。
基础知识:
因为待排序的记录数量不同,使得排序过程中设计的存储器不同,可将排序方法分为两大类:一类是内部排序,指的是待排序记录存放在计算机随机存储器中进行的排序过程。还有一类是外部排序,指的是待排序记录的数量非常大。以致内存一次不能容纳所有记录。在排序过程中尚需对外存进行訪问的排序过程。
在排序的过程中需进行下列两种基本操作:1、比較两个keyword的大小;2、将记录从一个位置移动至还有一个位置。
操作1对大多数排序方法来说都是必要的。而操作2能够通过改变记录的存储方式予以避免。
待排序记录序列可有下列3种存储方式:1、待排序的一组记录存放在地址连续的一组存储单元上。2、一组待排序记录存放在静态链表中,记录之间的次序关系由指针指示,则实现不须要移动记录,仅须要改动指针就可以。3、待排序记录本身存储在一组地址连续的存储单元内,同一时候另设一个指示各个记录存储位置的地址向量。在排序过程中不移动记录本身。而移动地址向量中这些记录的”地址“,在排序结束之后再依照地址向量中的值调整记录的存储位置。
算法分析:
1、插入排序:
基本思想:将一个记录插入到已排好序的有序表中。从而得到一个新的、记录数增1的有序表。
时间复杂度为O(n^2),若待排记录序列为正序,时间复杂度可提高至O(n);空间上仅仅须要一个记录的辅助空间。
a、直接插入排序
演示样例代码1:
void InsertionSort(ElementType A[], int N)
{
int j, P; ElementType Tmp;//记录辅助空间
for(P = 1; P < N; P++){
Tmp = A[P];
for(j = P; j > 0 && A[j - 1] > Tmp; j--)//将一个记录插入已排好序的有序表中
A[j] = A[j - 1];
A[j] = Tmp;
}
}
演示样例代码2:
void insertionsort(ElementType A[], int N)
{
for(int i = 1; i < N; i++){
int tmp = A[i]; //记录辅助空间
int j = i - 1;
while(j > -1 && A[j] > A[i]){
A[j+1] = A[j]; //将一个记录插入已排好序的有序表中
--j;
}
A[j+1] = tmp;
}
return;
}
插入排序算法简单,且easy实现。当待排序记录的数量n非常小时。这是一种非常好的排序方法。但n非常大时,则不宜採用直接排序。由于直接排序。基本的时间消耗在“比較”和“移动”上,因此。在直接排序的基础上,从降低“比較”和“移动”这两种操作的次数着眼,可得“折半插入排序”、“2-路插入排序”、“表插入排序”等。
b、折半插入排序
因为插入排序的基本操作是一个有序表中进行查找和插入,这个"查找"操作可利用"折半查找"来实现。由此进行的插入排序称之为折半插入排序。
2、希尔排序
基本思想:先将整个待排记录序列切割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。能够看出希尔排序事实上仅仅是改进了的插入排序,因此上面的插入排序也被称为直接插入排序。
特点:子序列的构成不是简单地“逐段切割”。而是将相隔某个“增量”的记录组成一个子序列。它通过比較相距一定间隔的元素来工作;各趟比較所用的距离随着算法的进行而减小,直到仅仅比較相邻元素的最后一趟排序为止。
演示样例代码
void Shellsort(ElementType A[], int N)
{
int i, j, Increment;
ElementType Tmp; for(Increment = N / 2; Increment > 0; Increment /= 2){
for(i = Increment; i < N; i++){
Tmp = A[i];
for(j = i; j >= Increment; j -= Increment){
if(Tmp < A[j - Increment])
A[j] = A[j - Increment];
else
break;
}
A[j] = Tmp;
}
}
}
上面给出的演示样例中选择的排序增量是使用shell建议的序列:N/2和Increment/2。
使用希尔增量时希尔排序的最坏情形执行时间为O(n^2)。
3、冒泡排序
基本思想:首先将第一个记录的keyword和第二个记录的keyword进行比較。若为逆序,则将两个记录交换之,然后比較第二个记录和第三个记录的keyword。依次类推,直至第n-1个记录和第n个记录的keyword进行过比較为止。上述过程称做第一趟冒泡排序,其结果使得keyword最大的记录被安置到最后一个记录的位置上。
然后进行第二趟冒泡排序。对前n-1个记录进行相同操作,其结果是使keyword次大的记录被安置到第n-1个记录的位置上。一般地。第i趟冒泡排序是从1到n-i+1依次比較相邻两个keyword,并在“逆序”时交换相邻记录,其结果是这n-i+1个记录中keyword最大的记录被交换到第n-i+1的位置上。判别冒泡排序结束的条件应该是“在一趟排序过程中没有进行过交换记录的操作”。
演示样例代码1:
void bubblesort(ElementType A[], int N)
{
int i, j;
ElementType tmp;
for(i = 0; i < N; i++)
{
for(j = 0; j < N-i; j++){
if(A[j] > A[j+1]){
tmp = A[j];
A[j] = A[j+1];
A[j+1] = tmp;
}
}
}
}
演示样例代码2:
void bubblesort(ElementType a[], int n)
{
int j;
bool flag;
ElementType tmp; flag = true;
while(flag){
flag = false;
for(j = 1; j < n; j++){
if(a[j-1] > a[j]){
tmp = a[j-1];
a[j-1] = a[j];
a[j] = tmp;
flag = true;
}
}
n--;
}
}
冒泡排序的时间复杂度为O(n^2)。
效率比較底下。当数据量比較小的时候,能够採用冒泡排序。
4、简单选择排序
基本思想:每一趟在n-i+1(i=1,2,…,n-1)个记录中选取keyword最小的记录作为有序序列中第i个记录。
直接选择排序和直接插入排序类似,都将数据分为有序区和无序区,所不同的是直接插入排序是将无序区的第一个元素直接插入到有序区以形成一个更大的有序区,而直接选择排序是从无序区选一个最小的元素直接放到有序区的最后。
演示样例代码:
void Selectsort(int a[], int n)
{
int i, j, nMinIndex, tmp;
for(i = 0; i < n; i++){
nMinIndex = i;
for(j = i + 1; j < n; j++)
if(a[j] < a[nMinIndex])
nMinIndex = j; tmp = a[i];
a[i] = a[nMinIndex];
a[nMinIndex] = tmp;
}
}
简单选择排序的时间复杂度是O(n^2)。
5、高速排序
基本思想:高速排序是对冒泡排序的一种改进。
它的基本思想是。通过一趟排序将待排记录切割成独立的两部分,当中一部分记录的keyword均比还有一部分记录的keyword小。则可分别对这两部分记录继续进行排序,以达到整个序列有效。
一趟高速排序的详细做法是:附设两个指针low和high,它们的初值分别为low和high。设枢轴记录的keyword为pivotkey,则首先从high所指位置起向前搜索找到第一个keyword小于prvotkey的记录和枢轴记录互相交换,然后从low所指位置起向后搜索,找到第一个keyword大于privotkey的记录和枢轴记录互相交换,反复这两步直至low=high为止。
演示样例代码1:
void Swap(ElementType *left, ElementType *right)
{
ElementType temp = *left;
*left = *right;
*right = temp;
} int Partition(ElementType A[], int low, int high)
{
ElementType pivotkey = A[low]; while(low < high){
while(low < high && A[high] >= pivotkey)
high--;
Swap(&A[low], &A[high]); while(low < high && A[low] <= pivotkey)
low++;
Swap(&A[low], &A[high]);
} return low;
} void QSort(ElementType A[], int low, int high)
{
int pivotloc; if(low < high){
pivotloc = Partition(A, low, high);
QSort(A, low, pivotloc - 1);
QSort(A, pivotloc + 1, high);
}
} void QuickSort(ElementType A[], int low, int high)
{
QSort(A, low, high);
}
高速排序的平均时间为O(n) = nlogn;它是眼下被觉得的最好的一种内部排序方法。
6、归并排序
基本思想:将两个或两个以上的有序表组合成一个新的有序表。2-路归并排序为例:如果初始序列含有n个记录,则可看成是n个有序的子序列,每一个子序列的长度为1,然后两两归并,得到n/2(或n/2+1)个长度为2或1的有序子序列;再两两归并,……如此反复。直至得到一个长度为n的有序序列为止。
演示样例代码:
void Merge(ElementType A[], ElementType TmpArray[], int Lpos, int Rpos, int RightEnd)
{
int i, LeftEnd, NumElements, TmpPos; LeftEnd = Rpos - 1;
TmpPos = Lpos;
NumElements = RightEnd - Lpos + 1; /*main loop*/
while(Lpos <= LeftEnd && Rpos <= RightEnd)
if(A[Lpos] <= A[Rpos])
TmpArray[TmpPos++] = A[Lpos++];
else
TmpArray[TmpPos++] = A[Rpos++]; while(Lpos <= LeftEnd) /*Copy rest of first half*/
TmpArray[TmpPos++] = A[Lpos++];
while(Rpos <= RightEnd) /*Copy rest of second half*/
TmpArray[TmpPos++] = A[Rpos++]; /*Copy TmpArray back*/
for(i = 0; i < NumElements; 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; TmpArray = (ElementType *)malloc(N*sizeof(ElementType));
if(TmpArray == NULL){
fprintf(stderr, "no space for tmp array!\n");
return;
} MSort(A, TmpArray, 0, N-1);
free(TmpArray); return; }
归并排序的效率比較高,设数列长为N。将数列分开成小数列一共要logN步,每步都是一个合并有序的数列的过程,时间复杂度记为O(N),因此时间复杂度是O(N*LogN)。
它非常难用于主存排序,主要问题在于合并两个排序的表须要线性附加内存。在整个算法中还要花费将数据复制到暂时数组再拷贝回来这样一些附加的工作,其结果严重放慢了排序的速度。
7、堆排序
堆是具有下列性质的全然二叉树:每一个节点的值都大于或等于其左右孩子节点的值,称为大顶堆;或者每一个节点的值都小于或等于其左右孩子节点的值,称为小顶堆。
堆排序就是利用堆进行排序的方法.基本思想是:将待排序的序列构造成一个大顶堆.此时,整个序列的最大值就是堆顶 的根结点.将它移走(事实上就是将其与堆数组的末尾元素交换, 此时末尾元素就是最大值),然后将剩余的n-1个序列又一次构造成一个堆,这样就会得到n个元素的次大值.如此重复运行,便能得到一个有序序列了。 时间复杂度为 O(nlogn),好于冒泡,简单选择,直接插入的O(n^2)
该算法的主要问题在于它使用了一个附加的数组。因此,存储需求添加一倍。注意:将第二个数组拷贝回第一个数组的额外时间消耗仅仅是O(N),这不可能显著影响执行时间。这个问题是空间的问题。
演示样例代码:
#define LeftChild(i) (2*(i) + 1) void Swap(ElementType *pa, ElementType *pb)
{
ElementType *pc = pa;
pa = pb;
pb = pc;
} void PercDown(ElementType A[], int i, int N)
{
int Child;
ElementType Tmp; for(Tmp = A[i]; LeftChild(i) < N; i = Child){
Child = LeftChild(i);
if(Child != N-1 && A[Child + 1] > A[Child])
Child++;
if(Tmp < A[Child])
A[i] = A[Child];
else
break;
}
A[i] = Tmp;
} void Heapsort(ElementType A[], int N)
{
int i; for(i = N/2; i >= 0; i--) /*BuildHeap*/
PercDown(A, i, N);
for(i = N - 1; i > 0; i--){
Swap(&A[0], &A[i]); /*DeleteMax*/
PercDown(A, 0, i);
}
}
总结:
上面尽管给了7种内部排序的方法。可简单的对它们大致分为下面几类:插入排序(直接插入排序、希尔排序)、高速排序(冒泡排序、高速排序)、选择排序(简单选择排序、堆排序)、归并排序和基数排序。
各内部排序方法的比較:
1、平均时间性能而言,高速排序最佳,其所需时间最省。但高速排序的最坏情况下的时间性能不如堆排序和归并排序。而后两者相比較的结果是,在n较大时,归并排序所需时间较堆排序省,但它所需的辅助存储量最多。
2、简单排序包含除希尔排序之外的全部插入排序,冒泡排序和简单选择排序。当中以直接插入排序为最简单。当序列中的记录"基本有序"或n值较小时,它是最佳的排序方法。因此常将它和其它的排序方法,诸如高速排序、归并排序等结合在一起使用。
3、基数排序的实际复杂度可写成O(d*n)。它最适用于n值非常大而keyword较小的序列。
若keyword也非常大,而序列中大多数记录的"最高位keyword"均不同。则亦可先按"最高位keyword"不同将序列分成若干"小"的子序列,而后进行直接插入排序。
4、从方法的稳定性来比較,基数排序是稳定的内排方法,全部时间复杂度为O(n^2)的简单排序法也是稳定的。然而。高速排序、堆排序和希尔排序时性能更好的排序方法是不稳定。
版权声明:本文博客原创文章,博客,未经同意,不得转载。