原地矩阵转置 微软面试题
昨天微软面试遇到的一道题:
题意大约是这样:一个m×n的矩阵保存在一个一维数组里,然后要求空间复杂度不超过O(n)的条件下完成对它的转置,转置结果还是保存在这个数组里。
因为如果没有空间的限制时,转置的复杂度为O(n×m),所以,拿到这题我就本能的想有没有什么巧妙的方法在O(n)的空间下时间也是O(m×n)。想了半天没想出来,只想到了一个O(m×m×n)的算法。
算法思想大致是,每次完成一行的转置,把转置后的一行n个元素放到辅助空间,然后所有元素像后平移把空出来的位置填满,然后前面自然就是n个空位,然后把这n个元素插入。这样m-1次插入后,矩阵完成转置。
算法是下如下:
#include<iostream> using namespace std; #define MAX_LEN 10000 int p[MAX_LEN]; //时间复杂度O(m*m*n) //空间复杂度O(n) void transposition(int *p,int m,int n) { int s,e,step,len = m * n; int *buffer = new int[n]; //只使用了O(n)的空间 for( s = 0; m > 1; m-- , s += n) { for( int i = 0; i < n; i++) buffer[i] = p[s+i*m]; for( e = len - m, step = 1; step < n; step++) { for(int i = 1 ; i < m; i++,e--) p[e] = p[e-step]; } for( int i = 0; i < n; i++) p[s+i] = buffer[i]; } delete []buffer; } int main() { int n,m,cnt; while(scanf("%d%d",&m,&n)!=EOF) { cnt = 0; for(int i = 0; i < n; i++) for(int j = 0; j < m; j++) p[i*m+j] = cnt++; transposition(p,m,n); for(int i = 0; i < m; i++) { for(int j = 0; j < n; j++) printf("%4d",p[i*n+j]); printf("\n"); } } return 0; }
跟面试官描述完这个算法后,我自己觉得很没底,因为这个算法可以说一点儿也不巧妙,速度也不快。面试官好像也觉得这个算法效率不高。我说我再想想有没有更好的算法。
然后面试官提示说这题用O(1)的空间复杂度也可以实现,由于一开始给了O(n)这个限制,我就一直在想如何充分利用这些空间提高效率。没有考虑O(1)来实现,但是被他这么一提示,我很快就反应到,矩阵转置后的每一个位置都是确定的,所以,可以算出来每一个点应该移到哪里,但是不能直接移过去,因为那个位置有别的点,那就把那个点移到它该去的位置,依次类推,必然形成一个环,经过这么一次循环,这一个环上的元素就都在自己该在的位置了。多么完美啊,这不就是O(m×n)么,但是仔细想想是不对的,因为一个矩阵可能包含多个环。那么就要判断当前位置是否在已经遍历过的环上了。
然后我就一直围绕着O(m×n)想怎么判断,如何能找到环的规律,想了半天也没想出来。
最后,面试结束,面试官描述了一下他的算法,从左到右遍历,如果发现有一个节点的环中存在它右边的节点,则这个环遍历过。当时比较混乱,只是大致知道他的意思,回来之后写出了如下代码:
#include<iostream> using namespace std; #define MAX_LEN 10000 int p[MAX_LEN]; //时间复杂度O((m*n)^2) //空间复杂度O(1) void transposition(int *p,int m,int n) { int len = m * n,a,b,t; for(int i = 0; i < len; i++) //cnt==len代表所有位置都已转置无需再循环 { for( b = m*(i%n)+(i/n); b > i; b = m*(b%n)+(b/n)); if( b == i ) //如果这个环回到了一个小于i的位置,证明这个环已经遍历过 { t = p[i]; for( a = i, b = m*(i%n)+(i/n); b != i; b = m*(a%n)+(a/n)) //将这个环循按位置排好 { p[a] = p[b]; a = b; } p[a] = t; } } } int main() { int n,m,cnt; while(scanf("%d%d",&m,&n)!=EOF) { cnt = 0; for(int i = 0; i < n; i++) for(int j = 0; j < m; j++) p[i*m+j] = cnt++; transposition(p,m,n); for(int i = 0; i < m; i++) { for(int j = 0; j < n; j++) printf("%4d",p[i*n+j]); printf("\n"); } } return 0; }
但是写完之后我发现这个代码的复杂度是O((m×n)^2)的,如果分析的仔细一点来说,这个算法的复杂度是m×n×(所有环的长度的平方和),由于环的分布我们是不知道的,起码我是没有找到规律,所有环的长度和m×n,所以这个复杂度的上界为O((m×n)^2)。
如果有神牛可以找到环的分布规律,证明一个更低的复杂度下界请给我留言。
如果有神牛可以找到每一个环的最左元素的递推式,这个问题就可以在O(m×n)的时间复杂度内解,那么你务必要给我留言。
所以,综上所述,这个算法确实比我的算法还要慢。
然后我就在网上各种百度,矩阵原地转置,也没有发现O(1)空间复杂度低于O((m*n)^2)的算法,如果有神牛知道,请给我留言。
总结:面试完,问了问在微软的朋友,朋友说这样评分,先看有没有给出正确答案,然后再看能不能在正确答案的基础上进行优化。我觉得这次面试发挥不好主要就是选错了答题策略,一开始就想最优的解法是什么。应该先快速的想一个简单而且正确的解法,回答面试官,如果面试官问你能否优化,再去考虑优化,因为有的题可能就没什么好优化的,比方说这题。我最初的想法好像也不像我想象中的效率那么低,到目前为止我也没有找到更好的方法。