“《编程珠玑》(第2版)第2章”:B题(向量旋转)
B题是这样子的:
将一个n元一维向量向左旋转(即循环移位)i个位置。例如,当n=8且i=3时,向量abcdefgh旋转为defghabc。简单的代码使用一个n元的中间向量在n步内完成该工作。你能否仅使用数十个额外字节的存储空间,在正比于n的时间内完成向量的旋转?
以下题目的解答部分参考自一博文及该书参考答案。
分析:
“abcdefgh”严格来说并不是一个字符串,因为'\0'是不会移动的。为了叙述方便,可以把它认为是字符串,只是不对'\0'进行操作罢了。
如果不考虑时间要求为O(n),那么可以每次整体左移一位,一共移动i次。只使用O(1)的空间的条件下,一共要进行元素交换O(n*i)次;
如果不考虑空间要求为O(1),那么可以把前i个存入临时数组,剩下的左移i位,再把临时数组里的内容放入后i个位置中。
很可惜,由于两个限制条件,以上两种思路都不满足要求。
1. 法一:"杂技"代码
为了满足O(1)空间的限制,延续第一个思路,如果每次直接把原向量的一个元素移动到目标向量中它的应该出现新位置上就行了。先把array[0]保存起来,然后把array[i]移动到array[0]上,array[2i]移到array[i]上,直至返回取原先的array[0]。但这需要解决的问题是,如何保证所有元素都被移动过了?数学上的结论是,依次以array[0],...,array[gcd(i,n)-1]为首元进行循环即可,其中gcd(a,b)是a与b的最大公约数。
1 void cyclicShift(char * in, int len, int shift) 2 { 3 assert(len >= 0); 4 assert(shift >= 0); 5 if (shift == 0 || shift == len) 6 { 7 return; 8 } 9 if (shift > len) 10 { 11 shift = shift % len; 12 } 13 14 int k, j; 15 char temp; 16 int times = gcd(len, shift); 17 for (int i = 0; i < times; i++) 18 { 19 j = i; 20 temp = in[i]; 21 while (true) 22 { 23 k = (j + shift) % len; 24 if (k == i) break; 25 in[j] = in[k]; 26 j = k; 27 } 28 in[j] = temp; 29 } 30 } 31 32 int gcd(int a, int b) 33 { 34 assert(a > 0 && b > 0); 35 36 // Method1 37 // int r; 38 //while (b > 0) 39 //{ 40 // r = a%b; 41 // a = b; 42 // b = r; 43 //} 44 45 // Method2 46 while (a != b) 47 { 48 if (a > b) 49 a -= b; 50 else 51 b -= a; 52 } 53 54 return a; 55 }
正如“杂技”一词所暗示的一样,这个算法就像在玩杂耍球,你要让它们中的每一个都在合适的位置上,这些球,除了手中有一个,其它几个都在空中。如果不熟悉,很容易手忙脚乱,把球掉的满地都是。
2. 法二:求逆(推荐)
把原向量分为两部分a和b,分别对a、b求逆得到arbr,再对整体求逆便获得了ba!即(arbr)r = ba。代码如下:
1 void cyclicShift3(char * in, int len, int shift) 2 { 3 assert(len >= 0); 4 assert(shift >= 0); 5 if (shift == 0 || shift == len) 6 { 7 return; 8 } 9 if (shift > len) 10 { 11 shift = shift % len; 12 } 13 14 reverse(in, 0, shift - 1); 15 reverse(in, shift, len - 1); 16 reverse(in, 0, len - 1); 17 } 18 19 void reverse(char * in, int first, int last) 20 { 21 assert(first >= 0); 22 assert(last >= 0); 23 assert(first <= last); 24 25 char temp; 26 while (first < last) 27 { 28 temp = in[first]; 29 in[first] = in[last]; 30 in[last] = temp; 31 first++; 32 last--; 33 } 34 }
这种方法无疑既高效,也难以在编写时出错。有人曾主张把这个求逆的左旋方法当做一种常识。
来看看这种思想的应用吧:
扩展:(google面试题)用线性时间和常数附加空间将一篇文章的所有单词倒序。
举个例子:This is a paragraph for test
处理后: test for paragraph a is This
如果使用求逆的方式,先把全文整体求逆,再根据空格对每个单词内部求逆,是不是很简单?另外淘宝今年的实习生笔试有道题是类似的,处理的对象规模比这个扩展中的“一篇文章”小不少,当然解法是基本一样的,只不过分隔符不是空格而已,这里就不重述了。