注:本文为《算法导论》中排序相关内容的笔记。对此感兴趣的读者还望支持原作者。
基本概念
与插入排序相同,归并排序也是一种常见的排序算法。归并排序是建立在归并操作上的一种高效且稳定的排序算法。该算法是采用分治法将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
算法思想
归并算法的核心思想即分治法。所谓分治法,就是将问题分而治之。它将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解来求解出原问题的解。
分治法在每层递归中都有三个步骤:
- 分解原问题为若干问题,这些子问题是原问题的规模较小的类似问题;
- 求解这些子问题,各自递归地求解各子问题。然而,若子问题的规模足够小,则直接求解;
- 合并这些子问题的解以建立原问题的解。
而具体到归并排序,其完全遵循此模式:
- 分解待排序的n个元素的序列成各具 n / 2 n/2 n/2个元素的两个子序列。(若无法对2整除,则分别上下取整);
- 使用归并排序递归地排序求解两个子序列;
- 合并两个已排序的子序列以产生已排序的答案。
其实,说白了归并排序首先将原序列递归地分解为足够小的子序列,一般为1,然后比较两个相邻的子序列的元素大小,每次取较大值或者较小值,直至两个子序列的元素都被取完,合并成一个序列,最后,不断地重复此过程,直至原序列有序。
代码示例
千言万语,不如代码一段,废话少说,直接上代码。此代码段给出了归并排序的Java实现版本。
/**
* 归并排序的简单实现,非降序(整型数组)
* @author 爱学习的程序员
* @version V1.0
* */
import java.lang.Integer;
import java.util.Random;
public class MergeSort{
// 排序
public static void merge(int[] arr, int n, int mid, int m){
// 划分后的左数组
int[] left = new int[mid - n + 2];
// 划分后的右数组
int[] right = new int[m - mid + 1];
// 为划分数组设置哨兵
left[left.length - 1] = right[right.length - 1] = Integer.MAX_VALUE;
// 复制
int i = 0;
for(; i < left.length - 1; i++)
left[i] = arr[n+i];
for(i = 0; i < right.length - 1; i++)
right[i] = arr[mid+1+i];
// 归并排序(左右两数组比较,取较小的)
int j = 0, k = 0;
for(i = n; i <= m; i++){
if(left[j] < right[k]){
arr[i] = left[j];
j++;
}
else{
arr[i] = right[k];
k++;
}
}
}
// 归并(递归)
public static void split(int[]arr, int n, int m){
// 数组错误
if(arr.length <= 0) return;
// 无需排序
if(arr.length == 1) return;
// 划分结束
if(n >= m) return;
int mid = (n + m) / 2;
split(arr, n, mid);
split(arr, mid+1, m);
merge(arr, n, mid, m);
}
public static void main(String[] args){
//测试数组生成
int[] arr = new int[10];
Random rand = new Random();
System.out.println("测试数组:");
int i = 0;
int length = arr.length;
for(i = 0; i < arr.length; i++){
arr[i] = rand.nextInt(100);
System.out.print(arr[i]+" ");
}
// 归并排序
split(arr, 0, length-1);
// 输出排序结果
System.out.println("\n"+"排序结果");
for(i = 0; i < arr.length; i++)
System.out.print(arr[i]+" ");
}
}
值得一提的是,本程序在合并两个子序列时使用了设置哨兵的技巧。哨兵为一特殊值,用于简化代码。这里,使用整型的最大值作为哨兵值,结果每当一个序列的元素值为哨兵值时,该子序列的元素全部取完。
算法分析
从示例代码中不难看出,归并排序的运行时间为分治法的三个基本步骤。假设把原问题分解为 a a a个子问题,每个子问题的规模是原问题的 1 / b 1/b 1/b(对于归并排序, a = b = 2 a=b=2 a=b=2)。为了求解一个规模为 n / b n/b n/b的子问题,需要 T ( n / b ) T(n/b) T(n/b)的时间,所以需要 a ( n / b ) a(n/b) a(n/b)的时间来求解 a a a个子问题。如果分解成子问题需要时间 D ( n ) D(n) D(n),合并子问题的解成原问题的解需要时间 C ( n ) C(n) C(n),那么可有 T ( n ) = a T ( n / b ) + D ( n ) + C ( n ) T(n)=aT(n/b)+D(n)+C(n) T(n)=aT(n/b)+D(n)+C(n)。而由代码可得,分解步骤仅仅计算子数组的中间位置,需要常量时间,因此 D ( n ) = O ( 1 ) D(n)=O(1) D(n)=O(1)。而合并子问题的解成原问题的解的过程需要 O ( n ) O(n) O(n)的时间,又有 a = n = 2 a=n=2 a=n=2,所以最坏情况下,原公式可改写为 T ( n ) = 2 T ( n / 2 ) + n T(n)=2T(n/2)+n T(n)=2T(n/2)+n,使用归纳法可求得 T ( n ) = O ( n lg n ) T(n)=O(n \lg n) T(n)=O(nlgn)。
算法总结
与插入排序相比,归并排序同样简单,而且它高效,排序结果稳定,广泛应用于实际生活中。