本文图片来在极客时间-王争老师的《数据结构与算法之美》
归并排序(MergeSort)
算法说明
先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。
例如要对一组元素11 8 3 9 7 1 2 5进行归并排序,步骤如下:
步骤说明
递推公式:
merge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r))
终止条件:
p >= r 不用再继续分解
JAVA代码
package cn.hgy.data.algorithm.lesson2;
/**
* 归并排序
* <pre>
* 算法介绍:
* 1. 先把数组从中间分成前后两部分{@link #sort(int[], int, int)}
* 2. 对前后两部分分别排序,再将排好序的两部分合并在一起{@link #merge(int[], int, int, int)}}
* 3. 当数组只有1个元素时,表示该数组已经排好序
* </pre>
*
* @author guoyu.huang
* @version 1.0.0
*/
public class MergeSort {
public static void main(String[] args) {
int[] arrays = {11, 8, 3, 9, 7, 1, 2, 5};
sort(arrays, 0, arrays.length - 1);
for (int i = 0; i < arrays.length; i++) {
System.out.print(arrays[i] + " ");
}
}
/**
* 排序,适用递归方式来实现
*
* @param content
* @param p
* @param r
*/
public static void sort(int[] content, int p, int r) {
// 递归停止条件: p>=r,表示这时候元素集合最小单位是2个
if (p < r) {
int mid = (p + r) / 2;
sort(content, p, mid);
sort(content, mid + 1, r);
// 开始合并元素
merge(content, p, mid, r);
}
}
/**
* 合并元素,保证每个元素集合是有序的
*
* @param content
* @param p
* @param mid
* @param r
*/
public static void merge(int[] content, int p, int mid, int r) {
// 因为归并排序合并时,是两个元素集合进行合并,所以需要两个指针下标
// p2指针为第二个元素的下标,所以是mid+1
// 例如元素集合(11 8),那么第一个元素集合下标就是0,第二个就是1,以此类推
int p1 = p, p2 = mid + 1;
// 申请一个临时数组,用于暂存有序集合
int[] temp = new int[r - p + 1];
int i = 0;
while (p1 <= mid && p2 <= r) {
if (content[p1] < content[p2]) {
temp[i++] = content[p1++];
} else {
temp[i++] = content[p2++];
}
}
// 当存在其中一个元素集合为空的时候,直接将另一个元素集合的元素追加在尾部
while (p1 <= mid) {
temp[i++] = content[p1++];
}
while (p2 <= r) {
temp[i++] = content[p2++];
}
// 将有序部分进行替换
for (int j = p; j <= r; j++) {
content[j] = temp[j - p];
}
}
}
快速排序(QuickSort)
算法说明
如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。
我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。
优化方案-原地排序
步骤说明
递推公式:
quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1… r)
终止条件:
p >= r
JAVA代码
package cn.hgy.data.algorithm.lesson2;
/**
* 快速排序
* <pre>
* 算法介绍:
* 1. 取一个中心点,将数组进行分区{@link #partition(int[], int, int)},并返回中心点位置,数组为小于中心点,中心点,大于等于中心点
* 2. 再分别对小于中心点,中心点和中心点,大于等于中心点进行分区{@link #sort(int[], int, int)}
* 3. 当数组只有1个元素时,表示该数组已经排好序
* </pre>
*
* @author guoyu.huang
* @version 1.0.0
*/
public class QuickSort {
public static void main(String[] args) {
int[] arrays = {11, 8, 3, 9, 7, 1, 2, 5};
sort(arrays, 0, arrays.length - 1);
for (int i = 0; i < arrays.length; i++) {
System.out.print(arrays[i] + " ");
}
}
public static void sort(int[] content, int p, int r) {
// 递归停止条件:p>=r,表示这时候元素集合最小单位是2个
if (p < r) {
// 开始分区排序
int mid = partition(content, p, r);
sort(content, p, mid - 1);
sort(content, mid + 1, r);
}
}
/**
* 分区,取分区中的最后一个元素作为pivot,将元素集合分为三个部分:小于pivot,pivot, 大于pivot
*
* @param content
* @param p
* @param r
* @return 返回pivot的位置
*/
public static int partition(int[] content, int p, int r) {
int pivot = content[r];
int i = p, j = p;
for (; i < r; i++) {
// 如果当前元素小于pivot,则需要将当前元素和j指向的元素交换位置
if (content[i] < pivot) {
int tmp = content[i];
content[i] = content[j];
content[j] = tmp;
j++;
}
}
// 最后交换pivot和j下标的位置
content[r] = content[j];
content[j] = pivot;
return j;
}
}
总结
分析维度一:时间复杂度
- 归并排序
我们假设对 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/2^k)=T(1) 时,也就是 n/2^k=1,我们得到 k=log2n 。我们将 k 值代入上面的公式,得到 T(n)=Cn+nlog2n 。如果我们用大 O 标记法来表示的话,T(n) 就等于 O(nlogn)。所以归并排序的时间复杂度是 O(nlogn)。
- 快速排序
大部分情况时间复杂度为O(nlogn),当中心值为元素集合中的极值时,时间复杂度会为O(n^2)
分析维度二:空间复杂度
- 归并排序:O(n)
- 快速排序:O(1)
分析维度三:是否稳定
- 归并排序:稳定
- 快速排序:不稳定