左旋转字符串

微软面试题之一,难度系数高。

题目描述如下:

定义字符串的左旋转操作:把字符串前面的若干个字符移动到字符串的尾部。 
如把字符串abcdef 左旋转2 位得到字符串cdefab。请实现字符串左旋转的函数。
要求时间对长度为n 的字符串操作的复杂度为O(n),辅助内存为O(1)。


逻辑分析:

1、这是一道很经典,很精妙的算法题。而对于我来说,这一道题尤为深刻,原因在于这是大神July在程序员编程艺术系列中的开篇第一讲,其汪洋恣意,大开大合的文章让我沉醉其中,难以自拔。这里也强烈推荐各位看客百度或者google结构之法 算法之道,奇文共欣赏。

2、最简单粗暴的想法就是一位一位移过去,写一个完成此功能的子函数:‘

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;    
}   
当我们需要左移m位时,只需要循环调用m次,时间复杂度O(m*n),平均时间复杂度O(n^2),显然笨拙。


3、另一种比较好的方法则是将移动的单元看成一个整体,比如abc defghi,假使移动abc到最后,可以先让abc和def交换,然后abc再和ghi交换,如此,我们采用两个指针,分别指向两个移动的单位,每个过程为m次交换,整个过程看来,就是abc逐步后移,借用July大神的图解:

有一个显而易见的问题,那就是n%m==0并不是一直成立的,假使上面的例子,i的后方还有1~m-1个数据,如何处理?

如果abcdef ghij要变成defghij abc:
  abcdef ghij
1. def abc ghij
2. def ghi abc j      //接下来,j 步步前移
3. def ghi ab jc
4. def ghi a j bc
5. def ghi j abc 

依然借用July的图解:


也就是说,我们只需要在p2+m-1越界的时候,处理尾巴即可。由n-p2得到尾巴个数r,执行r次循环。

//copyright@July、颜沙    
//最终代码,July,updated again,2011.04.17。    
#include     
#include     
using namespace std;    
    
void rotate(string &str, int m)    
{    
        
    if (str.length() == 0 || m <= 0)    
        return;    
        
    int n = str.length();    
        
    if (m % n <= 0)    
        return;    
        
    int p1 = 0, p2 = m;    
    int k = (n - m) - n % m;    
        
    // 交换p1,p2指向的元素,然后移动p1,p2    
    while (k --)     
    {    
        swap(str[p1], str[p2]);    
        p1++;    
        p2++;    
    }    
        
    // 重点,都在下述几行。    
    // 处理尾部,r为尾部左移次数    
    int r = n - p2;    
    while (r--)    
    {    
        int i = p2;    
        while (i > p1)    
        {    
            swap(str[i], str[i-1]);    
            i--;    
        }    
        p2++;    
        p1++;    
    }    
    //比如一个例子,abcdefghijk    
    //                    p1    p2    
    //当执行到这里时,defghi a b c j k    
    //p2+m出界 了,    
    //r=n-p2=2,所以以下过程,要执行循环俩次。    
        
    //第一次:j 步步前移,abcjk->abjck->ajbck->jabck    
    //然后,p1++,p2++,p1指a,p2指k。    
    //               p1    p2    
    //第二次:defghi j a b c k    
    //同理,此后,k步步前移,abck->abkc->akbc->kabc。    
}    
    
int main()       
{       
    string ch="abcdefghijk";       
    rotate(ch,3);       
    cout<


4、对上述尾巴问题,有一位道友的思想很好(摘录原说法):

把一个规模为N的问题化解为规模为M(M     举例来说,设字符串总长度为L,左侧要旋转的部分长度为s1,那么当从左向右循环交换长度为s1的小段,直到最后,由于剩余的部分长度为s2(s2==L%s1)而不能直接交换。

    该问题可以递归转化成规模为s1+s2的,方向相反(从右向左)的同一个问题。随着递归的进行,左右反复回荡,直到某一次满足条件L%s1==0而交换结束。


实际上,就是当我们处理到def ghi abc jk时,将abcjk看做相同的子问题,只是规模和方向发生变化,看成右旋,m=2的子问题,abcjk=>ajkbc,而当处理到ajk时,问题规模继续下降,方向左旋,m=1,即为jak->jka。(July原文所用为n=10的例子,不便于说明,个人举了一个更好的例子^.^)


实质:递归的将问题规模逐渐变小。


今天再度翻阅July的该文章时,惊奇的发现竟然补充了实例。

举个具体事例说明,如下:

1、对于字符串abc def ghi gk

abc右移到def ghi gk后面,此时n = 11m = 3m = n % m = 2;

abc def ghi gk -> def ghi abc gk

2、问题变成gk左移到abc前面,此时n = m + m = 5,m = 2m = n % m 1;

abc gk -> a gk bc

3、问题变成a右移到gk后面,此时n = m + m = 3,m = 1m = n % m = 0;

a gk bc-> gk a bc。 由于此刻,n % m = 0,满足结束条件,返回结果

 

    即从左至右,后从右至左,再从左至右,如此反反复复,直到满足条件,返回退出。


//递归,    
//感谢网友Bluesmic提供的思路    
    
//copyright@ yansha 2011.04.19    
//July,updated,2011.04.20.    
#include     
using namespace std;    
    
void rotate(string &str, int n, int m, int head, int tail, bool flag)    
{    
    //n 待处理部分的字符串长度,m:待处理部分的旋转长度    
    //head:待处理部分的头指针,tail:待处理部分的尾指针    
    //flag = true进行左旋,flag = false进行右旋    
        
    // 返回条件    
    if (head == tail || m <= 0)    
        return;    
        
    if (flag == true)    
    {    
        int p1 = head;    
        int p2 = head + m;  //初始化p1,p2    
            
        //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    
        //(相信,经过上文中那么多繁杂的叙述,此类的转换过程,你应该是了如指掌了。)    
            
        int k = (n - m) - n % m;   //p1,p2移动距离,向右移六步    
    
        /*---------------------  
        解释下上面的k = (n - m) - n % m的由来:  
        yansha:  
        以p2为移动的参照系:  
        n-m 是开始时p2到末尾的长度,n%m是尾巴长度  
        (n-m)-n%m就是p2移动的距离  
        比如 abc def efg hi  
        开始时p2->d,那么n-m 为def efg hi的长度8,  
        n%m 为尾巴hi的长度2,  
        因为我知道abc要移动到hi的前面,所以移动长度是  
        (n-m)-n%m = 8-2 = 6。  
        */    
            
        for (int i = 0; i < k; i++, p1++, p2++)    
            swap(str[p1], str[p2]);    
            
        rotate(str, n - k, n % m, p1, tail, false);  //flag标志变为false,结束左旋,下面,进入右旋    
    }    
    else    
    {    
        //2、右旋:问题变成gk左移到abc前面,此时n = m’ + m = 5,m = 2,m’ = n % m 1;    
        //abc gk -> a gk bc    
            
        int p1 = tail;    
        int p2 = tail - m;    
            
        // p1,p2移动距离,向左移俩步    
        int k = (n - m) - n % m;    
            
        for (int i = 0; i < k; i++, p1--, p2--)    
            swap(str[p1], str[p2]);    
            
        rotate(str, n - k, n % m, head, p1, true);  //再次进入上面的左旋部分,    
        //3、左旋:问题变成a右移到gk后面,此时n = m’ + m = 3,m = 1,m’ = n % m = 0;    
        //a gk bc-> gk a bc。 由于此刻,n % m = 0,满足结束条件,返回结果。    
    
    }    
}    
    
int main()    
{    
    int i=3;    
    string str = "abcdefghijk";    
    int len = str.length();    
    rotate(str, len, i % len, 0, len - 1, true);    
    cout << str.c_str() << endl;   //转化成字符数组的形式输出    
    return 0;    
}    


5、一个更为有趣的方法,是采用gcd算法辅助实现。TAOCP的第一卷第一个算法就是gcd,即最大公约数求解。

  用数学定理表示即为:“定理:gcd(a,b) = gcd(b,a mod b) (a>b 且a mod b 不为0)”。以下,是此算法的具体流程:
    1[求余数],令r=m%n,r为n除m所得余数(0<=r     2、[余数为0?],若r=0,算法结束,此刻,n即为所求答案,否则,继续,转到3;
    3、[重置],置m<-n,n<-r,返回步骤1.

在描述这一算法如何辅助实现之前,先给大家看一个有趣的例子:

假使我们让abcd循环移位到dabc,即n=4,m=3,经过这样的一种方法,可以实现:

  ch[0]->temp, ch[3]->ch[0], ch[2]->ch[3], ch[1]->ch[2], temp->ch[1];  (*)
    字符串变化为:abcd->_bcd->dbc_->db_c->d_bc->dabc;


这一方法看起来神奇(每一个字符我们只移动了一次,即一步到位),实际上却是采用数学方法得到的最终化简式,那么我们就要寻找什么样的单元可以满足一步到位?

这里引用数学大帝的解释:

    1、对于正整数m、n互为质数的情况,通过以下过程得到序列的满足上面的要求:
 for i = 0: n-1
      k = i * m % n;
 end

    举个例子来说明一下,例如对于m=3,n=4的情况,
        1、我们得到的序列:即通过上述式子求出来的k序列,是0321
        2、然后,你只要只需按这个顺序赋值一遍就达到左旋3的目的了:
    ch[0]->temp, ch[3]->ch[0], ch[2]->ch[3], ch[1]->ch[2], temp->ch[1];   (*) 

    ok,这是不是就是按上面(*)式子的顺序所依次赋值的序列阿?哈哈,很巧妙吧。当然,以上只是特例,作为一个循环链,相当于rotate算法的一次内循环。

     2、对于正整数m、n不是互为质数的情况(因为不可能所有的m,n都是互质整数对),那么我们把它分成一个个互不影响的循环链,正如flyinghearts所言,所有序号为 (j + i * m) % nj为0到gcd(n, m)-1之间的某一整数,i = 0:n-1会构成一个循环链,一共有gcd(n, m)个循环链,对每个循环链分别进行一次内循环就行了。

//④ 所有序号为 (j+i *m) % n (j 表示每个循环链起始位置,i 为计数变量,m表示左旋转位数,n表示字符串长度),  
//会构成一个循环链(共有gcd(n,m)个,gcd为n、m的最大公约数),  
  
//每个循环链上的元素只要移动一个位置即可,最后整个过程总共交换了n次  
//(每一次循环链,是交换n/gcd(n,m)次,共有gcd(n,m)个循环链,所以,总共交换n次)。  
  
void rotate(string &str, int m)   
{   
    int lenOfStr = str.length();   
    int numOfGroup = gcd(lenOfStr, m);   
    int elemInSub = lenOfStr / numOfGroup;    
      
    for(int j = 0; j < numOfGroup; j++)      
        //对应上面的文字描述,外循环次数j为循环链的个数,即gcd(n, m)个循环链  
    {   
        char tmp = str[j];   
  
        for (int i = 0; i < elemInSub - 1; i++)      
            //内循环次数i为,每个循环链上的元素个数,n/gcd(m,n)次  
            str[(j + i * m) % lenOfStr] = str[(j + (i + 1) * m) % lenOfStr];  
        str[(j + i * m) % lenOfStr] = tmp;   
    }   
}  
证明:

   1、首先,直观的看肯定是有循环链,关键是有几条以及每条有多长,根据(i+j *m) % n这个表达式可以推出一些东东,一个j对应一条循环链,现在要证明(i+j *m) % n有n/gcd(n,m)个不同的数。
    2、假设j和k对应的数字是相同的, 即(i+j*m)%n = (i+k*m)%n, 可以推出n|(j-k)*m,m=m’*gcd(n.m), n=n’*gcd(n,m), 可以推出n’|(j-k)*m’,而m’和n’互素,于是n’|(j-k),即(n/gcd(n,m))|(j-k),
    3、所以(i+j*m) % n有n/gcd(n,m)个不同的数。则总共有gcd(n,m)个循环链。符号“|”是整除的意思。

6、最后一种方法,是《编程珠玑》第二章所提出的方法,即镜像翻转。

将一个字符串分成两部分,X和Y两个部分,在字符串上定义反转的操作X^T,即把X的所有字符反转(如,X="abc",那么X^T="cba"),那么我们可以得到下面的结论:(X^TY^T)^T=YX。显然我们这就可以转化为字符串的反转的问题了。

原理图:


代码如下:

//Copyright@ 小桥流水 && July    
//c代码实现,已测试正确。    
//http://www.smallbridge.co.cc/2011/03/13/100%E9%A2%98    
//_21-%E5%B7%A6%E6%97%8B%E8%BD%AC%E5%AD%97%E7%AC%A6%E4%B8%B2.html    
//July、updated,2011.04.17。    
char * invert(char *start, char *end)    
{       
    char tmp, *ptmp = start;        
    while (start != NULL && end != NULL && start < end)      
    {       
        tmp = *start;       
        *start = *end;          
        *end = tmp;         
        start ++;       
        end --;     
    }    
    return ptmp;    
}    
  
char *left(char *s, int pos)   //pos为要旋转的字符个数,或长度,下面主函数测试中,pos=3。    
{    
    int len = strlen(s);    
    invert(s, s + (pos - 1));  //如上,X->X^T,即 abc->cba    
    invert(s + pos, s + (len - 1)); //如上,Y->Y^T,即 def->fed    
    invert(s, s + (len - 1));  //如上,整个翻转,(X^TY^T)^T=YX,即 cbafed->defabc。    
    return s;    
}    


你可能感兴趣的:(算法)