归并排序(Merge Sort)
1945年由冯诺依曼首次提出。
执行流程
1 不断的将当前序列平均分割成两个子序列
直到不能再分割(序列中只剩1个元素)
2 不断的将两个子序列合并成一个有序序列
直到最终只剩下1个有序序列
归并排序,主要有分和和两个步骤。
其中,分感觉好理解些,找出begin,end,求出middle,然后递归调用,直到元素个数小于或等于1时,不再分割。不难写出:
public static void divideSort(int[] array, int begin, int end)
{
if (end - begin <= 1) return;
int middle = (begin + end)/2;
//假如[9, 18, 67, 99, 116, 156, 8]
//[begin, middle) = [0, 3)
divideSort(array, begin, middle);
//[middle, end) = [3, 6)
divideSort(array, middle, end);
}
合并就有些许难度,这其实算是另外一道算法题:
如何将两个有序数组合并成一个大数组?
比如,我们需要将两个有序数组[3, 8]、[6, 10]合并成一个大数组。
大致流程是:
-
建立一个可以容纳两个数组长度的大数组array,用以存放排序好的数组。并设立一个变量ai,用以存放新元素要存放的位置。默认是0;
-
设立两个变量lefti、righti,分别指向两个数组当前比较到了哪里。默认都是0,也就是li = 0; ri = 0;
-
比较array1[li] 与array2[ri]值的大小
如果array1[li] <= array2[ri],则将array[ai] = array1[li]; li++;
如果li超过array1的边界,则将array2剩余的元素依次放在新数组array后面。如果array1[li] > array2[ri],则将array[ai] = array2[ri]; ri++;
如果ri超过array2的边界,则将array1剩余的元素依次放在新数组array后面。
有了以上思路,不难写出代码:
public static void main(String[] args)
{
int[] array1 = {1, 2, 3};
int[] array2 = {2, 4, 6, 10};
mergeSortArray(array1, array2);
}
public static void mergeSortArray(int[] array1, int[] array2)
{
int li = 0;
int ri = 0;
int ai = 0;
int[] array = new int[array1.length + array2.length];
while ((li < array1.length) && (ri < array2.length)) {
if (array1[li] <= array2[ri]) {
array[ai++] = array1[li++];
}else {
array[ai++] = array2[ri++];
}
}
if (li == array1.length) {
for (int i = ri; i < array2.length; i++) {
array[ai++] = array2[ri++];
}
}
if (ri == array2.length) {
for (int i = li; i < array1.length; i++) {
array[ai++] = array1[li++];
}
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]+ " ");
}
}
打印结果:
1 2 2 3 4 6 10
写完了两个不相关的数组,我们再看一下归并排序的合并。
归并排序的两个数组是相连的,并且数组大小是等分的,因此,在建立新的数组时候,不需要建立一个等长的大数组。
只需要将前面一个数组copy出一份,然后将新copy出来的数组,与后面的数组做对比,将比较出来的元素放入到原数组中即可。这样,空间复杂度比上面的少一半。
左边先结束,什么也不做
右边先结束,左边按个移过来
public static void main(String[] args)
{
int[] array = {1, 2, 3, 2, 4, 6, 10};
divideSort(array, 0, array.length);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]+ " ");
}
}
public static void divideSort(int[] array, int begin, int end)
{
if (end - begin <= 1) return;
int middle = (begin + end)/2;
//假如[9, 18, 67, 99, 116, 156, 8]
//[begin, middle) = [0, 3)
divideSort(array, begin, middle);
//[middle, end) = [3, 6)
divideSort(array, middle, end);
mergeSort(array, begin, middle, end);
}
public static void mergeSort(int[] array, int begin, int middle, int end)
{
int li = begin, le = middle - begin;//左边数组
int ri = middle, re = end;//右边数组
int ai = begin;//array需要插入的位置
//copy左边的数组
int[] leftArray = new int[middle];
for (int i = 0; i < le; i++) {
//注意右边是begin + i
leftArray[i] = array[begin + i];
}
//如果左边还没结束
while (li < le) {
if (ri < re) {
if (leftArray[li] <= array[ri]) {//左边数组小于或等于大数组右边的值
array[ai++] = leftArray[li++];
}else {
array[ai++] = array[ri++];
}
}else {//右边结束,将leftArray依次移过来
array[ai++] = array[li++];
}
}
//如果左边先结束,则剩下的啥也不需要做了
}
其中,里面的while循环可优化:
while (li < le) {
if (ri < re && (leftArray[li] > array[ri])) {
array[ai++] = array[li++];
}else {//右边结束,将leftArray依次移过来
array[ai++] = leftArray[li++];
}
}
归并排序复杂度分析
归并排序花费时间
假设归并排序花费的时间为T(n)
里面有两个均分的归并排序T(n/2)
以及一个合并遍历O(n)
那么,可以得出:
T(n) = 2*T(n/2) + O(n);…1
并且T(1) = O(1);
那么1式左右同除以n
T(n)/n = 2*T(n/2)/n + O(1);
T(n)/n = T(n/2)*2/n + O(1);
T(n)/n = T(n/2)/n/2 + O(1);
令S(n) = T(n)/n;,那么S(1) = T(1) = O(1);
S(n) = S(n/2)+ O(1) = S(n/4)+ O(2) = S(n/8)+ O(3) = S(n/2^k)+ O(k)
n = 2^k,k = logn
S(n) = S(2^k) = S(1) + O(logn) = O(logn)
S(n) = T(n)/n = O(logn)
那么,T(n) = O(nlogn)
由于归并排序总是平均分割子序列,所以最好、最坏、平均时间复杂度都是O(nlogn),属于稳定排序。
归并排序的空间复杂度为:
在分割的时候,由于是递归调用,每次递归需要占用内存空间,递归调用分割logn次,也就是分割是O(logn)
在合并的时候,由于需要将左边的数组copy出一份,因此,占用空间是O(n/2)
所以,归并排序的空间复杂度为O(logn + n/2) 约等于 O(n)