1、归并排序 Merge Sort
1.一、实现原理
- 若是要排序一个数组,咱们先把数组从中间分红先后两部分,而后对先后两部分分别排序,再将排好序的两部分合并在一块儿,这样整个数组就都有序了。
- 归并排序使用的就是分治思想。分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。
- 分治思想跟递归思想很像。分治算法通常都是用递归来实现的。 分治是一种解决问题的处理思想,递归是一种编程技巧,这二者并不冲突。
- 写递归代码的技巧就是,分析得出递推公式,而后找到终止条件,最后将递推公式翻译成递归代码。因此,要想写出归并排序的代码,咱们先写出归并排序的递推公式。
- 递推公式:erge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r))
- 终止条件:p >= r 不用再继续分解
- merge_sort(p…r)表示,给下标从 p 到 r 之间的数组排序。
- 咱们将这个排序问题转化为了两个子问题, merge_sort(p…q) 和 merge_sort(q+1…r),其中下标 q 等于 p 和 r 的中间位置,也就是 (p+r)/2。
- 当下标从 p 到 q 和从 q+1 到 r 这两个子数组都排好序以后,咱们再将两个有序的子数组合并在一块儿,这样下标从 p 到 r 之间的数据就也排好序了。
- 实现思路以下:
/**
* 归并排序
* @param arr 排序数据
* @param n 数组大小
*/
public static void merge_sort(int[] arr, int n) {
merge_sort_c(arr, 0, n - 1);
}
// 递归调用函数
public static void merge_sort_c(int[] arr, int p, int r) {
// 递归终止条件
if (p >= r) {
return;
}
// 取p到r之间的中间位置q
int q = (p + r) / 2;
// 分治递归
merge_sort_c(arr, p, q);
merge_sort_c(arr, q + 1, r);
// 将 arr[p...q] 和 arr[q+1...r] 合并为 arr[p...r]
merge(arr[p...r],arr[p...q],arr[q + 1...r]);
}
- merge(arr[p...r], arr[p...q], arr[q + 1...r]) 这个函数的做用就是,将已经有序的 arr[p…q] 和 arr[q+1…r] 合并成一个有序的数组,而且放入 arr[p…r]。
- 以下图所示,咱们申请一个临时数组 tmp,大小与 arr[p…r] 相同。
- 咱们用两个游标 i 和 j,分别指向 arr[p…q] 和 arr[q+1…r] 的第一个元素。
- 比较这两个元素 arr[i] 和 arr[j],若是 arr[i] <= arr[j],咱们就把 arr[i] 放入到临时数组 tmp,而且 i 后移一位,不然将 arr[j] 放入到数组 tmp,j 后移一位。
- 继续上述比较过程,直到其中一个子数组中的全部数据都放入临时数组中,再把另外一个数组中的数据依次加入到临时数组的末尾,这个时候,临时数组中存储的就是两个子数组合并以后的结果了。
- 最后再把临时数组 tmp 中的数据拷贝到原数组 arr[p…r] 中。
/**
* merge 合并函数
* @param arr 数组
* @param p 数组头
* @param q 数组中间位置
* @param r 数组尾
*/
public static void merge(int[] arr, int p, int q, int r) {
if (r <= p) return;
// 初始化变量i j k
int i = p;
int j = q + 1;
int k = 0;
// 申请一个大小跟A[p...r]同样的临时数组
int[] tmp = new int[r - p + 1];
// 比较排序移动到临时数组
while ((i <= q) && (j <= r)) {
if (arr[i] <= arr[j]) {
tmp[k++] = arr[i++];
} else {
tmp[k++] = arr[j++];
}
}
// 判断哪一个子数组中有剩余的数据
int start = i, end = q;
if (j <= r) {
start = j;
end = r;
}
// 将剩余的数据拷贝到临时数组tmp
while (start <= end) {
tmp[k++] = arr[start++];
}
// 将tmp中的数组拷贝回 arr[p...r]
for (int a = 0; a <= r - p; a++) {
arr[p + a] = tmp[a];
}
}
1.二、性能分析
- 归并排序稳不稳定关键要看 merge() 函数,也就是两个有序子数组合并成一个有序数组的那部分代码。
- 在合并的过程当中,若是 arr[p…q] 和 arr[q+1…r] 之间有值相同的元素,那咱们能够像伪代码中那样,先把 arr[p…q] 中的元素放入 tmp 数组。
- 这样就保证了值相同的元素,在合并先后的前后顺序不变。因此,归并排序是一个稳定的排序算法。
- 其时间复杂度是很是稳定的,不论是最好状况、最坏状况,仍是平均状况,时间复杂度都是 O(nlogn)。
- 归并排序的合并函数,在合并两个有序数组为一个有序数组时,须要借助额外的存储空间。
- 尽管每次合并操做都须要申请额外的内存空间,但在合并完成以后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。
- 临时内存空间最大也不会超过 n 个数据的大小,因此空间复杂度是 O(n),不是原地排序算法。
2、快速排序 Quicksort
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,就说明全部的数据都有序了。
- 用递推公式来将上面的过程写出来的话,就是这样:quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1, r)。
- 终止条件:p >= r
/**
* 快速排序
* @param arr 排序数组
* @param p 数组头
* @param r 数组尾
*/
public static void quickSort(int[] arr, int p, int r) {
if (p >= r)
return;
// 获取分区点 并移动数据
int q = partition(arr, p, r);
quickSort(arr, p, q - 1);
quickSort(arr, q + 1, r);
}
partition() 分区函数:java
- 是随机选择一个元素做为 pivot(通常状况下,能够选择 p 到 r 区间的最后一个元素),而后对 arr[p…r] 分区,并将小于 pivot 的放右边,大于的放左边,函数返回 pivot 的下标。
partition() 的实现有两种方式:算法
-
一种是不考虑空间消耗,此时很是简单。编程
- 申请两个临时数组 X 和 Y,遍历 arr[p…r],将小于 pivot 的元素都拷贝到临时数组 X,将大于 pivot 的元素都拷贝到临时数组 Y,最后再将数组 X 和数组 Y 中数据顺序拷贝到arr[p…r]。
-
/**
* 分区函数方式一
*
* @param arr 数组
* @param p 上标
* @param r 下标
* @return 函数返回 pivot 的下标
*/
public static int partition1(int[] arr, int p, int r) {
int[] xArr = new int[r - p + 1];
int x = 0;
int[] yArr = new int[r - p + 1];
int y = 0;
int pivot = arr[r];
// 将小于 pivot 的元素都拷贝到临时数组 X,将大于 pivot 的元素都拷贝到临时数组 Y
for (int i = p; i < r; i++) {
// 小于 pivot 的存入 xArr 数组
if (arr[i] < pivot) {
xArr[x++] = arr[i];
}
// 大于 pivot 的存入 yArr 数组
if (arr[i] > pivot) {
yArr[y++] = arr[i];
}
}
int q = x + p;
// 再将数组 X 和数组 Y 中数据顺序拷贝到 arr[p…r]
for (int i = 0; i < x; i++) {
arr[p + i] = xArr[i];
}
arr[q] = pivot;
for (int i = 0; i < y; i++) {
arr[q + 1 + i] = yArr[i];
}
return q;
}
-
另一种有点相似选择排序。数组
- 咱们经过游标 i 把 arr[p…r-1] 分红两部分。arr[p…i-1] 的元素都是小于 pivot 的,咱们暂且叫它“已处理区间”,arr[i…r-1] 是“未处理区间”。
- 咱们每次都从未处理的区间 arr[i…r-1] 中取一个元素 arr[j],与 pivot 对比,若是小于 pivot,则将其加入到已处理区间的尾部,也就是 arr[i]的位置。
- 在数组某个位置插入元素,须要搬移数据,很是耗时。此时能够采用交换,在 O(1) 的时间复杂度内完成插入操做。须要将 arr[i] 与 arr[j] 交换,就能够在 O(1)时间复杂度内将 arr[j] 放到下标为 i 的位置。
-
/**
* 分区函数方式二
* @param arr 数组
* @param p 上标
* @param r 下标
* @return 函数返回pivot的下标
*/
public static int partition2(int[] arr, int p, int r) {
int pivot = arr[r];
int i = p;
for (int j = p; j < r; j++) {
if (arr[j] < pivot) {
if (i == j) {
++i;
} else {
int tmp = arr[i];
arr[i++] = arr[j];
arr[j] = tmp;
}
}
}
int tmp = arr[i];
arr[i] = arr[r];
arr[r] = tmp;
return i;
}
2.二、性能分析
- 由于分区的过程涉及交换操做,若是数组中有两个相同的元素,好比序列 6, 8, 7, 6, 3, 5, 9, 4,在通过第一次分区操做以后,两个 6 的相对前后顺序就会改变。因此,快速排序并不是稳定的排序算法。
- 按照上面的第二种分区方式,快速排序只涉及交换操做,因此空间复杂度为 Q(1),是原地排序算法。
- 时间复杂度为 Q(nlogn),最差为Q(n²)。
3、二者对比
|
归并排序 |
快速排序 |
排序思想 |
处理过程由下到上,先处理子问题,而后在合并 |
由上到下,先分区,在处理子问题 |
稳定性 |
是 |
否 |
空间复杂度 |
Q(n) |
Q(1) 原地排序算法 |
时间复杂度 |
都为 O(nlogn) |
平均为 O(nlogn),最差为 O(n²) |
- 归并之因此是非原地排序算法,主要缘由是合并函数没法在原地执行。快速排序经过设计巧妙的原地分区函数,能够实现原地排序,解决了归并排序占用太多内存的问题。
- 归并排序算法是一种在任何状况下时间复杂度都比较稳定的排序算法,这也使它存在致命的缺点,即归并排序不是原地排序算法,空间复杂度比较高,是 O(n)。正由于此,它也没有快排应用普遍。