数据结构——归并排序
系列文章:数据结构与算法系列——从菜鸟到入门
简述
归并排序是将两个或两个以上的有序表组合成一个新的有序表。
其基本思想是:先将 N 个数据看成 N 个长度为 1 的表,将相邻的表成对合并,得到长度为 2 的 N/2 个有序表,进一步将相邻的合并,得到长度为 4 的 N/4 个有序表,以此类推,直到所有数据均合并成一个长度为 N 的有序表为止。每一次归并过程称做一趟。
那么归并排序就可以分解为,分解、归并两个子问题。
分解
分解方式分为“从上往下”和“从下往上”两种方式。如下图所示:
1.“从上往下”的归并排序:
分解:将当前空间一分为二,即求分裂点 mid=(low+high)/2;
求解:递归地对两个子区间 a[low...mid]和 a[mid+1...high]进行归并排序。递归的结束条件为子区间长度为 1(或low>=high)。
/** * 分解 * 归并排序(从上至下) */ private static void mSort(int[] arr,int low,int high){ if (low >= high) { return; } int mid = (high+low)/2; mSort(arr, low, mid); // 继续分解左子区间 mSort(arr, mid+1, high); // 继续分解右子区间 merge(arr, low, mid, high); // 合并两个子区间 }
2.“从下往上”的归并排序:
将待排序的数列分成若干个长度为 1 的子数列,然后将这些数列两两合并;得到若干个长度为 2 的有序数列,再将这些数列两两合并,得到长度为 4 的有序数列,直到合并成一个完整数列为止。这就得到了我们想要的排序结果。
/** * 分解 * 归并排序(从下往上) */ private static void mSort2(int[] A, int len) { for (int i = 1;i < len;i *= 2) { // i 为每次合并子数组的长度;每次为上一次的2倍 int step = i; // 要合并的子数组的长度 int low; // 当前左区间的起始索引 int group = step*2;// 两个子数组合并后的长度 for (low = 0;low+group-1 < len;low+=group) { merge(A, low, low+step-1, low+group-1); } if (low+step-1 < len-1) { // 子数组量为奇数个,剩余一个子数组没有配对 merge(A, low, i+step-1, len-1); } } }
归并
将两个子区间 arr[low...mid]和 arr[mid+1...high]归并为一个有序的区间 arr[low...high]。
在归并的过程中需要申请一个临时数组空间,将待排序的两数组顺序的保存在该临时空间中。最后将有序的临时空间覆盖到原数组中。此时数组就变为局部有序了。
/** * 归并 */ private static void merge(int[] arr, int low, int mid, int high) { int[] tempArr = new int[high - low + 1]; // 临时空间 int tempIndex = 0; // 临时空间的下标 int left = low; // 左区间起始索引 int right = mid+1; // 右区间起始索引 while (left<=mid && right<=high) { if (arr[left] < arr[right]) { tempArr[tempIndex++] = arr[left++]; }else { tempArr[tempIndex++] = arr[right++]; } } // 将左区间中剩余元素添加至临时空间 while (left <= mid) { tempArr[tempIndex++] = arr[left++]; } // 将右区间中剩余元素添加至临时空间 while (right <= high) { tempArr[tempIndex++] = arr[right++]; } // 此时的临时空间为有序序列 // 将临时空间覆盖至原数组相应的位置 tempIndex-=1; while (tempIndex>=0) { arr[low+tempIndex] = tempArr[tempIndex]; tempIndex--; } }
性能分析
时间复杂度:
归并排序的时间复杂度是O(nlgn)。
假设被排序的数组的长度为n,遍历一趟的时间复杂度为O(n),那么排序过程需要遍历多少次?
归并排序的形式就是二叉树,需要遍历的次数就是二叉树的深度,根据完全二叉树的性质,其深度为lgn,则得出时间复杂度为O(n*lgn)。
稳定性:
归并排序是稳定的算法,它满足稳定算法的定义。
算法稳定性——假设在数列中存在 a[i]=a[j],若在排序之前,a[i]在 a[j]前面;并且排序之后,a[i]仍然在 a[j]前面。则这个排序算法是稳定的。
实现源码
/** * 归并排序 * @param A * @param n * @return */ public int[] mergeSort(int[] A, int n) { int low = 0; int high = n-1; mSort(A, low, high); return A; } /** * 分解 * 归并排序(从上至下) */ private static void mSort(int[] arr,int low,int high){ if (low >= high) { return; } int mid = (high+low)/2; mSort(arr, low, mid); // 继续分解左子区间 mSort(arr, mid+1, high); // 继续分解右子区间 merge(arr, low, mid, high); // 合并两个子区间 } /** * 分解 * 归并排序(从下往上) */ private static void mSort2(int[] A, int len) { for (int i = 1;i < len;i *= 2) { // i 为每次合并子数组的长度;每次为上一次的2倍 int step = i; // 要合并的子数组的长度 int low; // 当前左区间的起始索引 int group = step*2;// 两个子数组合并后的长度 for (low = 0;low+group-1 < len;low+=group) { merge(A, low, low+step-1, low+group-1); } if (low+step-1 < len-1) { // 子数组量为奇数个,剩余一个子数组没有配对 merge(A, low, i+step-1, len-1); } } } /** * 合并 * 将两个子区间arr[low...mid]和 arr[mid+1...high]归并为一个有序的区间a[low...high]。 */ private static void merge(int[] arr, int low, int mid, int high) { int[] tempArr = new int[high - low + 1]; // 临时空间 int tempIndex = 0; // 临时空间的下标 int left = low; // 左区间起始索引 int right = mid+1; // 右区间起始索引 while (left<=mid && right<=high) { if (arr[left] < arr[right]) { tempArr[tempIndex++] = arr[left++]; }else { tempArr[tempIndex++] = arr[right++]; } } // 将左区间中剩余元素添加至临时空间 while (left <= mid) { tempArr[tempIndex++] = arr[left++]; } // 将右区间中剩余元素添加至临时空间 while (right <= high) { tempArr[tempIndex++] = arr[right++]; } // 此时的临时空间为有序序列 // 将临时空间覆盖至原数组相应的位置 tempIndex-=1; while (tempIndex>=0) { arr[low+tempIndex] = tempArr[tempIndex]; tempIndex--; } }
参考资料
[1] 归并排序
[2] 数据结构与算法分析——Java语言描述, 7.5 - 归并排序