归并排序
介绍
有时候我们需要把两个有序的序列进行合并从而得到有序的大序列,基于这个思想,便引出了归并排序,其思路就是把一个无序序列拆分成多个有序序列(当序列的元素为1时就是有序序列),然后对所有序列进行合并操作,最终得到一整个有序序列。
实现
cpp
typedef int ElementType;
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++];
}
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) {
Msort(A,TmpArray,0,N-1);
free(TmpArray);
}else {
printf("Not enough memory for Merge Sort\n");
}
}
算法有两个重要的操作,一个是将大序列分成小序列的操作,一个是将两个有序序列合并成有序大序列的操作。
合并操作的逻辑:假设要合并序列A和序列B(都是升序),首先创建一个空间大小为A和B元素数量之和的大小。然后比较A和B最左边的元素,较小的放新序列里,较大的那个和另一个序列的下一个元素比,持续这个过程直到其中一个序列遍历完,那就把另一个序列剩下的元素放到新序列里就可以了。
递归拆分的逻辑:将数组从中间拆成左右两部分,再对左右两部分再以中间拆成两部分,一直这样拆到小序列只要一个元素,然后再对这两个小序列进行合并操作,一步一步合并直至左右两大部分的序列进行最后一次合并成为最终完成排序的总序列。
这里开了一个临时变量TmpArray为了方便后面进行合并操作时频繁新辟用于存两个序列的新数组。
分析
归并操作包括拆分和合并,将数组拆分成许多小数组的的过程一共要进行LogN次,每一层的合并最坏要比较N次,所以总的时间复杂度为O(LogN)。
归并排序是稳定排序,因为每一次的拆分都是相邻序列的拆分,合并时若两元素相等,也是先将左数组的元素放入临时数组,这不改变相同元素的相对位置。
快速排序
介绍
当得到一个无序数组,那我们随机选其中一个元素,然后让剩下的元素中小于它的排在它左边,大于它的元素排在它右边,这样我们就让这个元素归位了,它所在的位置不会在变了。接着再对剩下的元素重复进行这样的操作,这样随着一个个元素的归位,序也就排好了。
实现
cpp
typedef int ElementType;
#define Cutoff (3)
void Swap(ElementType *a,ElementType *b) {
ElementType Tmp=*a;
*a=*b;
*b=Tmp;
}
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;
}
}
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];
}
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);
}
这里的排序操作比较灵活,算法步骤如下:
为了提高算法效率防止出现快速排序最坏的情况(避免选到极值做枢轴),先找到待排序数组的左端点,中点和右端点这三个位置,然后对这三个位置进行排序,排好序后将中点的元素和Right-1处的元素进行交换并且将其作为枢轴(枢轴的左边所有元素小于枢轴本身,枢轴的右边所有元素大于枢轴本身)。
快速排序的核心操作(此操作没有新辟一块空间来存储经交换后快速排序的数组,所以空间复杂度较低):使用双指针i,j分别从数组最左端和Right-2两次开始遍历,先i右移直到找比枢轴大的元素后,再j左移直到找到比枢轴小的元素,这样如果顺利的话会出现i位置的元素小于j位置的元素,那就交换这两个元素,然后i,j指针接着移动重复上面的操作,若出现i的元素大于等于j位置的元素,说明i,j指针已经不在正确的左右区间了,即此时i的位置的元素是第一个大于枢轴的元素的位置,那就将枢轴和i位置进行交换,这样枢轴的左侧全是小于它的元素,枢轴右侧都是大于它的元素。
接着再对枢轴左侧的子数组(left到i-1)和右侧数组(i+1到Right)递归地进行快速排序的核心操作,直到某个小数组的长度小于3,数组长度太小用快速排序还是太烦琐了,使用就对数组长度小于3的小序列进行插入排序。这样就逐步完成了整个序列的排序
分析
和归并排序一样,选数组的操作及递归层数为LogN,每一层最坏要遍历N个元素,所以快速排序的时间复杂度是O(NLogN)。
快速排序是不稳定排序,双指交换元素时可能会打乱相同元素的相对位置。