归并排序和快速排序(算法系列)

Posted 码农琐话

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了归并排序和快速排序(算法系列)相关的知识,希望对你有一定的参考价值。

归并排序和快速排序

1. 归并排序

1.1 介绍

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。

1.2 原理

归并排序采用分治思想。分治,顾名思义,就是分而治之。将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。归并排序的思想就是,如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。

1.3 用递归实现归并排序

1.3.1 归并排序的递推公式和终止条件

递推公式: MergeSort(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到q和从q+1到r这两个子数组都排好序之后,我们在将两个有序的子数组合并在一起,这样下标从p到r之间的数据就排序好了。

1.3.2 实现

void Merge(int*array,int start, int end)
{
int mid = start + (end -start)/2;
int tempSize = end - start + 1;
int* tempArray = new int[tempSize];
int i = start;
int j = mid + 1;
int k = 0;
while (i <= mid && j <= end)
{
if (array[i] <= array[j])
{
tempArray[k++] = array[i++];
}
else
{
tempArray[k++] = array[j++];
}
}

if (i > mid)
{
while (j <= end)
{
tempArray[k++] = array[j++];
}

}

if (j > end)
{
while (i <= mid)
{
tempArray[k++] = array[i++];
}

}

for (int i = 0; i < tempSize; i++)
{
array[start+i] = tempArray[i];
}
delete[] tempArray;

}

void MergeSortRecursion(int* array , int start, int end)
{
if (start >= end)
{
return;
}

int mid = start + (end -start)/2;
MergeSortRecursion(array, start,mid);
MergeSortRecursion(array, mid + 1, end);
Merge(array,start,end);
}


void MergeSort(int* array, int size)
{
MergeSortRecursion(array,0, size -1);
}

1.4 归并排序的性能分析

1.4.1 归并排序是否是稳定排序算法

归并排序是否是稳定算法,取决于Merge函数的实现方式。即两个有序子数组合并成一个有序数组的实现。

1.4.2 归并排序的时间复杂度

由于归并排序涉及到递归算法,首先说明一下递归算法的时间复杂度分析方法。
如果定义求解问题a的时间是T(a),求解问题b,c的时间是T(b)和T(c),那我们可以得到的递推关系式:
T(a) = T(b) + T(c) + k
其中 K 等于将两个子问题 b、c 的结果合并成问题 a 的结果所消耗的时间。
套用这个公式,我们来分析一下归并排序的时间复杂度。
我们假设对 n 个元素进行归并排序需要的时间是 T(n),那分解成两个子数组排序的时间都是 T(n/2)。我们知道,merge() 函数合并两个有序子数组的时间复杂度O(n)。所以,套用前面的公式,归并排序的时间复杂度的计算公式就是:

T(1) = C   n=1 时,只需要常量级的执行时间,所以表示为 C
T(n) = 2*T(n/2) + n n>1

T(n) = 2*T(n/2) + n
= 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2*n
= 4*(2*T(n/8) + n/4) + 2*n = 8*T(n/8) + 3*n
= 8*(2*T(n/16) + n/8) + 3*n = 16*T(n/16) + 4*n
......
= 2^k * T(n/2^k) + k * n
......

通过这样一步一步分解推导,我们可以得到 T(n) = 2kT(n/2k)+kn。当 T(n/2^k)=T(1) 时,也就是 n/2^k=1,我们得到 k=log2n 。我们将 k 值代入上面的公式,得到T(n)=Cn+nlog2n 。如果我们用大 O 标记法来表示的话,T(n) 就等于T(n) 就等于 O(nlogn)。所以归并排序的时间复杂度是 O(nlogn)。
从我们的原理分析和伪代码可以看出,归并排序的执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)。

1.4.3 归并排序的空间复杂度

归并排序不是原地排序算法。需要借助额外的存储空间。数据在每次合并操作都需要申请额外的内存空间,但在合并完成之后,,临时开辟的内存空间就被释放掉了。综合分析申请的额外控件不会超过n,所以控件复杂度为O(n)。

2. 快速排序

2.1 介绍

快速排序由C. A. R. Hoare在1960年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

2.2 原理

  • 如果要排序数组中下标从p到r之间的一组数据,我们选择p到r之间的任意一个数据作为pivot(分区点)。

  • 我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到 中间。

  • 经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小 于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。

  • 根据分治、递归的处理思想,我们可以用递归排序下标从 p 到 q-1 之间的数据和下标从 q+1 到 r 之间的数据,直到区间缩小为 1,就说明所有的数据都有序了。

1.3 用递归实现快速排序

1.3.1 快速排序的递推公式和终止条件

递推公式: QuickSort(p...r)= QuickSort(p...q-1) + QuickSor(q+1...r)
终止条件: p>=r

1.3.2 实现

void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}

int Partition(int*array, int start, int end)
{
int pivot = array[end];
int i = start;

for (int j = start; j < end; ++j)
{
if (array[j] < pivot)
{
swap(&array[i] , &array[j]);
i++;
}
}
swap(&array[i], &array[end]);

return i;
}

void QuickSortR(int* array, int start, int end)
{
if (start >= end)
{
return;
}
int pivot = Partition(array, start, end);
QuickSortR(array, start, pivot - 1);
QuickSortR(array,pivot + 1, end);
}


void QuickSort(int* array, int size)
{
QuickSortR(array, 0, size - 1);
}

1.4 快速排序的性能分析

1.4.1 快速排序是否是稳定排序算法

快速排序算法根据Partition的实现可知,快速排序是不稳定排序算法。

1.4.2 快速排序的时间复杂度

快速排序的时间复杂度取决于pivot的选取。

  1. 如果pivot选取的比较合适的话,正好能将大区间对等地一分为二。可以采用以下方式计算快速排序的最优时间复杂度。快速排序的最优时间复杂度为O(nlogn).

T(1) = C   n=1 时,只需要常量级的执行时间,所以表示为 C
T(n) = 2*T(n/2) + n n>1

  1. 如果pivot选取的不好的话,分区不均等,极端情况将大区间(1...M)分为(M-1)和1。快速排序的时间复杂度退化为O(n²)。

  2. 快速排序的平均时间复杂度为O(nlogn)。具体推算略,可以网上查询一下。

1.4.3 快速排序的控件复杂度

快速排序的空间复杂度取决于Partition的实现。根据实现方式可以是O(n)或者O(1)。上面的实现方式采用原地交换的方式,故空间复杂度为O(1).

3. 归并排序和快速排序的对比

归并排序的处理过程是由下到上的,先处理子问题,然后再合并。
而快排正好相反,它的处理过程是由上到下的,先分区,然后再处理子问题。归并排序虽然是稳定的、时间复杂度为 O(nlogn) 的排序算法,但是它是非原地排序算法。我们前面讲过,归并之所以是非原地排序算法,主要原因是合并函数无法在原地执行。快速排序通过设计巧妙的原地分区函数,可以实现原地排序,解决了归并排序占用太多内存的问题。


以上是关于归并排序和快速排序(算法系列)的主要内容,如果未能解决你的问题,请参考以下文章

基础算法系列之排序算法[快速排序,归并排序,二分查找]

算法快速排序与归并排序对比

JavaScript 数据结构与算法之美 - 归并排序快速排序希尔排序堆排序

数据结构与算法从零开始系列:冒泡排序选择排序插入排序希尔排序堆排序快速排序归并排序基数排序

重学数据结构和算法之归并排序快速排序

JavaScript算法(归并排序与快速排序)