归并排序与快速排序都运用了分治(divide-and-conquer)的思想,将一整段序列的排序划分为多段子序列的排序问题。这两个算法的划分法都是二分,最后可以达到 O(nlogn) 的效率(本文中的效率与复杂度取的都是平均值)。

归并排序

归并

首先要了解一下归并算法:给出两个升序序列,将其合并成一个新的升序序列,要怎么做?

如果将其简单组合再进行排序,未免太低效。我们可以利用其本身就升序的特性,每次先从两个序列头部取出较小的值,置于新序列的头部,并重复该步骤,直到两原序列都为空,算法结束,时间复杂度为 O(n)

归并排序

归并排序就是用了归并的思想:将一个序列分成两段,这两段 分别有序 ,那就可以对其进行归并,得到完整的有序数组。那么要怎么保证这两段序列分别有序呢?很简单,对其分别递归执行归并排序就可以了。由于这样分块执行的次数为 logn (即log(2)(n))次,所以完整的归并排序时间复杂度为 O(nlogn)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//归并排序 O(nlogn)
template <class Type>
void MergeSort(Type arr[], int n) {
Type* temp = (Type*)malloc(n * sizeof(Type));
MergeSort_rec(0, n - 1, arr, temp);
}
template <class Type>
void MergeSort_rec(int st, int ed, Type arr[], Type temp[]) {
if (st < ed) {
int mid = st + (ed - st) / 2; //不用mid=(st+ed)/2:防溢出
MergeSort_rec(st, mid, arr, temp); //递归调用
MergeSort_rec(mid + 1, ed, arr, temp);
int i = st, j = mid + 1, k = st; //i在[st,mid], j在[mid+1,ed], k在[st,ed]移动
while (i <= mid && j <= ed) temp[k++] = arr[i] < arr[j] ? arr[i++] : arr[j++]; //归并
while (i <= mid) temp[k++] = arr[i++];
while (j <= ed) temp[k++] = arr[j++];
for (i = st; i <= ed; i++) arr[i] = temp[i];
}
}

归并排序

快速排序

快速排序的基本思路是:先取出一个 基准值 pivot,然后把所有小于它的值放在它左边,大于它的值放在它右边,接着分别对左边和右边的序列重复递归执行该过程,最终使得序列有序。

我主要介绍一下单次执行中如何把小于基准值的值置左,大于的置右:首先我们取序列第一位为基准值,然后有两个箭头,分别指向序列的最左边与最右边(除去基准值的部分)。首先是右指针向左移动,直到找到一个小于基准值的数,将其置于基准值原本的位置,接着是左指针向右移动,找到一个大于基准值的数,将其置于上一个数原本的位置(即右指针当前指向位置)。然后右指针左移,找数,填坑;左指针右移,找数,填坑……直到左右指针相遇,便将基准值填入最后这个坑。一轮算法结束:

由于平均情况下需要 logn 次递归执行,而每次执行又只需遍历一遍序列,所以其时间复杂度也为 O(nlogn)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//快速排序 O(nlogn)
template <class Type>
void QuickSort(Type arr[], int n) {
QuickSort_rec(0, n - 1, arr);
}
template <class Type>
void QuickSort_rec(int st, int ed, Type arr[]) {
if (st < ed) {
int i = st, j = ed;
Type temp = arr[i]; //基准数
while (i < j) {
while (i < j && arr[j] >= temp) j--;
arr[i] = arr[j];
while (i < j && arr[i] <= temp) i++;
arr[j] = arr[i];
}
arr[i] = temp;
QuickSort_rec(st, i - 1, arr);
QuickSort_rec(i + 1, ed, arr);
}
}