排序算法系列—-归并排序

归并排序的核心思想同上一篇介绍的快速排序,都是采用了分治法的思想。其基本思想是将一个待排序序列,划分成两个子序列,然后将这两个子序列排好序之后合并,并递归的将子序列划分为更小的子序列,一直到只有一个元素的子序列,然后自底向上两两合并。
由于归并排序的核心就是合并两个数组,所以先看看如何合并两个有序的数组,并保证合并后的数组是有序的。

数组合并

首先同时遍历两个数组(A,B),并将两个数组的第一个元素进行对比,假设A的第一个元素较小,则将A[0]复制到合并后的数组中,然后比较A[1]与B[0],如果B[0]较小,则将B[0]复制到合并后的数组中,然后比较A[1]与B[1],如果A[1]较小,则将A[1]复制到合并后的数组中,继续比较A[2]与B[0]。如此重复,直至某一个数组全部复制到合并后的数组,然后将另一个数组中剩下的元素按顺序复制到合并后的数组,这样整个合并就完成了。

基本思想

归并排序主要采用分治法的思想,将一个问题划分成多个子问题,然后递归解决各个子问题。主要就是将一个待排序序列划分成两个子序列,然后再继续对子序列进行划分,一直划分到只有一个元素为止,然后自下往上两两合并子序列,最终完成排序。

实现要点

实现的要点主要就是如何完成合并,上面已经详细介绍了合并的方法,这里也不叙述了。直接看下面的实现代码吧。
Java实现

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package com.vicky.sort;

import java.util.Random;

/**
* 归并排序
*
* 时间复杂度:O(nlgn)
*
* 空间复杂度:O(n)
*
* 稳定性:稳定
*
* @author Vicky
*
*/

public class MergeSort {
public static <T extends Comparable<T>> void sort(T[] data) {
long start = System.nanoTime();
if (null == data) {
throw new NullPointerException("data");
}
if (data.length == 1) {
return;
}
Object[] newData = new Object[data.length];
devideSort(newData, data, 0, data.length - 1);
System.out.println("use time:" + (System.nanoTime() - start) / 1000000);
}

/**
* 分治法排序
*
* @param <T>
* @param newData
* @param data
* @param start
* @param end
*/

private static <T extends Comparable<T>> void devideSort(Object[] newData,
T[] data, int start, int end)
{

int middle = (start + end) / 2;
if (middle > start) {
devideSort(newData, data, start, middle);
}
if (middle + 1 < end) {
devideSort(newData, data, middle + 1, end);
}
mergeArray(newData, data, start, middle, middle + 1, end);
}

/**
* 合并两个数组
*
* @param <T>
* @param newData
* 用于辅助
* @param data
* 被合并的数组
* @param start1
* @param end1
* @param start2
* @param end2
*/

@SuppressWarnings("unchecked")
private static <T extends Comparable<T>> void mergeArray(Object[] newData,
T[] data, int start1, int end1, int start2, int end2)
{

int i = start1, j = start2, index = start1;
while (i <= end1 || j <= end2) {
if (i > end1) {// 第一个数组已经全部合并到新的数组,则将第二个数组剩下的元素按顺序直接复制到新的数组
newData[index++] = data[j++];
continue;
}
if (j > end2) {// 第二个数组已经全部合并到新的数组,则将第一个数组剩下的元素按顺序直接复制到新的数组
newData[index++] = data[i++];
continue;
}
// 将两个数组从第一位开始比较,取出较小的复制到新的数组,同时继续将较大的元素与较小的元素所在的数组后续的元素进行对比
if (data[i].compareTo(data[j]) <= 0) {
// 此处i<=j的时候也选择i的原因是为了保证稳定性,因为i在原数组中是在j前面的,
// 所以即使i==j我们也选择i保证排序后i依旧在j前面
newData[index++] = data[i++];
} else {
newData[index++] = data[j++];
}
}
// 将排好序的元素复制回原数组
for (i = start1; i <= end2; i++) {
data[i] = (T) newData[i];
}
}

public static void main(String[] args) {
Random ran = new Random();
Integer[] data = new Integer[10000];
Integer[] data2 = new Integer[10000];
for (int i = 0; i < data.length; i++) {
data[i] = ran.nextInt(1000000);
data2[i] = data[i];
}
MergeSort.sort(data);
QuickSort.sort(data2);
SortUtils.printArray(data);
SortUtils.printArray(data2);
}
}
效率分析

(1)时间复杂度
O(nlgn)
对长度为n的文件,需进行lgn 趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlgn)。
(2)空间复杂度
O(n)
合并的时候需要O(n)长度的辅助数组进行合并,所以归并排序的空间复杂度是O(n)。
(3)稳定性
稳定
排序中有可能发生两个相同元素交换的地方就是在合并的时候,所以我们可以在合并的时候认为的控制其稳定性,具体方式在上面代码的mergeArray()中有注释。