合并两个有序的子序,要求空间复杂度为O(1)

百度2012实习生校园招聘笔试题

数组al[0,mid-1]和al[mid,num-1]是各自有序的,对数组al[0,num-1]的两个子有序段进行merge,得到al[0,num-1]整体有序。要求空间复杂度为O(1)。注:al[i]元素是支持'<'运算符的。

先是琢磨良久,是否可能时间复杂度为O(n),因为这让我想起了,左旋转数组的相关的算法。深陷下去后,突然什么光一闪。。。

果断放弃,使用最简单的算法,几分钟笔墨,错改调。(上次就是如此深陷泥潭而不可自拔):

 1 void swap(int *a, int *b)
 2 {
 3     int tmp;
 4 
 5     tmp = *a;
 6     *a = *b;
 7     *b = tmp;
 8 }
 9 
10 void func(int a[], int n)
11 {
12     int i,j;
13 
14     if (n<=0)
15     {
16         return;
17     }
18     i = 0;
19     j = n/2;
20 
21     while(j<n && i<j)
22     {
23         if (a[i]<=a[j])
24         {
25             i++;
26         }
27         else
28         {
29             swap(a[i], a[j]);
30             for (int k=j-1;k>i; k--)
31             {
32                 swap(a[k], a[k+1]);
33             }
34             i++;
35             j++;
36         }
37     }
38 
39 }

算法简单,最坏的时间复杂度为O(n^2)。 

 

红色的区域是已排好的,绿色为前一子序,紫红为后子序;比较前后子序的最小元素,小的放入红色区域。

如果i的小,只需要往前一步;否则i与j的元素互换,各自向前一步;但此时导致前子序不再有序,通过调整恢复有序即可;

接着又网上搜索工作,找到一个block swapping。懒得看完,但却给了我提示,算法可以优化;

之前所说的调整恢复其实就是一个右移了一位,每当j中有元素较小时,就需要前移到红色区域中,这样导致了要移位;

试想j的后面有几个连续的元素都比i 小,这样每次就会导致j-i个移动元素;所以改动一下,没有每次都移动,等到j开始比i大的时候,

才开始移位,这时应用之前的“左移k的算法(貌似就是block swapping)”,其实时间复杂度为O(n),所以当j连续比i小的数很多,效率就比较高了。

例如:1 6 9 12 2 3 4 15

1、i=0;j=4;a[i]<a[j] 所以i++

2、i=1; j=4; a[i]>a[j] 所以交换为 1 2 9 12 6 3 4 15; i++;j++

3、i=2; j=5; a[i]>a[j] 所以交换为 1 2 3 12 6 9 4 15; i++; j++

4、i=3; j=6; a[i]<a[j] 已经a[j]开始小了,1 2 3 12 6 9 4 15,所以这时蓝色部分就要一次右移两位(两个连续的小),移位后1 2 3 6 9 12 4 15

5、之后,沿袭刚才的思想,继续下去。。。

这样只是稍微的优化一下而已。又是一番折腾

 1 void reverse(int a[], int i, int j)
 2 {
 3 
 4     if (j-i<1)
 5     {
 6         return;
 7     }
 8     for (;i<j; i++,j--)
 9     {
10         swap(&a[i], &a[j]);
11     }
12 }
13 
14 void rightmove(int a[], int i, int j, int k)
15 {
16     if (j-i<1)
17     {
18         return;
19     }
20 
21     reverse(a, i, j-k);
22     reverse(a, j-k+1, j);
23     reverse(a, i, j);
24 }
25 
26 void func1(int a[], int n)
27 {
28     int i,j,tmp;
29     i=0;
30     j = n/2;
31     while(i<j&& j<n)
32     {
33         tmp = j;
34         while(i<j && a[i]<=a[j])
35         {
36             i++;
37         }
38         while(i<j && a[j]< a[i])
39         {            
40             swap(&a[i], &a[j]);
41             j++;
42         }
43 
44         rightmove(a, i+1, j-1, j-tmp);
45         i++;
46     }
47 }

结果看了看标准答案,华丽地把哥的秒了:

 1 /*
 2 数组a[begin, mid] 和 a[mid+1, end]是各自有序的,对两个子段进行Merge得到a[begin , end]的有序数组。 要求空间复杂度为O(1)
 3 方案:
 4 1、两个有序段位A和B,A在前,B紧接在A后面,找到A的第一个大于B[0]的数A[i], A[0...i-1]相当于merge后的有效段,在B中找到第一个大于A[i]的数B[j],
 5 对数组A[i...j-1]循环右移j-k位,使A[i...j-1]数组的前面部分有序
 6 2、如此循环merge
 7 3、循环右移通过先子段反转再整体反转的方式进行,复杂度是O(L), L是需要循环移动的子段的长度
 8 */
 9 #include<iostream>
10 using namespace std;
11 
12 void Reverse(int *a , int begin , int end )   //反转
13 {
14     for(; begin < end; begin++ , end--)
15         swap(a[begin] , a[end]);
16 }
17 void RotateRight(int *a , int begin , int end , int k)    //循环右移
18 {
19     int len = end - begin + 1;  //数组的长度
20     k %= len;
21     Reverse(a , begin , end - k);
22     Reverse(a , end - k + 1 , end);
23     Reverse(a , begin , end);
24 }
25 
26 // 将有序数组a[begin...mid] 和 a[mid+1...end] 进行归并排序
27 void Merge(int *a , int begin , int end )
28 {
29     int i , j , k;
30     i = begin;
31     j = 1 + ((begin + end)>>1);    //位运算的优先级比较低,外面需要加一个括号,刚开始忘记添加括号,导致错了很多次
32     while(i <= end && j <= end && i<j)
33     {
34         while(i <= end && a[i] < a[j])
35             i++;
36         k = j;   //暂时保存指针j的位置
37         while(j <= end && a[j] < a[i])
38             j++;
39         if(j > k)
40             RotateRight(a , i , j-1 , j-k);   //数组a[i...j-1]循环右移j-k次
41         i += (j-k+1);  //第一个指针往后移动,因为循环右移后,数组a[i....i+j-k]是有序的
42     }
43 }
44 
45 void MergeSort(int *a , int begin , int end )
46 {
47     if(begin == end)
48         return ;
49     int mid = (begin + end)>>1;
50     MergeSort(a , begin , mid);   //递归地将a[begin...mid] 归并为有序的数组
51     MergeSort(a , mid+1 , end);   //递归地将a[mid+1...end] 归并为有序的数组
52     Merge(a , begin , end);       //将有序数组a[begin...mid] 和 a[mid+1...end] 进行归并排序
53 }
54 
55 int main(void)
56 {
57     int n , i ;
58     //    int a[] = {1, 5,6,7,15,66, 2,3,4,8,33,35};
59     int a[] = {1 ,6 ,9 , 2 ,3,4 ,15};
60     
61     n = sizeof(a)/sizeof(int);
62     
63     MergeSort(a , 0 , n - 1);
64     
65     for(i = 0 ; i < n ; ++i)
66         cout<<a[i]<<" ";
67     
68     return 0;
69 }

咋一看,和哥的像,太像了;可是算法的思想的是两码事。(思考:1 3 5 7 2 4 6 8,我的算法则将要退化)

答案的思想高明要与结合了,归并(分治)的思想。继续考虑刚才的例子:

1 6 9 12 2 3 4 15

1、递归到底,将数组分每个元素为一个子序列,显然有序

2、退上一层,两个一组,1 6 | 9 12 | 2 3 | 4 15,依然有序

3、退上一层,四个一组,1 6 9 12 | 2 3 4 15

4、退上一层,8个一组,1 2 3 4 6 12 15

坑爹啊~这不是和哥的算法一样么。。。。其实是这样的当只有偶数个的时候,就退化为第二个算法了。

因为其每次的分组刚好是有序的,只有退回至最上层的时候,才需要真正移位。

再考虑 1 ,6 ,9 , 2 ,3,4 ,15

1、递归到底,将数组分每个元素为一个子序列,显然有序

2、退上一层,两个一组,1 6 | 9  2 | 3 4 | 15=>1 6 | 2 9 | 3 4 | 15

3、退上一层,四个一组,1 2 6 9 | 3 4 15

4、退上一层,7个一组,1 2 3 4 6 9 15

其实回想起来,没见得算法三很好,而且使用了两个递归。至于效率上,隐约感觉算法三稍比算法二的好;

posted @ 2013-04-04 20:16  legendmaner  阅读(1947)  评论(0编辑  收藏  举报