数组al[0,mid-1] 和 al[mid,num-1],都分别有序。将其merge成有序数组al[0,num-1],要求空间复杂度O(1)
首先回忆一下概念:归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。算法如下:
// 将有二个有序数列a[first...mid]和a[mid+1...last]合并。
private static void mergearray(int a[], int first, int mid, int last,
int temp[]) {
int i = first, j = mid + 1;
int m = mid, n = last;
int k = 0;
while (i <= m && j <= n) {
if (a[i] < a[j])
temp[k++] = a[i++];
else
temp[k++] = a[j++];
}
while (i <= m)
temp[k++] = a[i++];
while (j <= n)
temp[k++] = a[j++];
for (i = 0; i < k; i++)
a[first + i] = temp[i];
}
private static void merge(int a[], int first, int last, int temp[]) {
if (first < last) {
int mid = (first + last) / 2;
merge(a, first, mid, temp); // 左边有序
merge(a, mid + 1, last, temp); // 右边有序
mergearray(a, first, mid, last, temp); // 再将二个有序数列合并
}
}
public static void mergeSort(int a[]) {
int n = a.length;
int[] p = new int[n];
merge(a, 0, n - 1, p);
}
好了,介绍了最基本的归并知识,会到本题。
第一种:先看一种最简单、也最容易想到的方法:开辟一个辅助数组c。(以下所有算法假设数组都是正有序)
// 将有序数组a[]和b[]合并到c[]中
public static int[] mergeSort(int a[], int b[], int c[]) {
int i, j, k;
int n = a.length;
int m = b.length;
i = j = k = 0;
while (i < n && j < m) {
if (a[i] < b[j]) {
c[k++] = a[i++];
} else {
c[k++] = b[j++];
}
}
while (i < n) {
c[k++] = a[i++];
}
while (j < m) {
c[k++] = b[j++];
}
return c;
}
这种方法的时间复杂度、空间复杂度都为O(n),显然不符合题目要求。
第二种:不开辟另一块内存,直接在原数组上操作。
/**
* 将a[0...m-1]和a[m,n-1]归并;时间复杂度为O(m*n),空间复杂度为O(1).
* 把后半部分插到前半部分
*
* @param a 待排序数组
* @param m
*/
public static void merge(int[] a, int m) {
int i = 0;
int j = m;
int n = a.length;
while (j < n && i < j) {
if (a[j] >= a[j - 1]) {
break;
} else if (a[j] < a[i]) {
int temp = a[j];
for (int k = j - 1; k >= i; k--) {
a[k + 1] = a[k];
}
a[i] = temp;
i++;
j++;
} else {
i++;
}
}
}
此时,空间复杂度为O(1),符合要求,但增加了对前半部分的移动,增加了时间复杂度。在最好情况下,时间复杂度O(1),在最坏情况下,时间复杂度为O(m*n),典型的以时间换空间。此时,所耗费的时间代价太大,还是不合适,有没有更好的呢?
第三种:我们知道,堆排序的空间复杂度为O(1),时间复杂度为O(nlgn),比原来那个好,试一下:
/**
* 堆排序
* @param a
*/
private static void heapSort(int[] a) {
int n = a.length;
for (int i = n / 2 - 1; i >= 0; i--) {
build(a,n,i);
}
for(int i=0;i<n;i++){
//将第一个根节点和最后n-1-i个叶子结点交换
int temp = a[0];
a[0] = a[n-1-i];
a[n-1-i] = temp;
//调整堆
build(a,n-1-i,0);
}
}
/**
* 建一个小根堆
* @param a
* @param n
* @param index
*/
static void build(int[] a, int n, int index) {
int i = index;
int j = 2 * i + 1;//根节点i的左子节点
while (j < n) {
if(j+1<n && a[j]>a[j+1]){//如果左节点比右结点大,则左节点满足要求
j++;
}
if(a[i]<=a[j]){//如果父节点大于或等于右结点,则满足要求;此子大根堆完成(等于号会影响该算法的稳定性)
break;
}else{
int temp = a[i];
a[i] = a[j];
a[j] = temp;
i=j;
j = 2* i +1;
}
}
return;
}
时间复杂度得到了降低(当然也可以把数组后半部分建堆,前半部分和堆的根节点比较交换,然后调整堆,时间可以得到优化,为nlg(n/2),但时间复杂度仍然不变)。
但题目要求的是用归并,有没有这样的归并呢?
第四种:在第二种的基础上加以优化
不需要辅助数组即可归并。
关键在于merge这个函数。两段递增的子数组arr[begin…mid-1]和arr[mid…end],i=begin,j=mid,k=end
i往后移动,找到第一个arr[i]>arr[j]的索引
j往后移动,再找第一个arr[j]>arr[i]的索引
附:上面的分析图来自http://www.cnblogs.com/daniagger/archive/2012/07/25/2608373.html
然后我们将i到mid的部分和mid到j-1的部分对调,较小的部分就调到前面去了,然后从后面的部分与j到k的部分又是两个递增的子数组,继续迭代即可。
/**
* 此算法是不稳定的
*
* @param a
* @param start
* @param mid
* @param end
*/
private static void in_place_mergeSort(int[] a, int start, int mid, int end) {
if (a[mid] >= a[mid - 1]) {
return;
}
int i = start, j = mid;
int step = 0;
while (i < mid && j < end) {
while (i < mid && j < end && a[i] <= a[j]) {
i++;
}
while (i < mid && j < end && a[i] >= a[j]) {// 此等号破坏了该算法的稳定
j++;
step++;
}
int n=j-1;
exchange(a,i,mid,n);
i += step+1;
mid += step;
j = mid;
step=0;
if (j>=end || a[mid] >= a[mid - 1]) {
break;
}
}
}
private static void exchange(int[] a,int start,int mid,int end){
reverse(a, start, mid-1);
reverse(a, mid, end);
reverse(a, start, end);
}
private static void reverse(int[] a, int start, int end) {
int i = start, j = end;
for (; i < j; i++, j--) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
在最好情况下,时间复杂度为O(1),在最坏情况下,最外层while循环次数为end-mid,和第二种方法的效果一样;reverse方法中for循环次数为mid,时间复杂度仍然为O(m*n),但是在一般情况下,经过测试,效率要比第二种高一倍。
转载请声明出处,http://blog.csdn.net/gaopo_y/article/details/8086143