程序员编程艺术1:左旋转字符串

来源:互联网 发布:照片打印软件下载 编辑:程序博客网 时间:2024/05/07 15:52
程序员编程艺术系列(简称TAOPP系列),围绕“面试”、“算法”、“编程”三个主题,注重提高广大初学者的编程能力,以及如何运用编程技巧和高效的算法解决实际应用问题。感谢“研究者July“分享的博客,这也是我一个朋友给我推荐他的博客,阅读同时实践,为了加深自己的理解,学习后写个博客总结下还是很有必要。
第一章是字符串的左旋转操作:
题目描述:
定义字符串的左旋转操作:把字符串前面的若干个字符移动到字符串的尾部,如把字符串abcdef左旋转2位得到字符串cdefab。
请实现字符串左旋转的函数,要求对长度为n的字符串操作的时间复杂度为O(n),空间复杂度为O(1)。
看到这题目,我首先想了一下,好像不复杂,比如:n个字符要左移m位。
思路1:首先想到的一种处理方式是通过临时字符数组,保存左边的m个字符,然后其他的都已到左移m位,然后把临时数组的m写道最右边。这种算法时间复杂度为O(n+m)
void StringReverse::LeftShitChar2(string &str,int m){int length=str.length();if(length<=0 || length<=m){return;}char temp[100];for(int i=0;i<m;i++){temp[i]=str[i];}for(int j=0;j<(length-m);j++){str[j]=str[j+m];}for(int k=length-m;k<length;k++){str[k]=temp[k-length+m];}}
然后看了下其他思路,我都自己写了下,接着下面就来。
思路2:比较简单的思路,要左移m位,可以没吃左移1位总共要移n-1个字符,循环m次就行了。
时间复杂度O(n*m);
void leftshiftone(char *s,int n) {        char t = s[0];       for (int i = 1; i < n; ++i) {            s[i - 1] = s[i];        }        s[n - 1] = t;    }  void leftshift(char *s,int n,int m) {        while (m--) {            leftshiftone(s, n);        }    }     
思路3:指针翻转放(我理解跳跃式移位,类似希尔排序思想,在思路2基础上改进的,减少移位次数)。时间复杂度是O(m+n)
引入原作者的解释:
咱们先来看个例子,如下:abc defghi,若要让abc移动至最后的过程可以是:abc defghi->def abcghi->def ghiabc
如此,我们可定义俩指针,p1指向ch[0],p2指向ch[m];
一下过程循环m次,交换p1和p2所指元素,然后p1++, p2++;。
- 第一步,交换abc 和def ,abc defghi->def abcghi
- 第二步,交换abc 和 ghi,def abcghi->def ghiabc
 整个过程,看起来,就是abc 一步一步 向后移动
- abc defghi
- def abcghi
- def ghi abc
//最后的 复杂度是O(m+n)
但如果是abcdefghijk,通过上面移位后会有剩余接下来又2种处理方式
方式1:移位后变成def ghi abc jk
当p1指向a,p2指向j时,由于p2+m越界,那么此时p1,p2不要变
这里p1之后(abcjk)就是尾巴,处理尾巴只需将j,k移到abc之前
方式2:def ghi abc jk

当p1指向a,p2指向j时,那么交换p1和p2,
此时为:
def ghi jbc ak

p1++,p2++,p1指向b,p2指向k,继续上面步骤得:
def ghi jkc ab

p1++,p2不动,p1指向c,p2指向b,p1和p2之间(cab)也就是尾巴,
那么处理尾巴(cab)需要循环左移一定次数

下面使用方式1实现代码:
void StringReverse::LeftShitChar(string &str,int m){int length=str.length();if(length<=0 || length<=m){return;}int c=length/m;int p1=0,p2=m;for (int i=0;i<(c-1)*m;i++){swap(str[p1],str[p2]);p1++;p2++;}int remain=length-p2;while(remain--){                int i=p2;while(i>p1){swap(str[i],str[i-1]);i--;}p1++;p2++;}}

思路3:使用递归,很经典的思路,把一个规模为N的问题化解为规模为M(M<N)的问题。时间复杂度O(n)
    举例来说,设字符串总长度为L,左侧要旋转的部分长度为s1,那么当从左向右循环交换长度为s1的小段,直到最后,由于剩余的部分长度为s2(s2==L%s1)而不能直接交换。
    该问题可以递归转化成规模为s1+s2的,方向相反(从右向左)的同一个问题。随着递归的进行,左右反复回荡,直到某一次满足条件L%s1==0而交换结束。
     举例解释一下:
    设原始问题为:将“123abcdefg”左旋转为“abcdefg123”,即总长度为10,旋转部("123")长度为3的左旋转。按照思路二的运算,演变过程为“123abcdefg”->"abc123defg"->"abcdef123g"。这时,"123"无法和"g"作对调,该问题递归转化为:将“123g”右旋转为"g123",即总长度为4,旋转部("g")长度为1的右旋转。

举个具体事例说明,如下:
1、对于字符串abc def ghi gk,
将abc右移到def ghi gk后面,此时n = 11,m = 3,m’ = n % m = 2;
abc def ghi gk -> def ghi abc gk
2、问题变成gk左移到abc前面,此时n = m’ + m = 5,m = 2,m’ = n % m 1;
abc gk -> a gk bc
3、问题变成a右移到gk后面,此时n = m’ + m = 3,m = 1,m’ = n % m = 0;
a gk bc-> gk a bc。 由于此刻,n % m = 0,满足结束条件,返回结果。
即从左至右,后从右至左,再从左至右,如此反反复复,直到满足条件,返回退出。
void ShitChar(string &str,int n,int m,int head,int tail,bool flag){if(flag==true){int k=n-m-n%m;int p1=head;int p2=head+m;for(int i=0;i<k;i++){swap(str[p1+i],str[p2+i]);head++;}if(n%m>0){ShitChar(str,n-k,n%m,head,tail,false);}}else{int k=n-m-n%m;int p1=tail;int p2=tail-m;for(int i=0;i<k;i++){swap(str[p1-i],str[p2-i]);tail--;}if(n%m>0){ShitChar(str,n-k,n%m,head,tail,true);}}}void StringReverse::LeftShitChar3(string &str,int m){int length=str.length();if(length<=0 || length<=m){return;}int head=0;int tail=length-1;bool flag=true;ShitChar(str,length,m,head,tail,true);}

思路4:循环移位(gcd),非常经典的方法,我看了好几遍才明白,时间复杂度(n)
我理解是将1个字符串分为几条循环链(>1 且 <=m),每条循环链2个节点之间的距离都是m,每条链每个字符都只需左移一位就好了。关键在于有多少条,经过数学推理,有gcd(n,m)条,即n,m的最大公约数条。
跟详细的解释见原文:http://blog.csdn.net/v_july_v/article/details/6322882
int gcd(int a,int b){if(a%b==0){return b;}else {    return gcd(b,a%b);}}void StringReverse::LeftShitChar4(string &str,int m){int lenOfStr = str.length();       int numOfGroup = gcd(lenOfStr, m);       int elemInSub = lenOfStr / numOfGroup;        for(int j = 0; j < numOfGroup; j++)             {           char tmp = str[j];           for (int i = 0; i < elemInSub - 1; i++)      {            str[(j + i * m) % lenOfStr] = str[(j + (i + 1) * m) % lenOfStr];  }        str[(j + i * m) % lenOfStr] = tmp;       }  }

思路5:三步旋转法,也是非常经典的方法,时间复杂度(n)
abcdef 这个例子来说,若要让def翻转到abc的前头,那么只要按下述3个步骤操作即可:
1、首先分为俩部分,X:abc,Y:def;
2、X->X^T,abc->cba, Y->Y^T,def->fed。
3、(X^TY^T)^T=YX,cbafed->defabc,即整个翻转。
void invest (string &str,int begin,int end){char temp;int  number=end-begin;    while(begin<=end){temp=str[begin];str[begin]=str[end];str[end]=temp;begin++;end--;}}void StringReverse::LeftShitChar5(string &str,int m){int length=str.length();invest(str,0,m-1);invest(str,m,length-1);invest(str,0,length-1);}
总结比较时间复杂度:
1.循环链法=递归发=三步翻转法=O(n)
2.指针翻转法= 临时数组法=O(n+m)
3.循环移位法=O(n*m)

0 0
原创粉丝点击