归并排序与快速排序
本文最后更新于:2023年12月3日 晚上
阅前警告
本文内容可能高度抽象,因为这段时间实在太忙,只能大概写个大致想法,想起一个引导的作用,没想过如何组织文章。
归并排序
归并排序的基本思想:要将一个数组排序,可以先(递归地)将它分成两半分别排序,然后将结果归并起来。其中的“归并”其实也是排序,方法是:通过比较左右两边数组的元素来进行排序。
那么要如何实现这种归并呢?一种简单的办法是使用额外的空间,然后把不同的两个有序数组归并到这一块空间中。但是这样做并不明智,因为每次归并时都需要创建新数组来存储排序结果,所以使用原地归并会更好,即将归并后的结果放回原数组中。(但即便是原地归并,我们仍然需要使用一个和原数组一样大的辅助数组来帮助我们进行归并。)
1 | |
自顶向下的归并排序
分治思想,递归实现。Sort方法要做的事情就是将数组尽可能地切分,然后进行归并。
1 | |
优化方式:
- 对小规模数组使用插入排序
- 测试数组是否有序
- 不将元素复制到辅助数组
优化后的代码:这里最需要注意的点就是弄清楚两个数组的交换。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54public static new void Sort(T[] a)
{
aux = (T[])a.Clone();
ImprovedSort(aux, a, 0, a.Length - 1);
}
public static void ImprovedSort(T[] src, T[] dst, int lo, int hi)
{
if (hi <= lo + length)
{ // 对小规模子数组使用插入排序
for (int i = 1; i <= hi; i++)
{
for (int j = i; j > 0 && Less(src[j], src[j - 1]); j--)
{
Exch(src, j, j - 1);
}
}
return;
}
int mid = (lo + hi) / 2; // or: lo + (hi - lo) / 2
ImprovedSort(dst, src, lo, mid); // 将左半边排序
ImprovedSort(dst, src, mid + 1, hi); // 将右半边排序
if (!Less(src[mid + 1], src[mid])) // 如果 a[mid + 1] 大于等于 a[mid],说明数组是有序的(因为a[mid]为左半部分最大元素,a[mid + 1]为右半部分最小元素)
{
Array.Copy(src, lo, dst, lo, hi - lo + 1);
return;
}
ImprovedMerge(src, dst, lo, mid, hi);
}
private static void ImprovedMerge(T[] src, T[] dst, int lo, int mid, int hi)
{
int i = lo, j = mid + 1;
for (int k = lo; k <= hi; k++)
{
if (i > mid)
{ // 左边处理完毕了
dst[k] = src[j++]; // 处理右边剩下的元素
}
else if (j > hi)
{ // 右边处理完毕了
dst[k] = src[i++]; // 处理左边剩下的元素
}
else if (Less(src[i], src[j]))
{ // 两边都没处理完毕,且左边元素比右边元素小
dst[k] = src[i++]; // 把左边元素放回原数组中
}
else
{ // 两边都没处理完毕,且右边元素比左边元素小
dst[k] = src[j++]; // 把右边元素放回原数组中
}
}
}src是 source 的缩写,也就是源;dst是 destination 的缩写,也就是目标。最后排好序的是目标,不是源,别搞错了。
自底向上的归并排序
同样的,也是分治思想,但是迭代实现。先归并好所有的小数组,然后增大数组,再归并增大后的所有数组,继续增大,继续归并…如此继续下去直至归并完成。
但是需要注意的是最后一个数组的长度可能会小于我们设定的要进行归并的子数组的长度,所以写代码的时候不要忘记这点。
1 | |
快速排序
分治思想,递归实现。
基本思路:选择一个“锚点”(pivot),一般选择打乱后(打乱是为了避免最差情况)的数组中的第一个元素;基于这个锚点,用双指针分别从左右边界开始,扫描元素,左边直到找到一个比pivot大的元素或是越界后停下,右边直到找到一个比pivot小的元素或是越界后停下,检查左指针是否小于右指针,若是,交换两个元素,重复此行为至扫描结束;
扫描结束后,需要把基准元素放在正确的位置上,也就是数组开头元素跟右指针上的元素进行交换,因为扫描结束后右指针指向的元素是小于pivot的;现在,在pivot左边的元素都小于pivot,在pivot右边的元素都大于pivot了,之后,再根据这个右指针的值(也就是pivot的位置),将数组切分成左右两个子数组,再对左右子数组进行找pivot排序的操作,如此递归下去,最后就能得到排好序的原数组了。
1 | |
如何优化:
- 切换到插入排序:跟归并排序一样,对小数组使用插入排序。
- 三切分取样:使用子数组的一小部分元素的中位数来切分数组。
- 熵最优的排序:改进算法在一个元素全部重复的子数组下继续切分为更小数组的情况。
这里先讨论最后一个优化的实现方法。一个简单的想法是将数组切分为三部分,分别对应小于、等于和大于切分元素的数组元素,然后要排序的就是两边的元素,这样就能避免重复元素排序了。
这里介绍 Dijkstra 的解法:维护三个指针lf、i和gt,其中,lt使得a[lo...lt - 1]中的元素都小于pivot,gt使得a[gt + 1...hi]中的元素都大于pivot,i使得a[lt...i - 1]中的元素等于pivot,a[i...gt]中的元素还未确定。代码如下:
1 | |
然后是根据第一点和第二点改进的二路快速排序算法:
1 | |
最后是三点结合起来,也就是改进的三路快速排序算法:
1 | |
测试方法
尝试写一个从文本中读取待排序的内容的方法。本来想写好一点不过还是偷懒了,将就测试一下吧。
1 | |

希望本文章能够帮到您~