【算法05】左旋转字符串

题目:字符串的左旋转操作:把字符串前面的若干字符移到字符串的后面。例如:字符串abcdefg左旋转2位得到cdefgab。请实现左旋转字符串函数,要求对于长度为n的字符串,时间复杂度为O(n),辅助内存为O(1)。

分析1:看到这道题我们最直观的思路就是:如果要把长度为n的字符串左移k位,那么我们可以动态分配长度为k的临时数组,存储前面k个字符,然后将后面的字符逐一向前移动k位,然后将临时数组中的字符拷贝到后面k位。但是这种方法使用的k个额外位置产生了过大的存储空间的消耗。

分析2:我们也可以定义一个子函数,该函数的功能是将一个字符串左旋转1位(其时间正比于n),然后调用该函数k次。但是该方法又产生了过多的时间消耗。

分析3:要在题目限定的范围内解决该问题,我们有一个著名的“杂技算法”:移动S[0]到临时变量t,然后移动S[K]到S[0];移动S[2K]到S[K],依此类推(将S中的所有下标对n取模),直到返回到取S[0]中的元素,此时改为从t取值然终止过程。当n为12,k为3时,元素按如下顺序移动(如下图,画得太丑了,莫怪)。如果该过程没有移动全部的元素,则从S[1]开始再次移动,直到所有的元素都已经移动位置。

根据上面的思路,我们可以写出如下的代码:

 1 #include<iostream>
2 #include<string>
3 using namespace std;
4
5 //求两个整数i,j的最大公约数
6 int gcd(int i,int j)
7 {
8 while(i != j)
9 {
10 if(i > j)
11 i -= j;
12 else
13 j -= i;
14 }
15 return i;
16 }
17
18
19 //左旋转字符串函数,将pStr字符串向左旋转k位,
20 //first指向pStr[i],second指向pStr[2i],以此类推
21 char* LeftRotateString(char* pStr,int k)
22 {
23 if(pStr == NULL || k < 1)
24 return 0;
25
26 int n = strlen(pStr);
27
28 //共进行i趟循环,i为n,k的最大公约数
29 for(int i = 0;i < gcd(n,k);++i)
30 {
31 char temp = pStr[i];
32 int first = i;
33 while(1)
34 {
35 int second = (first + k) % n;
36
37 if(second == i)
38 break;
39
40 pStr[first] = pStr[second];
41 first = second;
42 }
43
44 //将临时变量中的字符存至每一趟的循环的最后一个字符中
45 pStr[first] = temp;
46 }
47 return pStr;
48 }
49
50 int main()
51 {
52 string demo("abcdefgh");
53 cout<<demo<<endl;
54
55 LeftRotateString(&demo[0],3);
56
57 cout<<demo<<endl;
58 return 0;
59 }

运行结果如下:

效率分析:很显然这个算法的空间复杂度是O(1),看似是for循环中嵌套了while循环,然而仔细想想不难发现,这个算法实际上只遍历了一个数组,只不过采取的遍历方式是“跨越式”访问的,所以它的时间复杂度是O(n),满足题目的要求,是一个比较好的算法了。

 

分析4:我们可以将该问题看成是数组ab转换成ba,同时假定我们有一个子函数可以将数组中的所有元素求逆序。显然有(a^r)^r=a,即对一个数组a求逆再求逆等于它本身。所以我们从ab开始,首先对a求逆,得到a^r,然后对b求逆得到b^r。最后整体求逆,得到(a^r*b^r)^r=ba。这正是我们要的结果!

于是我们有如下代码:

 1 #include<string>
2 #include<iostream>
3 using namespace std;
4
5 //对一个数组中的所有元素求逆,即把abcde变为edcba
6 void ReverseString(char *pStart,char *pEnd)
7 {
8 if(pStart != NULL && pEnd != NULL)
9 {
10 while(pStart <= pEnd)
11 {
12 char temp = *pStart;
13 *pStart = *pEnd;
14 *pEnd = temp;
15
16 ++pStart;
17 --pEnd;
18
19 }
20 }
21 }
22
23
24 //把字符串pStr左旋转k位
25 char* LeftRotateString(char* pStr, int k)
26 {
27 if(pStr != NULL && k > 0)
28 {
29 int n = strlen(pStr);
30 if(n > 0 && k < n)
31 {
32 char* pFirstStart = pStr;
33 char* pFirstEnd = pStr+k-1;
34 char* pSecondStart = pStr+k;
35 char* pSecondEnd = pStr+n-1;
36
37 //求第一部分字符串的逆,即求a^r
38 ReverseString(pFirstStart,pFirstEnd);
39 //求第二部分字符串的逆, 即求b^r
40 ReverseString(pSecondStart,pSecondEnd);
41 //求前两部分整体的逆, 即求(a^r*b^r)^r
42 ReverseString(pFirstStart,pSecondEnd);
43 }
44 }
45
46 return pStr;
47 }
48
49
50 int main()
51 {
52 string demo("abcdefgh");
53 cout<<demo<<endl;
54
55 LeftRotateString(&demo[0],3);
56
57 cout<<demo<<endl;
58 return 0;
59
60 }

运行结果如下:

效率分析:“翻转代码在时间和空间上效率都很高,而且代码非常的简短,很难出错”(出自《编程珠玑》)。实际上由于要进行ab的求逆运算,对a,b分别求逆的时候相当于遍历一遍数组,而对整体求逆的时候又要遍历一边数组,这样整体看来对于长度为n的字符串需要遍历两遍,因此时间复杂度为O(n)。但是翻转代码不需要额外分配新的内存,一个额外的空间都不需要。

 

参考文献:

【1】Jon Bentley.  《编程珠玑(第二版)》.  人民邮电出版社,2008      p13-p15

【2】程序员面试题精选100题:http://zhedahht.blog.163.com/blog/static/2541117420073993725873/

 

注:

1)本博客所有的代码环境编译均为win7+VC6。所有代码均经过博主上机调试。

2)博主python27对本博客文章享有版权,网络转载请注明出处http://www.cnblogs.com/python27/。对解题思路有任何建议,欢迎在评论中告知。

 

posted @ 2011-11-28 15:36  python27  阅读(2254)  评论(8编辑  收藏  举报