在这篇文章:九月腾讯,创新工场,淘宝等公司最新面试十三题的第5题(一个未排序整数数组,有正负数,重新排列使负数排在正数前面,并且要求不改变原来的正负数之间相对顺序),自从去年九月收录了此题至今,一直未曾看到令人满意的答案,为何呢?
因为一般达不到题目所要求的:时间复杂度O(N),空间O(1),且保证原来正负数之间的相对位置不变。本编程艺术系列第27章就来阐述这个问题,若有任何漏洞,欢迎随时不吝指正。谢谢。
原题是这样的:
一个未排序整数数组,有正负数,重新排列使负数排在正数前面,并且要求不改变原来的正负数之间相对顺序。
比如: input: 1,7,-5,9,-12,15 ,ans: -5,-12,1,7,9,15 。且要求时间复杂度O(N),空间O(1) 。
OK,下面咱们就来试着一步一步解这道题,如下5种思路(从复杂度O(N^2)到O(N*logN),从不符合题目条件到一步步趋近于条件)):
此思路来自朋友胡果果,空间复杂度虽为O(1),但其时间复杂度O(N*logN)。更多具体细节参看原文:http://qing.weibo.com/1570303725/5d98eeed33000hcb.html。故,不符合题目要求,继续寻找。
如果无翻转点, 则不操作,如果有翻转点, 则待终止点出现后, 做翻转, 即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(此种情况未能处理)
1 7 -5 -6 -12 9 15
1 -12 -5 -6 7 9 15
-6 -12 -5 1 7 9 15 (此时,正负数之间的相对顺序已经改变,本应该是-5,-6,-12,而现在是-6 -12 -5)
其实上述思路5非常简单,既然单个翻转无法解决问题,那么咱们可以区间翻转阿。什么叫区间翻转?不知读者朋友们是否还记得本blog之前曾经整理过这样一道题,微软面试100题系列第10题,如下:
10、翻转句子中单词的顺序。
题目:输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。句子中单词以空格符隔开。为简单起见,标点符号和普通字母一样处理。例如输入“I am a student.”,则输出“student. a am I”。而此题可以在O(N)的时间复杂度内解决:
由于本题需要翻转句子,我们先颠倒句子中的所有字符。这时,不但翻转了句子中单词的顺序,而且单词内字符也被翻转了。我们再颠倒每个单词内的字符。由于单词内的字符被翻转两次,因此顺序仍然和输入时的顺序保持一致。
以上面的输入为例:翻转“I am a student.”中所有字符得到“.tneduts a ma I”,再翻转每个单词中字符的顺序得到“students. a am I”,正是符合要求的输出(编码实现,可以参看此文:http://zhedahht.blog.163.com/blog/static/254111742007289205219/)。
对的,上述思路3就是这个意思,单词翻转便相当于于区间翻转,既如此,咱们来验证下上述思路2中那个测试用例,如下:
1, 7, -5, -6, 9, -12, 15
1 7 -5 -6 -12 9 15
-12 -6 -5 7 1 9 15 (借用单词翻转的方法,先逐个数字翻转,后正负数整体原地翻转)
-5 -6 -12 1 7 9 15
但是,我还想再问,问题至此被解决了么?真的被KO了么?NO,咱们来看这样一种情况,正如威士忌所说:
看看这个数据,+-+-+-+-+------+,假如Nminus 等于 n/2,由于前面都是+-+-+-,区间交换需要 n/2/2 = n/4次,每次交换是 T(2*(Nminus + Nplus)) >= T(n),n/4 * T(n) = T(n*n/4)=O(n^2)。
还有一种更坏的情况,就是+-+-+-+-+------+这种数据可能,后面一大堆的负数,前面正负交替。所以,咱们的美梦再次破灭,路漫漫其修远兮,此题仍然未找到一个完全解决了的方案。
下面公开征集解决思路:如果你能想到完全满足和符合题目三个条件的思路(不改变相对顺序&时间O(N)&空间O(1)),欢迎随时在本文下留言或评论,或者发至我邮箱:[email protected]。
如果验证完全正确属实,或者要么你就证明在那三个条件下此题无解,当然,你若提出了本文内没有的思路,虽然严格论证下可能并不符合题目三个条件,但我将依然邀请您作为听众来参加读书会第2期(到场嘉宾可能包括为pongba,xlvector,penny,以届时现场为准),再或者,你能指出本文文末updated部分:本题思路征集中的那些思路的不符要求与错误,也行,限前10位,额满为止。
具体时间、场地待后续确定(微博上和本blog内会届时通知)。
updated:本题思路征集中之一解一点评
关于本题不改变正负数相对顺序重新排列数组,陆陆续续有不少朋友或发来了邮件,或在本文评论下提供了他们自己的思路或解法,思维理性碰撞,共同享受思考的乐趣,我觉挺有意思,精选其中一些解答贴出来,让大家评判、讨论,如下:
很高兴看到你的问题,真的很有意思!
我这里想到一种解法,主要的思路是通过改变数字内容来实现保留数字之间相对的顺序:
比方说数列 3 4 -1 -3 5 2 -7 6 1
那负数相对顺序是: -1 -3 -7, 正数是 3 4 5 2 6 1
我们可以做变形:
负数成为 -1.1 -2.3, -3.7 (整数部分为相对顺序,小数部分为原来的数字)
同理 正数为: 1.3 2.4 3.5 4.2 5.6 7.1
现在数组变成: 1.3 2.4 -1.1 -2.3 3.5 4.2 -3.7 4.6 5.1
接下去 先通过置换把所有负数排到前面:具体方法为从前往后扫描数组,每碰到一个负数 就和数组最前面的正数交换, 结果如下:
-1.1 -2.3 -3.7 2.4 3.5 4.2 1.3 4.6 5.1
可以看到负数部分已经完成题目要求(只需要把整数部分去掉即可),接下去对于正数数列 2.4 3.5 4.2 1.3 4.6 5.1, 也用类似的方法还原先前的顺序:具体方法为一次遍历每个数字,检查其整数部分是否与其所在的位置相同,如不相同,将该数字与位置为该数字整数部分的交换, 比如说2.4 整数部分为2,但是现在位于数列第一位,所以与位于第二位的3.5交换,得到:3.5 2.4 4.2 1.3 4.6 5.1(最多只需要O(n)因为每次交换都保证一个数字回到原来的位置,而总共有n个数字),最后和前面负数的处理相同,即去掉整数部分(+1,邀请来参加读书会第2期)。
点评:但此方法在本文评论下马上有人指出:不过,整数变成浮点数,存储空间要扩大一倍,跟申请一个大小为n的数组一个道理,空间复杂度O(N)不符要求。更多请看本文评论下第18楼。(zj060607 & topskycen,+2)。
July巨巨,
由于在csdn上那贴删改留言次数有点多,csdn不让留言了,就发邮件给您吧。应该是最终稿了。
稍微改动下Muqi的方法,可以得到平均时间O(n),最坏时间O(n^2),空间复杂度O(1)的。当把负数放到数列前半部分操作时,这个负数是和前面的一个正数交换的。交换过后,把这个正数变成他的相反数(5变成-5这种)。那么当第一轮把负数放到前面过后,剩下的部分又形成了一个相同的子问题。当然,后面几轮把负数放到前面后,得把他们重新恢复成相应的正数。
还是用3 4 -1 -3 5 2 -7 6 1为例子:
第一轮: -1 -3 -7 [ -3 -4 2 -5 6 1].
第二轮: -1 -3 -7 -(-3) -(-4) -(-5) [-2 6 1].
第三轮: 1 3 7 3 4 5 2 6 1
最终添上-号,-1 -3 -7 3 4 5 2 6 1
平均时间复杂度(假设数组是随机的):
T(n)=T(n/2)+O(n). T(n)=O(n).
如果遇到+++++...+-这种情况,就会导致最坏的时间复杂度。
这也不算是完美的解法。有点怀疑完美的解是不存在的,但不知道怎么证明。
谢谢,
mourisl
点评:from litaoye:我的想法(见下文之综合点评)也许同上述解法2类似,但我确实没看明白解法2的操作过程,并且我认为解法2十有八九是错的。举个例子来说,如
1,2,-4,-5,3,-6
-4,2,1,-5,3,-6
-4,-5,1,-2,3,-6
-4,-5,-6,-2,3,-1
这样的话-2同-1的顺序就乱了。更多请看本文评论下第29楼。
点评:from 威士忌(+5),jiangbin00cn和Muqi的方法都很取巧。其实他们的方法都是压缩了整数值域或者扩大值域来保存附加信息,虽然符合时间O(N)和空间O(1)的要求,但是并不适用所有int值。
这些方法的思路其实很简单,比如:
num[] = {1,7,-5,9,-12,15};
pos[]={2,3,0,4,1,5};
pos的计算扫描2遍num数组即可,有了pos数组当然排序不成问题。
关键解决pos空间问题时,两位做法分别是,Muqi保存到double浮点域,jiangbin00cn是利用进制方法保存到int高位(其实根本不需桶排了),更明显的做法就是flag(num[i])*pos[i]*1000+num[i]转换为3007,-0005,-1012。
很高兴看到如此让人眼前一亮的方法,但是仔细想想的话,就觉得还是不符合要求。
首先明确题目的题意要求空间复杂度是O(1),我的理解就是只能有一个空间来存储数据,其他的任何临时变量都不能出现,包括循环变量和临时开辟的空间(例如数字交换时)。
下边我的解法是在 允许自己输入数据,可以利用数组大小n的情况下产生的,只包含一个额外的变量。
用pos记录正数的最最左位置减一, a[pos]记录负数最右的位置加一
基本步骤:
1 让pos代表最后一个数据的位置
2 然后输入一个数据存储在最后的位置 a[pos]
3 如果输入的数据a[pos]是正数,我们就让pos = pos - 1;如果输入的是负数就把这个负数挪到前边,让a[pos]-1来记录负数最右的位置。
4 发生相应交换
代码在本文评论下第28楼。
综合点评
也有朋友反应,根据算导第8章中定理8.1:任意一个比较排序算法在最坏情况下,都需要nlgn次比较。即给定n个不同的输入元素,对于任何确定或随机的比较排序算法,其期望运行时间都有下界O(nlgn)。由此推出此题无解。但他们忽略了:不一定非要排序非要比较。
也就是说,尽管:
但本题统统与这些无关,因为追根究底,本题实质性上只是一个排列,重新组合问题,与排序无关。
更多还可参考此论文:《STABLE MINIMUM SPACE PARTITIONING IN LINEAR TIME》。待后续验证。