这篇文章是看了“左程云”老师在b站上的讲解之后写的, 自己感觉已经能理解了, 所以就将整个过程写下来了。
这个是“左程云”老师个人空间的b站的链接, 数据结构与算法讲的很好很好, 希望大家可以多多支持左程云老师, 真心推荐.
https://space.bilibili.com/8888480?spm_id_from=333.999.0.0
1. 原理与递归实现
1.1 逻辑实现
有一个数组:arr[] = [6, 4, 2, 3, 9, 4]
. 通过归并排序的方式将其变得有序.
- 因为我们这里利用的是
递归
实现, 所以要先设置子过程 (分解), 还是分一半, 这个数组的下标是:
0, 1, 2, 3, 4, 5
, 最开始是:f(0, 5)
所以对应的, 要将左边分为:f(0, 2)
, 右边是:f(3, 5)
, 然后一直往下划分, 直到有序 (就是l == r
) 的时候, 因为这个时候只剩下1
个数字了, 肯定是有序的. - 然后开始进行
merge
过程,merge
过程就是将返回的左右两边的两个(或者以上的)值开辟一个数组进行单独排序, 然后再将排好序的数组刷回原数组中. - 最后一直重复这个过程, 直到排好序.
1.2 代码实例
这里的代码是单拿出来的, 全局静态变量什么的都在左程云老师的 github
上, 直接下载就行了.
1.2.1 几个需要注意的点
-
merge
为什么要传递m
这个中间位置, 因为我们需要注意排序对应的位置, 我们进行传递的过程中, 已经认为是左侧和右侧都是有序的,merge
过程就是一个将左侧和右侧一起变得有序的过程. 比如:左侧是6
, 右侧是4
, 这两个都是有序的, 所以merge
过程需要将这两个数字变得有序:-> [4, 6]
. -
下面代码的后面两个
while()循环
只会执行其中一个, 因为一定会有一侧的数组中有数字没有进入到help
数组中, 举一个极端一点的例子:左侧数组是1, 2, 3
, 右侧数组是5, 6, 7
, 这样的话用第一个while()循环
之后, 右侧数组中就没有一个数字进入到help数组
中, 因为此时左侧数组已经越界了, 不会执行第一个while()循环
了, 所以下面两个就是将没有进入到help
数组中的数字放入的. -
传递过去的是整个
arr数组
只是将arr数组
的一小部分进行了排序, 然后继续将排好序的数字刷回了原来的数组.
public static void mergeSort1(int l, int r) {
if (l == r) {
return; // 因为这里肯定是有序的, 所以什么都不用做, 直接返回就行了.
}
int m = (l + r) / 2;
mergeSort1(l, m); // 使用递归过程进行分析理解(最好是画递归决策图).
mergeSort1(m + 1, r);
merge(l, m, r); // merge过程是将到底返回之后的几个数字进行排序.(传递的时候需要将l, r传递过去, 因为我们需要注意排序对应的位置).
}
// merge过程.
public static void merge(int l, int m, int r) {
int i = l; // 这里需要用i来表示help位置的索引值.需要用i表示help位置,要将需要排序的值放到对应位置上去.
int a = l;
int b = m + 1;
while (a <= m && b <= r) { // 在左侧和右侧的数组中, 哪一个小就将哪一个数字拷贝到help数组中去
help[i++] = arr[a] <= arr[b] ? arr[a++] : arr[b++];
}
// 左侧指针、右侧指针,必有一个越界、另一个不越界 (所以只会执行一个while)
while (a <= m) {
help[i++] = arr[a++];
} // 若是左侧数组没耗尽, 就将左侧数组放入, 若是右侧数组没耗尽, 就将右侧数组放入,
while (b <= r) {
help[i++] = arr[b++];
}
for (i = l; i <= r; i++) {
arr[i] = help[i]; // 最后将已经排好序的数组放到原来的数组中.
}
}
1.3 复杂度分析
1.3.1 merge
过程
将两边的数组放入 help数组
中进行排序, 所以肯定会将两边数组的值过一遍, 假设有 n
个值, 那就将所有的值放入, 然后会将排好序的值放入原来的数组中, 所以还是 n
个值过一遍, 那对应的时间复杂度是:O(n) + O(n) -> 2 * O(n) -> O(n)
. (常数就直接忽略了, 不用管).
1.3.2 递归过程
利用上一节课讲过的 master公式
:分析这个递归过程是:左边开一半, 右边开一半, 所以一半一半地分,
master公式前半部分是:T(N) = T(N/2) + T(N/2) -> T(N) = 2 * T(N/2)
. 所以:a = 2, b = 2
.
我们刚刚也分析了 merge
过程, 时间复杂度是:O(n)
所以:T(n) = 2 * T(n/2) + O(n), a = 2, b = 2, c = 1
, 根据上一节课的 master公式
:归并排序的时间复杂度是:O(n * logn)
.
1.3.3 空间复杂度分析
因为开了一个 help数组
, 所以时间复杂度是:O(n)
.
2. 归并排序非递归实现
2.1 逻辑实现
有一个数组:arr[] = [6, 2, 3, 3, 4, 6, 9, 3, 1]
. 将这个数组用非递归的归并排序的方式进行排序.
- 先设置一个步长
step = 1
, 然后将步长step * 2
直到步长step >= arr.length
. 例子中的arr.length == 9, 所以 step == 16 才行
. - 然后在数组中找对应的步长, 比如:
最开始 step = 1
, 先找到6(左边) 和 2(右边)
, 将6, 4
进行merge
过程进行排序, 然后继续向后寻找3, 3
进行merge过程进行排序
, 最后到1(左边)
位置, 没有右边
位置了, 所以这个就不用管了, 就放着就行. (此时arr数组:[2, 6, 3, 3, 4, 6, 3, 9, 1]
) - 然后继续到第二步长:
step = 2
, 找到2, 6(左边) 和 3, 3(右边)
, 然后进行merge过程
排序, 最后的arr数组:2, 3, 3, 6, 3, 4, 6, 9, 1
. - 然后继续执行上述过程, 执行到
step == 8
的时候, 左侧是:2, 3, 3, 3, 4, 6, 6, 9
, 右侧的数字是:1
, 依然可以进行merge过程
, 所以最后就直接排好序了. - 当然每一次
merge过程
之后都是要将辅助数组中的数字刷回原来数组的. (这个过程已经包括在merge
) merge
过程哪怕是只有一侧有数字, 另一侧没有数字也能进行merge
, 不影响.
2.2 代码实例
merge
过程是一样的, 不用管, 还是直接去左程云老师的 github
下载就行了.
2.2.1 几个注意事项
m = l + step - 1
// 这个是左侧的最后一个位置.m + 1 >= n
这个的意义是说明右侧已经没有可以拿到的值了, 所以可以跳出while循环
到增加步长的下一步了. (还需要说明的是:这个不代表for循环
结束了, 有可能是step == 1
, 此时数组长度是奇数, 所以没有拿到右侧的数字, 跳出循环了).Math.min(l + (step << 1) - 1, n - 1)
, 这个的意义和上面差不多, 有可能我此时想要拿到最右侧的边界:但是有可能数组的最右侧没有l + (step << 1) - 1
这么大, 所以直接进行从arr.length - 1
和l + (step << 1) - 1
选择一个, 哪一个小用哪一个. (毕竟是不同的步长).
public static void mergeSort2() {
// 一共发生O(logn)次 // n是arr.length.
for (int l, m, r, step = 1; step < n; step <<= 1) { // 这里写成:step *= 2 也行.
// 内部分组merge,时间复杂度O(n)
l = 0;
while (l < n) {
m = l + step - 1; // 这个是左侧的最后一个位置.
if (m + 1 >= n) {
break;
}
r = Math.min(l + (step << 1) - 1, n - 1);
merge(l, m, r);
l = r + 1; // 这一组的l~r已经排好序了, 该到下一组了.
}
}
}
2.3 复杂度分析
2.3.1 时间复杂度分析
假设有数组的长度为:1000
, 但是 for循环的
步长 step
还是从 1
开始的, 直到超过 1000
, 所以这个的时间复杂度是 O(log(n))
.
merge
过程只不过是将所有的数字分组进行排序, 最后所有的分组之后的数字相加还是等于:n
, 所以时间复杂度是:O(n)
.
这样每进行一次 for循环
, 然后执行 merge
所以最后的时间复杂度是:O(n * log(n))
.
3. 总结和提醒
我们都知道:时间复杂度:O(n * log (n))
肯定是要比:O(n^2)
快很多很多的,
举一个例子: 假设数组长度是:10^6
, 所以 O(n^2) 就是 10^12
时间才能排好序, 而 O(n * log (n))
只需要 20 * 10^6 == 2 * 10^7
就行了, 肯定是要比 O(n^2)
快很多很多的.
原理解释:比较行为没有被浪费. 比如 冒泡排序
中, 全部都比较一次, 然后只是搞定了一个数字, 然后继续重新开始, 再解决一个数字, 这样存在大量的比较行为, 而且有很多都浪费了.
但是在归并排序中, 每一组比较之后都进行了重新排序, 没有浪费比较行为.