前言:
最近在上数据结构实验课,要求用数组实现一个链表,在做顺序合并的时候突然想起了归并排序也是那样做的;索性这次就来介绍七种排序中的后两种——归并排序和快速排序。我们先了解下它们的原理吧~~ 惯例还是先来一个GitHub传送门:
JZX555的GitHub~~
原理:
分治算法:
不论是对于归并排序还是快速排序,
核心思想都是五大基本算法中的分治算法——将一个大的问题分解成两个或更多子问题,在将子问题分解直到可以直接得出结果。在排序中,我们使用分治算法对数组进行排序,将大数组进行排序即将其分为两个小数组进行排序,一直分解直到只有一个元素或两个元素,可以直接进行比较排出顺序,在将结果进行合并!
归并排序:
如果我们得到了两个有序的数组怎么把它合并成一个有序的大数组呢?答案很简单,用三个下标即可做到,他们分别为数组1的下标A,数组2下标B,大数组下标C;我们比较A和B对应元素大小,将A(假设A<B,且数组为升序)的赋值给C对应下标,同时A和C加一,然后重复该步骤,直到所有元素都合并完成!
那么对于一个数组的排序,如果一个数组从某个下标开始,两边都是各自有序的,我们是不是也可以用这种方式来排序呢?那时肯定的,思想和之前大致相似,有些细节不同; 代码如下:
void Merge(int A[], int tmpArray[], int LPos, int RPos, int REnd) {
// LEnd:储存左数组的截止位置
// NumElements:储存元素总个数
// tmpPos:储存中间数组的位置
int LEnd, NumElements, tmpPos;
// 获取其对应的值
LEnd = RPos - 1;
tmpPos = LPos;
NumElements = REnd - LPos + 1;
// 将有序的两个数组进行合并,直到其中一个合并完成
while(LPos <= LEnd && RPos <= REnd) {
if(A[LPos] < A[RPos])
tmpArray[tmpPos++] = A[LPos++];
else
tmpArray[tmpPos++] = A[RPos++];
}
// 合并完成另外一个
while(LPos <= LEnd)
tmpArray[tmpPos++] = A[LPos++];
while(RPos <= REnd)
tmpArray[tmpPos++] = A[RPos++];
// 将中间数组的数据返回给目标数组
for(int i = 0; i < NumElements; i++, REnd--)
A[REnd] = tmpArray[REnd];
}
其中tmpArray是一个中间数组用来暂时储存排序结果; OK,现在我们知道了如果一个数组分两半有序该怎么排序,那么当我们实际对一个数组进行排序的时候不可能每次都那么巧合都是那种情况,那怎么办?答案也很显然——让它变成那样不就完了;我们想让一个完全无序的数组变为分两半有序,就要将其分为两半进行排序!这不是一个循环问题了吗?也就是说我们需要一直对子数组进行排序,那么这就是分治算法的
分——通过递归解决较小的问题!终止条件很明显,应该就是数组只剩一个元素的时候,应为这是数组一定是有序的!(无论升序还是降序~~)那么终止之后我们该做的就是分治中的
治——将子问题构建成原问题的接。这也很简单,就是我们前面提及的将两个有序的数组合并成一个大的有序数组,最后我们就可以得到一个排好序的目标数组了!
void MSort(int A[], int tmpArray[], int Left, int Right) {
// 使用递归的方式对数组进行分治排序
// 一直分治递归到重合为止
if(Left < Right) {
int Center = (Left + Right) / 2;
MSort(A, tmpArray, Left, Center); // 对左半部分进行递归排序
MSort(A, tmpArray, Center + 1, Right); // 对右半部分进行递归排序
// 特别说明:
// 因为 Merge的调用在 MSort最后,所以整个排序过程中只
// 会有一个程序在调用 tmpArray,避免了程序出错,同时也允许
// 我们只申请一次内存,可以避免申请内存占用大量的时间!
Merge(A, tmpArray, Left, Center + 1, Right); // 将排序后的部分合并
}
}
void MergeSort(int A[], int N) {
int *tmpArray;
// 申请中间数组
tmpArray = new int[N];
if(tmpArray == NULL)
cout << "归并排序失败!" << endl;
else {
// 将目标数组进行分治排序
// 并用 tmpArray暂时储存中间结果
MSort(A, tmpArray, 0, N - 1);
// 删除中间数组
delete tmpArray;
tmpArray = NULL;
}
}
这里我们没有直接写成一个程序是因为有tmpArray的存在,如果只有一个MergeSort,那么我们就会
多次申请tmpArray,那么会占用大量的时间,不符合我们所追求的O(N logN)的最坏运行时间。
快速排序:
快速排序正如同他的名称一样,是在实践中最快的已知排序算法,他的平均运行时间是O(N logN)。将数组S进行排序时,我们也使用了分治思想,将一个数组的排序分为更小数组的排序来进行。同时我们也将QuickSort(S)其分为四步进行: 1.如果S中元素个数为0或1则返回;
2.取S中的任一元素v,我们称之为枢纽元(pivot); 3.将S中除了v以外的元素分为两组A,B,A中的元素小于v,B中的元素大于v;
4.将S进行重新组合={QuickSort(A), v , QuickSort(B)}; 可以看出,快速排序和归并排序类似,都是将问题分——步骤1、2、3;再将其治——步骤4;通过这个递归条件,我们也可以很轻松的写出快速牌排序的代码了;不过在这之前我们还有一个重要的问题要处理,那就是
解决枢纽元的选取!可能有人会说,枢纽元直接选取中位数不久完了吗?能选到当然是好的,但是既然我们是排序,那么S很有可能就是一个无序的数组,那么我们怎么找中位数呢?直接选取中间吗?那也很有可能选到最大后最小值,那么快速排序的时间复杂度将会很高。这里提供了一种
中值法来选取枢纽元——选取数组开始、末尾以及中间的元素进行比较,其中大小为中间的数被选取为枢纽元。 中值法代码如下:
int Median3(int A[], int Left, int Right) {
int Center = (Left + Right) / 2; // 储存范围的中值
// 进行边界值与中值的比较与替换
// 保证: A[Left] <= A[Center] <= A[Right]
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]);
// 将中值与右边界 - 1 替换,因为A[Right]以及保证大于中值
// 同时可以对中值进行隐藏
Swap(A[Center], A[Right - 1]);
// 返回中值
return A[Right - 1];
}
其中Swap(a, b)是交换函数,用于交换a,b的值;在上述的代码中我们可以发现,其实在我们选取中值的时候就已经开始进行大小的选择了。 选取了中值后,快速排序的剩余工作就很简单了,就是将其分为小问题然后解决,然后构建。 代码如下:
void QSort(int A[], int Left, int Right) {
int Pivot; // 储存枢纽元
// 判断是否需要进行分治递归排序
if(Left + Cutoff <= Right) {
Pivot = Median3(A, Left, Right); // 获取枢纽元
int i = Left, j = Right - 1;
while(true) {
// 越过满足大小需求的数
// 注意一种会出错的情况:
// while(A[i] < Pivot) i++;
// while(A[j] > Pivot) j--;
// 会出现错误,因为当A[i] = A[j] = Pivot时,会无限循环
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(int A[], int N) {
QSort(A, 0, N - 1); // 对该数组全范围进行排序
}
在快速排序中,我们发现在QSort()中有InsertionSort()函数,这是插入排序;也是其中排序中的一种;因为实践中,元素较少时快速排序并不会比插入排序这种O(n * n)的排序方法有优势,因此当元素少时,我们直接进行插入排序。
C++实现:
首先是归并排序的C++代码:
/* 合并函数:将已经排好序的两个数组合并成一个有序的数组
* 返回值:无
* 参数:A[]:用于排序的数组;tmpArray:用于暂时储存合并结果的数组;LPos:左数组的起始位置;RPos:右数组的起始位置;REnd:右数组的截止位置
*/
void Merge(int A[], int tmpArray[], int LPos, int RPos, int REnd) {
// LEnd:储存左数组的截止位置
// NumElements:储存元素总个数
// tmpPos:储存中间数组的位置
int LEnd, NumElements, tmpPos;
// 获取其对应的值
LEnd = RPos - 1;
tmpPos = LPos;
NumElements = REnd - LPos + 1;
// 将有序的两个数组进行合并,直到其中一个合并完成
while(LPos <= LEnd && RPos <= REnd) {
if(A[LPos] < A[RPos])
tmpArray[tmpPos++] = A[LPos++];
else
tmpArray[tmpPos++] = A[RPos++];
}
// 合并完成另外一个
while(LPos <= LEnd)
tmpArray[tmpPos++] = A[LPos++];
while(RPos <= REnd)
tmpArray[tmpPos++] = A[RPos++];
// 将中间数组的数据返回给目标数组
for(int i = 0; i < NumElements; i++, REnd--)
A[REnd] = tmpArray[REnd];
}
/* 驱动函数:驱动 MergeSort功能进行
* 返回值:无
* 参数:A[]:想要进行排序的数组;tmpArray:用于暂时储存合并结果的数组;Left:进行排序部分的左界;Right:进行排序部分的右界
*/
void MSort(int A[], int tmpArray[], int Left, int Right) {
// 使用递归的方式对数组进行分治排序
// 一直分治递归到重合为止
if(Left < Right) {
int Center = (Left + Right) / 2;
MSort(A, tmpArray, Left, Center); // 对左半部分进行递归排序
MSort(A, tmpArray, Center + 1, Right); // 对右半部分进行递归排序
// 特别说明:
// 因为 Merge的调用在 MSort最后,所以整个排序过程中只
// 会有一个程序在调用 tmpArray,避免了程序出错,同时也允许
// 我们只申请一次内存,可以避免申请内存占用大量的时间!
Merge(A, tmpArray, Left, Center + 1, Right); // 将排序后的部分合并
}
}
/* 归并排序:将目标数组进行归并排序
* 返回值:无
* 参数:A[]:想要进行排序的数组;N:数组中元素的个数
*/
void MergeSort(int A[], int N) {
int *tmpArray;
// 申请中间数组
tmpArray = new int[N];
if(tmpArray == NULL)
cout << "归并排序失败!" << endl;
else {
// 将目标数组进行分治排序
// 并用 tmpArray暂时储存中间结果
MSort(A, tmpArray, 0, N - 1);
// 删除中间数组
delete tmpArray;
tmpArray = NULL;
}
}
接下来是快速排序的C++实现:(附带插入排序)
#define Cutoff 3
/* 交换函数:交换两个变量的值
* 返回值:无
* 参数:x:想要交换的参数1;y:想要交换的参数2
*/
void Swap(int &x,int &y) {
int tmp = x;
x = y;
y = tmp;
}
/* 插入排序:对数组进行插入排序(升序)
* 返回值:无
* 参数:A[]:想要进行排序的数组;N:数组中元素的个数
*/
void InsertionSort(int A[], int N) {
int tmp;
for(int i = 1; i < N; i++) {
int j;
tmp = A[i];
for(j = i; j > 0 && A[j - 1] > tmp; j--)
A[j] = A[j - 1];
A[j] = tmp;
}
}
/* 枢纽元函数:选取数组中的枢纽元
* 返回值:int:该数组选取范围内的枢纽元
* 参数:A[]:想要选取枢纽元的数组;Left:选取范围的左边界;Rgiht:选取范围的右边界
*/
int Median3(int A[], int Left, int Right) {
int Center = (Left + Right) / 2; // 储存范围的中值
// 进行边界值与中值的比较与替换
// 保证: A[Left] <= A[Center] <= A[Right]
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]);
// 将中值与右边界 - 1 替换,因为A[Right]以及保证大于中值
// 同时可以对中值进行隐藏
Swap(A[Center], A[Right - 1]);
// 返回中值
return A[Right - 1];
}
/* 驱动函数:对快速排序进行驱动
* 返回值:无
* 参数:A[]:想要进行排序的数组;Left:排序范围的左边界;Right:排序范围的右边界
*/
void QSort(int A[], int Left, int Right) {
int Pivot; // 储存枢纽元
// 判断是否需要进行分治递归排序
if(Left + Cutoff <= Right) {
Pivot = Median3(A, Left, Right); // 获取枢纽元
int i = Left, j = Right - 1;
while(true) {
// 越过满足大小需求的数
// 注意一种会出错的情况:
// while(A[i] < Pivot) i++;
// while(A[j] > Pivot) j--;
// 会出现错误,因为当A[i] = A[j] = Pivot时,会无限循环
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);
}
/* 快速排序:对目标数组进行快速排序(升序)
* 返回值:无
* 参数:A[]:向要进行排序的数组;N:数组中元素的个数
*/
void QuickSort(int A[], int N) {
QSort(A, 0, N - 1); // 对该数组全范围进行排序
}
那么这次的博客到这里也就结束啦!大家有什么问题或者意见也可以提出来,大家一起讨论提高!
参考文献:《数据结构与算法分析——C语言描述》 转载请注明出处~~