核心思想:将数组从中间分成前后两部分,然后对前后两部分分别进行排序,再将排序好的两个部分有序合并在一起,这样整个数组有序。全文图示来源于王争的《数据结构和算法之美》
归并排序使用的就是分治思想。分治,顾名思义,就是分而治之,讲一个大的问题分解成小的问题来解决,小的问题解决了大的问题也就解决了。分治算法一般都是用递归来实现,分治是一种解决问题的处理思想,递归是一种编程技巧,两者并不冲突。以下重点讨论如何用递归代码来实现归并排序。下面是归并排序的递推公式。
递推公式:
merge_sort(p...r) = merge(mergesort(p...q), mergesort(q+1...r))
终止条件:
p >= r 不用继续分解
mergesort(p...r) 表示给下标从 p 到 r 之间的数组排序。将这个排序问题转化为两个子问题 mergesort(p...q) 和mergesort(q+1...r),其中 q 为 p 和 r 的中间位置,即(p+r)/2。当前后两个子数组排好序之后,再将它们合并在一起,这样下标从 p 到 r 之间的数据也就排序好了。具体过程用C++实现如下:
void mergesort(vector<int>& A, int l, int r)
{
if (l < r)
{
int mid = l + (r - l) / 2;
mergesort(A, l, mid);
mergesort(A, mid+1, r);
merge(A, l, mid, r);
}
}
void merge(vector<int>& A, int l, int mid, int r)
{
vector<int> tmp(r - l + 1);
int i = l;
int j = mid + 1;
int k = 0;
while (i <= mid && j <= r) {
tmp[k++] = A[i] < A[j] ? A[i++] : A[j++];
}
while (i <= mid) {
tmp[k++] = A[i++];
}
while (j <= r) {
tmp[k++] = A[j++];
}
for (i = l, k = 0; i <= r; ++i, ++k)
A[i] = tmp[k];
}
归并排序的时间复杂度任何情况下都是 O(nlogn),看起来非常优秀(快速排序最坏情况系时间复杂度也是 O(n2))。但归并排序并没有像快排那样应用广泛,因为它有一个致命的“弱点”,那就是归并排序不是原地排序算法。原因是合并函数需要借助额外的存储空间,空间复杂度为 O(n)。
核心思想:选取一个基准元素 pivot,比 pivot 小的放到左边,比 pivot 大的放到右边,对 pivot 左右两边的序列递归进行以上操作。
快速排序也是根据分治、递归的处理思想实现。地推公式如下:
递推公式:
quicksort(p…r) = quicksort(p…q-1) + quicksort(q+1...r)
终止条件:
p >= r
在wikipedia看到一个比较好的快速排序实现,这里用C++实现一遍,这里的 partition 非常巧妙而且容易理解,取最后元素为基准 pivot,i 开始取值 l,j 从 l 遍历至 h-1,当 A[j] < pivot 时,交换 A[i] 和 A[j] 的值,同时 i++,实现将小于 pivot 的值全部放在区间[l, i)中,跳出循环体后将 A[i] 和 A[h] 值交换,至此,(i,r] 区间的值都不小于 pivot,返回 pivot 的索引。讲的比较啰嗦,直接看代码并推敲一下过程应该也容易弄懂。
void quicksort(vector<int>& A, int l, int r)
{
if (l < r)
{
int p = partition(A, l, r);
quicksort(A, l, p-1);
quicksort(A, p+1, r);
}
}
int partition(vector<int>& A, int l, int r)
{
int pivot = A[r];
int i = l;
for (int j = l; j < r; ++j)
{
if (A[j] < pivot) {
std::swap(A[i++], A[j]);
}
}
std::swap(A[i], A[r]);
return i;
}
快速排序的算法的平均时间复杂度是 O(nlogn),最坏时间复杂度是 O(n2),空间复杂度是 O(1)。快速排序不是一个稳定的排序算法。
由上图可以发现,归并排序的处理过程是由下到上的,先处理子问题,然后合并。而快排正好相反,其处理过程是由上而下的,先分区,然后处理子问题。归并排序虽然是稳定的,时间复杂度是 O(nlogn) 的排序算法,但它是非原地排序算法。快排通过设计巧妙的原地分区函数,可以实现原地排序,解决归并排序占用太多内存的问题。
利用快排思想可以实现O(n)时间复杂度内求无序数组中的第 K 大元素,LeetCode题目215. 数组中的第K个最大元素,具体实现如下:
class Solution {
public:
int findKthLargest(vector<int>& A, int k) {
return get_top_k(A, 0, A.size()-1, k);
}
int get_top_k(vector<int>& A, int l, int r, int k)
{
int p = partition(A, l, r);
if (k-1 == p) return A[p]; // 索引从0开始,第k大需减一
return k-1 < p ? get_top_k(A, l, p-1, k)
: get_top_k(A, p+1, r, k);
}
int partition(vector<int>& A, int l, int r)
{
int pivot = A[r];
int i = l;
for (int j = l; j < r; ++j)
{
if (A[j] > pivot) {
std::swap(A[i++], A[j]);
}
}
std::swap(A[i], A[r]);
return i;
}
};
非常巧妙,修改了 partition 函数的实现,左边放大于基准的元素,而且不需要将数组全部排序,只要求出第 k 大的值即可,非常高效。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。