归并排序
归并排序和快速排序这两种算法都采用了分治的思想,且速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列,其具体思想如下:
1、将序列每相邻两个数字进行归并操作(merge),形成(n/2)个序列,排序后每个序列包含两个元素;
2、将上述序列再次归并操作,形成(n/4)个序列,每个序列包含四个元素;
3、重复步骤2,直到所有元素排序完毕。
归并排序中一般所用到的是2-路归并排序,即将含有n个元素的序列看成是n个有序的子序列,每个子序列的长度为1,而后两两合并,得到n/2个长度为2或1的有序子序列,再进行两两合并……直到最后由两个有序的子序列合并成为一个长度为n的有序序列。2-路归并的核心操作是将一维数组中前后相邻的两个有序序列归并为一个有序序列。
1、一般归并排序
实现代码如下:
public class MergeSort {
/*
* 将有序的arr[start,...,mid]和有序的arr[mid+1,...,end]归并为有序的brr[0,...,end-start+1]
* 而后再将brr[0,...,end-start+1]复制到arr[start,...,end],使arr[start...end]有序
*/
public static void merge(int[] arr, int[] brr, int start, int mid, int end) {
int i = start;
int j = mid + 1;
int k = 0;
// 比较两个有序序列中的元素,将较小的元素插入到brr中
while (i <= mid && j <= end) {
if (arr[i] <= arr[j])
brr[k++] = arr[i++];
else
brr[k++] = arr[j++];
}
// 将arr序列中剩余的元素复制到brr中,这两个语句只可能执行其中一个
while (i <= mid)
brr[k++] = arr[i++];
while (j <= end)
brr[k++] = arr[j++];
// 将brr中的元素复制到arr中,使arr[start,...,end]有序
for (i = 0; i < k; i++)
arr[i + start] = brr[i];
}
/*
* 借助brr数组对arr[start,...,end]内的元素进行归并排序,归并排序后的顺序为从小到大
*/
public static int[] mergeSort(int[] arr, int[] brr, int start, int end) {
if (start < end) {
int mid = (start + end) / 2;
mergeSort(arr, brr, start, mid); // 左边递归排序
mergeSort(arr, brr, mid + 1, end); // 右边递归排序
merge(arr, brr, start, mid, end); // 左右序列归并
}
return arr;
}
public static void print(int[] array) {
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
public static void main(String args[]) {
int[] arr = { 3, 1, 5, 4, 9, 8, 15, 18, 2 };
int len = arr.length;
int[] brr = new int[len];
print(MergeSort.mergeSort(arr, brr, 0, len - 1));
}
}
总结:
时间复杂度为O(nlogn),这是该算法中最好、最坏和平均的时间性能。
空间复杂度为 O(n)。
归并排序比较占用内存,但却是几个高效排序算法(快速排序、希尔排序、堆排序)中唯一稳定的排序方法。
2、原地归并排序
上面的归并排序需要一个临时数组brr[]来存放归并后的数组arr[],然后将brr[]复制到arr[]中,空间复杂度为O(n),下面介绍一种时间复杂度为O(1)的原地归并排序。
原地归并排序,就是用“手摇算法”把归并排序的归并过程中使用到的辅助数组去掉,将空间复杂度降为O(1)。
“手摇算法”
首先,我们来看一个有趣的小算法:手摇算法(也叫三次反转算法)。 题目:将字符串abcdefg,变成efgabcd,要求空间复杂度O(1)。 解答: 第一步:将子串abcd反转,变成dcba。源字符串变成dcbaefg 第二步:将字串efg反转,变成gfe。源字符串变成dcbagfe 第三步:将整个字符串dcbagfe反转,变成efgabcd。 |
手摇算法常常被用来旋转字符串,一定要记住这个小算法。下面,我们来看看怎样用手摇算法,实现原地归并排序。
举例:
1.原始序列如图a, i到j-1为第一个序列(记为序列1),j到序列尾为第二个序列(记为序列2),我们要把两个有序序列合并成一个有序的序列
2.图a中因为i.data < j.data,所以i++,直到i.data> j.data,如图b
3.另index记下此时j的位置,j向后扫描,直到j.data>i.data为止,如图c
4.此时我们看到序列1,i之前的元素,都小于序列2,j之前的元素到小于[i,index)的元素。
5.将图d中蓝色部分和红色部分用手摇算法翻转。翻转后,结果如下图e:
6.然后将i移动j-index步,如图f。重复上述步骤,即可完成两个有序序列的合并了。
实现代码: