一、简介
归并排序 (merge sort) 是一类与插入排序、交换排序、选择排序不同的另一种排序方法。归并的含义是将两个或两个以上的有序表合并成一个新的有序表。归并排序有多路归并排序、两路归并排序 , 可用于内排序,也可以用于外排序。这里仅对内排序的两路归并方法进行讨论。
二、两路归并排序算法思路
分而治之(divide - conquer);每个递归过程涉及三个步骤
第一, 分解: 把待排序的 n 个元素的序列分解成两个子序列, 每个子序列包括 n/2 个元素.
第二, 治理: 对每个子序列分别调用归并排序MergeSort, 进行递归操作
第三, 合并: 合并两个排好序的子序列,生成排序结果.
三、算法实现
此算法的实现不像图示那样简单,现分三步来讨论。首先从宏观上分析,首先让子表表长 L=1 进行处理;不断地使 L=2*L ,进行子表处理,直到 L>=n 为止,把这一过程写成一个主体框架函数 mergesort 。然后对于某确定的子表表长 L ,将 n 个记录分成若干组子表,两两归并,这里显然要循环若干次,把这一步写成一个函数 mergepass ,可由 mergesort 调用。最后再看每一组(一对)子表的归并,其原理是相同的,只是子表表长不同,换句话说,是子表的首记录号与尾记录号不同,把这个归并操作作为核心算法写成函数 merge ,由 mergepass 来调用。假设我们有一个没有排好序的序列,那么首先我们使用分割的办法将这个序列分割成一个一个已经排好序的子序列,然后再利用归并的方法将一个个的子序列合并成排序好的序列。分割和归并的过程可以看下面的图例。
归并排序的基本步骤就是
- 先把一个数组以二分法的方式递归的分组,(分)
- 然后再将相邻的两个数组进行作对比,把两个已排序好的子数组中的数字由小到大(由大到小)地放到辅助数组temp[]中,(合)
- 最后再把辅助数组中的元素复制到原数组中返回。
这种排序的时间复杂度为O(NlgN),同时需要O(N)的辅助空间——保存N个元素。
这是一种稳定的算法。
四、代码实现
public static int[] sort(int[] a,int low,int high){
int mid = (low+high)/2;
if(low<high){
sort(a,low,mid);
sort(a,mid+1,high);
//左右归并
merge(a,low,mid,high);
}
return a;
}
public static void merge(int[] a, int low, int mid, int high) {
int[] temp = new int[high-low+1];
int i= low;
int j = mid+1;
int k=0;
// 把较小的数先移到新数组中
while(i<=mid && j<=high){
if(a[i]<a[j]){
temp[k++] = a[i++];
}else{
temp[k++] = a[j++];
}
}
// 把左边剩余的数移入数组
while(i<=mid){
temp[k++] = a[i++];
}
// 把右边边剩余的数移入数组
while(j<=high){
temp[k++] = a[j++];
}
// 把新数组中的数覆盖nums数组
for(int x=0;x<temp.length;x++){
a[x+low] = temp[x];
}
}
代码版本2:
public static void mergeSort(int[] nums) {
//创建与原数组相同长度的数组
int[] temp = new int[nums.length];
mergeSort(nums, temp, 0, nums.length-1);
}
private static void mergeSort(int[] nums, int[] temp, int left, int right) {
if(left < right) {
//从中间将数组分成两半
int mid = (left + right) >> 1;
mergeSort(nums, temp, left, mid);
mergeSort(nums, temp, mid+1, right);
//将两个数组重新合并
merge(nums, temp, left, mid+1, right);
}
}
private static void merge(int[] nums, int[] temp,
int leftPos, int rightPos, int rightEnd) {
int leftEnd = rightPos - 1;
int tmpPos = leftPos;
int numElements = rightEnd - leftPos + 1;
//对比左右两个数组并将较小的数先放到辅助数组
while(leftPos <= leftEnd && rightPos <= rightEnd) {
if(nums[leftPos] <= nums[rightPos]) {
temp[tmpPos++] = nums[leftPos++];
}else {
temp[tmpPos++] = nums[rightPos++];
}
}
//把左边数组剩下的原数放到辅助数组
while(leftPos <= leftEnd) {
temp[tmpPos++] = nums[leftPos++];
}
//把右边数组剩下的原数放到辅助数组
while(rightPos <= rightEnd) {
temp[tmpPos++] = nums[rightPos++];
}
//把辅助数组复制到原数组
for(int i = 0; i < numElements; i++,rightEnd--) {
nums[rightEnd] = temp[rightEnd];
}
}
原地归并算法实现
/*
* 原地归并
*/
public class InPlaceMergeSort {
private static void reverse(int[] arr, int i, int j) {
while(i < j)
{
int temp = arr[i];
arr[i++] = arr[j];
arr[j--] = temp;
}
}
// swap [bias, bias+headSize) and [bias+headSize, bias+headSize+endSize)
private static void swapAdjacentBlocks(int arr[], int bias, int oneSize, int anotherSize) {
reverse(arr, bias, bias + oneSize - 1);
reverse(arr, bias + oneSize, bias + oneSize + anotherSize - 1);
reverse(arr, bias, bias + oneSize + anotherSize - 1);
}
private static void inplaceMerge(int arr[], int l, int mid, int r)
{
int i = l; // 指示左侧有序串
int j = mid + 1; // 指示右侧有序串
while(i < j && j <= r) //原地归并结束的条件。
{
while(i < j && arr[i] <= arr[j])
{
i++;
}
int index = j;
while(j <= r && arr[j] <= arr[i])
{
j++;
}
swapAdjacentBlocks(arr, i, index-i, j-index);
i += (j-index);
}
}
public static void mergeSort(int arr[], int l, int r)
{
if(l < r)
{
int mid = (l + r) / 2;
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
inplaceMerge(arr, l, mid, r);
}
}
private static void print(int[] arr) {
for (int i : arr) {
System.out.print(i+", ");
}
System.out.println();
}
/* 测试用例 */
public static void main(String[] args) {
int[] arr = {3,5,1,7,0,6,9,11};
mergeSort(arr, 0, arr.length-1);
print(arr);
}
}
原地归并算法实现
package 算法视频.排序;
public class 归并排序_要求空间复杂度为O_1 {
public static void main(String[] args) {
int[] a = { 6, 8, 5, 7, 9, 3, 2 };
f1(a, 0, a.length - 1);
printResult(a);
}
private static void f1(int[] a, int left, int right) {
if (left >= right) {
return;
}
int middle = (left + right) >> 1; // 以中间点为分割点分割数组
f1(a, left, middle); // 将left到middle分割
f1(a, middle + 1, right); // 将middle+1到right分割
meger(a, left, middle, right); // 将分割后的数组合并
}
// 原地移动
private static void meger(int[] a, int left, int middle, int right) {
int i = left;
int j = middle + 1;
int index = j;
while (i <= middle && j <= right) {
// 先找到第一个数组中比第二个数组第一个数大的第一个值
while (i <= middle && a[i] < a[j]) {
i++;
}
// 然后再找到第二个数组中比a[i]大的第一个值
while (j <= right && a[i] > a[j]) {
j++;
}
// 此时a[left...i-1]均小于a[middle+1...j-1],交换位置并将i前移j-1-middle-1位即可,然后重复
swap(a, i, index-1);
swap(a, index, j - 1);
swap(a, i, j - 1);
i += j - index;
index = j;
}
}
private static void swap(int[] a, int i, int j) {
if (i > j || i < 0 || j < 0) {
return;
}
while (i <= j) {
int tem = a[i];
a[i] = a[j];
a[j] = tem;
i++;
j--;
}
}
private static void printResult(int[] a) {
if (a == null) {
return;
}
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
System.out.println();
}
}
五、算法分析
(1)稳定性
归并排序是一种稳定的排序。
(2)存储结构要求
可用顺序存储结构。也易于在链表上实现。
(3)时间复杂度
对长度为n的文件,需进行趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlgn)。
(4)空间复杂度
需要一个辅助向量来暂存两有序子文件归并的结果,故其辅助空间复杂度为O(n),显然它不是就地排序。
注意:
若用单链表做存储结构,很容易给出就地的归并排序
拓展:http://cmsblogs.com/?p=4701