不改变正负数之间相对顺序重新排列数组.时间O(N),空间O(1)

原题是这样的:

一个未排序整数数组,有正负数,重新排列使负数排在正数前面,并且要求不改变原来的正负数之间相对顺序。
比如: input: 1,7,-5,9,-12,15 ,ans: -5,-12,1,7,9,15 。且要求时间复杂度O(N),空间O(1) 。

一道曾经做过n次的题目,当时做时没有“要求不改变原来的正负数之间相对顺序”条件,几天都没有做些虐心的算法题了,被虐贯了%>_<%

一看题目先把去掉那个附加条件的经典题目扫一遍。思想简单利用快排的分类:

简单到都不愿写code:

 1 void func30(int a[], int n)
 2 {
 3     int i, j, tmp;
 4 
 5     assert(n>0);
 6     i=0;
 7     j=n-1;
 8     while(i<j)
 9     {
10         while(i<j && a[i]<0)
11         {
12             i++;
13         }
14         while(i<j && a[j]>0)
15         {
16             j--;
17         }
18         if (i<j)
19         {
20             tmp = a[i];
21             a[i] = a[j];
22             a[j] = tmp;
23             i++;
24             j--;
25         }
26     }
27 }

很显然,以前的算法是不能保证“不改变原来的正负数之间相对顺序”的条件的,思考快排的稳定性问题即可明白。

加入附加条件后,根据稳定性的特性,一般的比较相邻的元素这样可以维持其稳定的特性。冥想良久,想起过去做过这么一道什么题目,可以从中吸取思想:

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

沿袭其思想,块移动(不晓得就参考刚那“合并”题目)块移动保证其局部稳定问题,只需从头到尾,一次遍历,当然算法复杂度:为O(nlogn),因为块移动中的时间复杂度为O(n),最坏情况退化为O(n^2)(考虑交替正负,具体还是参考“合并”题目的解释吧)。

然后。。然后就没有然后了,想不到O(n)的了。

回到这道题目的原文解释(5种方案,未能完美解决):

    1. 最简单的,如果不考虑时间复杂度,最简单的思路是从头扫描这个数组,每碰到一个正数时,拿出这个数字,并把位于这个数字后面的所有数字往前挪动一位。挪完之后在数组的末尾有一个空位,这时把该正数放入这个空位。由于碰到一个正,需要移动O(n)个数字,因此总的时间复杂度是O(n2)。
    2. 既然题目要求的是把负数放在数组的前半部分,正数放在数组的后半部分,因此所有的负数应该位于正数的前面。也就是说我们在扫描这个数组的时候,如果发现有正数出现在负数的前面,我们可以交换他们的顺序,交换之后就符合要求了。
      因此我们可以维护两个指针,第一个指针初始化为数组的第一个数字,它只向后移动;第二个指针初始化为数组的最后一个数字,它只向前移动。在两个指针相遇之前,第一个指针总是位于第二个指针的前面。如果第一个指针指向的数字是正而第二个指针指向的数字是负数,我们就交换这两个数字。
      但遗憾的是上述方法改变了原来正负数之间的相对顺序。所以,咱们得另寻良策
    3. 首先,定义这样一个过程为“翻转”:(a1,a2,...,am,b1,b2,...,bn) --> (b1,b2,...,bn,a1,a2,...,am)。其次,对于待处理的未排序整数数组,从头到尾进行扫描,寻找(正正...正负...负负)串;每找到这样一个串,则计数器加1;若计数为奇数,则对当前串做一个“翻转”;反复扫描,直到再也找不到(正正...正负...负负)串。

      此思路来自朋友胡果果,空间复杂度虽为O(1),但其时间复杂度O(N*logN)。更多具体细节参看原文:http://qing.weibo.com/1570303725/5d98eeed33000hcb.html。故,不符合题目要求,继续寻找。

    4. 我们可以这样,设置一个起始点j, 一个翻转点k,一个终止点L,从右侧起,起始点在第一个出现的负数, 翻转点在起始点后第一个出现的正数,终止点在翻转点后出现的第一个负数(或结束)。

      如果无翻转点, 则不操作,如果有翻转点, 则待终止点出现后, 做翻转, 即ab => ba 这样的操作。翻转后, 负数串一定在左侧, 然后从负数串的右侧开始记录起始点, 继续往下找下一个翻转点。

      例子中的就是(下划线代表要交换顺序的两个数字):

      1, 7, -5, 9-12, 15  
      第一次翻转: 1, 7, -5, -12,9, 15   =>  1, -12, -5, 7, 9, 15
      第二次翻转: -5, -12, 1, 7, 9, 15

      此思路2果真解决了么?NO,用下面这个例子试一下,我们就能立马看出了漏洞:
      1, 7, -5, -6, 9-12, 15(此种情况未能处理)
      7 -5 -6 -12 9 15
      -12 -5 -6 7 9 15
      -6 -12 -5 1 7 9 15   (此时,正负数之间的相对顺序已经改变,本应该是-5,-6,-12,而现在是-6 -12 -5)
    5. 看来这个问题的确有点麻烦,不过我们最终貌似还是找到了另外一种解决办法,正如朋友超越神所说的:从后往前扫描,遇到负数,开始记录负数区间,然后遇到正数,记录前面的正数区间,然后把整个负数区间与前面的正数区间进行交换,交换区间但保序的算法类似(a,bc->bc,a)的字符串原地翻转算法。交换完之后要继续向前一直扫描下去,每次碰到负数区间在正数区间后面,就翻转区间。下面,将详细阐述此思路4。

结果哥一跳直接跳到了,原文的第五种方案了,看了做了些题目还是有点用的O(∩_∩)O。原文的解释和我想的也一致的。所以更多解释还是看原文吧。至于源码就修改一下合并两个有序的子序,要求空间复杂度为O(1) 里面的源码就可以用了,所以就给出了。

有种冲动,想认真研读然后证明这题是无解的(不知道是否真的是无解),最后还是放弃了这个想法,不折腾了。原文给的一个提示:本题实质性上只是一个排列,重新组合问题,与排序无关。参考文献:《STABLE MINIMUM SPACE PARTITIONING IN LINEAR TIME》

P.S. 欲纳百川,以通万术;资质愚笨,却未能长其一技;今其返,专一为之;

参考文献:http://blog.csdn.net/v_july_v/article/details/7506231

相关问题:重排问题

posted @ 2013-04-22 10:39  legendmaner  阅读(1798)  评论(0编辑  收藏  举报