归并排序
归并排序以O(NlogN)最坏运行时间运行,而所使用的比较次数几乎是最优的,它是递归算法一个很好的实例。
这个算法中基本的操作是合并两个已排序的表。因为这两个表示已排序的,所以若将输出放到第三个表中时则该算法可以通过对输入数据一趟排序来完成。基本的合并算法是取两个输入数组A和B,一个输出数组C,以及三个计数器Aptr,Bptr,Cptr,它们初始置于对应数组的开始端。A[Aptr],B[Bptr]中的较小者被拷贝到C中的下一个位置,相关的计数器向前推进一步,当两个输入表有一个用完的时候,则将另一个表中剩余部分拷贝到C中。
归并排序算法分析:假设N是2的幂,对于N=1,归并排序所用的时间为常数,即为1,。否则,对N个数归并排序的用时等于完成两个大小为N/2的递归排序所用的时间再加上合并的时间,它是线性的。下述方程给出准确的表示:
T(1)=1; T(N)= 2T(N / 2) + N;
借上述方程,我们可以得到:T(N) = NlogN + N = O(NlogN)。
虽然归并排序的运行时间是O(NlogN),但是它很难用于主存排序,主要问题在于合并两个排序的表需要线性附加内存,在整个算法中还要花费将数据拷贝到临时数组再拷贝回来这样一些匣的工作,其结果严总放慢了排序的速度,所以对于重要的内部排序应用而言,一般还是选择快速排序。以下是归并排序的实现代码:
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){
cout << "No space for tmp array!!!" << endl;
}
else{
MSort(A, TmpArray, 0, N - 1);
}
}
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;
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++];
//将TmpArray已经拍过序后的元素拷贝回原数组
for (i = 0; i < NumElements; ++i, RightEnd--)
A[RightEnd] = TmpArray[RightEnd];
}
快速排序
快速排序是实践中最快的已知排序算法,它的平均运行时间是O(NlogN)。该算法之所以特别快,主要是由于非常精炼和高度优化的内部循环。它的最坏情形的性能为O(N^2),但稍加努力就可避免这种情形。快速排序也是一种分治的递归算法。将数组S快速排序由一下简单四步组成:
(1) 如果S中的元素个数是0或1,返回
(2) 去S中任一元素v,称之为枢纽元
(3) 将S-{v}(S中的其余元素)分成两个不相交的集合:S1和S2,S1包含小于v的所有元素,S2则包含大于v的所有元素。
(4) 分别对这两个数组进行快速排序。
选取枢纽元:三数中值分割法
一组N个数的中值是第N/2个最大的数,枢纽元的最好选择是数组的中值。但这很难算出,且明显减慢算法的速度。这样的中值可以随机选取三个元素并用他们的中值作为枢纽元而得到。因此选取枢纽元一般的做法是使用左端A[Left],右端A[Right]和中心位置A[Center]上的三个元素的中值作为枢纽元。这种选取方法可以消除预排序输入的坏情形。这种方法还有额外的好处,即该三元素中的最小者被分在A[Left],三元素中最大者被分在A[Right],这是正确的位置,因为它大于枢纽元,因此,我们可以把枢纽元放到A[Right-1]并在分割阶段将I,j初始化为Left+1和Right-2.因为A[Left]比枢纽元小,所以将它用作j的警戒标记,这是另一好处。
具体实现代码如下:
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;
if (Left + Cutoff <= Right){
Pivot = Median3(A, Left, Right);
i = Left; j = Right - 1;
for (;;){
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
InsertionSort(A + Left, Right - Left + 1);
}
void QuickSort(ElementType A[], int N)
{
QSort(A, 0, N -1);
}
对于很小的数组(N<=20),快速排序不如插入排序好,因为快速排序是采用递归,对于函数的调用和返回都会花费一定时间,所以对于小数组,采用插入排序较好。因此,上述代码中,对子数组采用的是插入排序。