归并排序
2022-10-24 18:37:52 # 算法 # 排序

1. 归并排序算法思想

归并排序使用的是分治的思想,把问题拆分成子问题,逐个解决子问题,在把子问题的解合并,得到原问题的解。

2. 递归的归并排序

2.1. 算法描述

对于一个数组array,数组的长度是size:

  1. 如果size==1,那么数组已经有序了,返回;否则进行下面的步骤,
  2. 把原数组拆分成长度近似相等的两个子数组(如果size是奇数,那么无法平均分配),递归调用归并排序的方法,对这两个子数组进行排序;
  3. 把排好序的两个子数组和并在一起。

2.2. 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MergeSortRecursively {

public static void sort(int[] array) {
sort(array,0,array.length - 1,new int[array.length]);
}

private static void sort(int[] array, int l, int r,int[] help) {
// 对数组的[l..r]部分进行排序,使用辅助数组help
if(l == r) return;
int m = (l + r) >>> 1;// 获取中点位置,使用>>>是为了防止溢出
sort(array,l,m,help);
sort(array,m + 1,r,help);
merge(array,l,m,r,help);
}

private static void merge(int[] array, int l, int m ,int r, int[] help) {
int i = l, j = m + 1, k = 0;
while(i <= m && j <= r) help[k++] = array[i] <= array[j] ? array[i++] : array[j++];// 谁小拷贝谁,相等的时候,优先考虑左组(保证排序算法的稳定性)
while(i <= m) help[k++] = array[i++];
while(j <= r) help[k++] = array[j++];
System.arraycopy(help,0,array,l,k);
}
}

时间复杂度$O(N\log_2N)$,空间复杂度$O(N)$的合并辅助空间以及$O(\log_2N)$的递归栈空间。

3. 迭代的归并排序

递归版本的是从上往下的方法,大数组不断拆分成小数组。迭代的方法是从下往上的方法,先考虑合并最小的分组,然后逐渐增加分组的长度,直到整个数组变成一个分组的时候,数组就排好序了。

3.1. 算法描述

数组待排序部分的长度记为len,分组长度groupLen初始化为1;数组待排序部分开始下标l,末尾下标r;

只要groupLen<len,就执行下面的步骤:

  1. 使用一个变量x表示当前分组的开始位置,x初始化为l;只要x<r,执行下面的步骤;
    1. 如果数组剩余部分刚好够左分组的长度,或者不够左分组的长度,那么此时是没有右分组的,因此直接退出内循环;
    2. 否则,有右分组,那么确定右分组的结束位置y,以及左分组的结束位置mid;
    3. 执行merge操作
    4. 开始位置x = y+1;
  2. 如果分组长度groupLen已经超过了数组的长度了,那么退出外循环,排序结束,
  3. 否则groupLen *= 2

3.2. 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class MergeSortIteratively {

public static void sort(int[] array) {
sort(array,0,array.length - 1,new int[array.length]);
}

private static void sort(int[] array, int l, int r, int[] help) {
if(l == r) return;
int len = r - l + 1, groupLen = 1, x, m, y;
while(groupLen < len) {
x = l;
while(x < r) {

if((m = x + groupLen - 1) >= r) break;// m是左组的结束位置,如果m不小于r,则说明此时没有右组了,因此结束
y = Math.min(m + groupLen, r);// 有右组,但是右组不一定是满的,因此右组的结束位置是r和m+groupLen的二者的最小值。
merge(array,x,m,y,help);
x = y + 1;
}
if(groupLen > (len >> 1)) break; // 这里不能使用 if((groupLen << 1) > len),因为groupLen 乘以2以后可能会溢出。
groupLen <<= 1;
}
}

private static void merge(int[] array, int l, int m, int r, int[] help) {
int i = l, j = m + 1, k = 0;
while(i <= m && j <= r) help[k++] = array[i] <= array[j] ? array[i++] : array[j++];
while(i <= m) help[k++] = array[i++];
while(j <= r) help[k++] = array[j++];
System.arraycopy(help,0,array,l,k);
}
}

时间复杂度$O(N\log_2N)$,空间复杂度$O(N)$。没有过多的系统堆栈空间。

4. 测试程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.*;

public class MergeSortTest {

public static void main(String[] args) {
int testTimes = 100_0000, maxArraySize = 30;
while(testTimes-- > 0) {
Random r = new Random();
int size = r.nextInt(maxArraySize) + 1;
int[] array1 = new int[size], array2 = new int[size], array3 = new int[size];
for(int i = 0; i < size; ++i) array1[i] = array2[i] = array3[i] = r.nextInt(5000);
Arrays.sort(array1);
MergeSortRecursively.sort(array2);
MergeSortIteratively.sort(array3);
for(int i = 0; i < size; ++i) {
if(array1[i] != array2[i] || array1[i] != array3[i]) {
System.out.println("Oops!!, Not equals");
System.out.println("array:");
System.out.println(Arrays.toString(array1));
System.out.println("array2:");
System.out.println(Arrays.toString(array2));
System.out.println("array3:");
System.out.println(Arrays.toString(array3));
return;
}
}
System.out.println("Test case: " + testTimes + " pass");
}
System.out.println("Done Successfully!!");
}
}

5. Master公式

如果我们发现一个问题的时间复杂度$T(N)$和它的子问题的时间复杂度之间呈现这样的关系:
$$
T(N)=aT(\frac{N}{b}) + N^d
$$
那么$T(N)$是可以直接被算出来的,即:

$$ T(N) = \begin{cases}N^{max\{d,\log_b{a}\}}\quad \text{if}\log_b{a}\neq d; \\ N^d*\log_2{N}\quad \text{if} \log_b{a}= d; \end{cases} $$

这就是Master公式。

证明过程可以参考这里