1008. 数组元素循环右移问题 (20)

一个数组A中存有N(N>0)个整数,在不允许使用另外数组的前提下,将每个整数循环向右移M(M>=0)个位置,即将A中的数据由(A0A1……AN-1)变换为(AN-M …… AN-1 A0 A1……AN-M-1)(最后M个数循环移至最前面的M个位置)。如果需要考虑程序移动数据的次数尽量少,要如何设计移动的方法?

输入格式:每个输入包含一个测试用例,第1行输入N ( 1<=N<=100)、M(M>=0);第2行输入N个整数,之间用空格分隔。

输出格式:在一行中输出循环右移M位以后的整数序列,之间用空格分隔,序列结尾不能有多余空格。

输入样例:
6 2
1 2 3 4 5 6
输出样例:
5 6 1 2 3 4
#include <iostream>
#include   <algorithm>
using namespace std;
int Reverse(int* a, int b, int e)
 {
      for(; b < e; b++, e--)
		  swap(a[e],a[b]);
	  return 0;
 }
int main()
{
	int n,m;
	cin>>n>>m;
	int *a=new int[n]; 
	for(int i=0;i<n;i++)
		cin>>a[i];
	m %= n;
	Reverse(a, 0, n-m-1);
	Reverse(a, n-m,n-1);
	Reverse(a, 0, n-1);
	for(int i=0;i<n-1;i++)
		cout<<a[i]<<" ";
	cout<<a[n-1]<<endl;
	return 0;
}

下面深度剖析这个题目:

 第一章、左旋转字符串


作者:July,yansha、caopengcs。
时间:二零一一年四月十四日。


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

思路一、暴力移位法

    初看此题,咱们最先想到的笨方法可能就是一位一位移动,故咱们写一个函数叫做 leftshiftone(char *s,int n) 完成左移动一位的功能

  1. void leftshiftone(char *s,int n) {    
  2.     char t = s[0];    //保存第一个字符    
  3.     for (int i = 1; i < n; ++i) {    
  4.         s[i - 1] = s[i];    
  5.     }    
  6.     s[n - 1] = t;    
  7. }    
如此,左移m位的话,可以如下实现:

  1. void leftshift(char *s,int n,int m) {    
  2.     while (m--) {    
  3.         leftshiftone(s, n);    
  4.     }    
  5. }    

思路二、指针翻转法

    咱们先来看个例子,如下:abc defghi,若要让abc移动至最后的过程可以是:abc defghi->def abcghi->def ghiabc

    如此,我们可定义俩指针,p1指向ch[0],p2指向ch[m];
一下过程循环m次,交换p1和p2所指元素,然后p1++, p2++;。

  1. 第一步,交换abc 和def ,abc defghi->def abcghi
  2. 第二步,交换abc 和 ghi,def abcghi->def ghiabc

    整个过程,看起来,就是abc 一步一步 向后移动

  • abc defghi
  • def abcghi
  • def ghi abc  
  //最后的 复杂度是O(m+n)  

图解如下:

    由上述例子九个元素的序列abcdefghi,您已经看到,m=3时,p2恰好指到了数组最后一个元素,于是,上述思路没有问题。但如果上面例子中i 的后面还有元素列?

    即,如果是要左旋十个元素的序列:abcdefghij,ok,下面,就举这个例子,对abcdefghij序列进行左旋转操作:

如果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 

 下面,再针对上述过程,画个图清晰说明下,如下所示:

  ok,咱们来好好彻底总结一下此思路二(就4点,请仔细阅读)

1、首先让p1=ch[0]p2=ch[m],即让p1p2相隔m的距离;

2、判断p2+m-1是否越界,如果没有越界转到3,否则转到4(abcdefgh这8个字母的字符串,以4左旋,那么初始时p2指向e,p2+4越界了,但事实上p2至p2+m-1是m个字符,可以再做一个交换)

3、不断交换*p1*p2,然后p1++p2++,循环m次,然后转到2

4、此时p2+m-1 已经越界,在此只需处理尾巴。过程如下:

   4.1 通过n-p2得到p2与尾部之间元素个数r,即我们要前移的元素个数。

   4.2 以下过程执行r次:

       ch[p2]<->ch[p2-1],ch[p2-1]<->ch[p2-2]....ch[p1+1]<->ch[p1]p1++p2++

    所以,之前最初的那个左旋转九个元素abcdefghi的思路在末尾会出现问题的(如果p2后面有元素就不能这么变,例如,如果是处理十个元素,abcdefghij 列?对的,就是这个意思),解决办法有两个:

方法一(即如上述思路总结所述):
def ghi abc jk
当p1指向a,p2指向j时,由于p2+m越界,那么此时p1,p2不要变
这里p1之后(abcjk)就是尾巴,处理尾巴只需将j,k移到abc之前,得到最终序列,代码编写如下:

  1. //copyright@July、颜沙    
  2. //最终代码,July,updated again,2011.04.17。    
  3. #include <iostream>    
  4. #include <string>    
  5. using namespace std;    
  6.     
  7. void rotate(string &str, int m)    
  8. {    
  9.         
  10.     if (str.length() == 0 || m <= 0)    
  11.         return;    
  12.         
  13.     int n = str.length();    
  14.         
  15.     if (m % n <= 0)    
  16.         return;    
  17.         
  18.     int p1 = 0, p2 = m;    
  19.     int k = (n - m) - n % m;    
  20.         
  21.     // 交换p1,p2指向的元素,然后移动p1,p2    
  22.     while (k --)     
  23.     {    
  24.         swap(str[p1], str[p2]);    
  25.         p1++;    
  26.         p2++;    
  27.     }    
  28.         
  29.     // 重点,都在下述几行。    
  30.     // 处理尾部,r为尾部左移次数    
  31.     int r = n - p2;    
  32.     while (r--)    
  33.     {    
  34.         int i = p2;    
  35.         while (i > p1)    
  36.         {    
  37.             swap(str[i], str[i-1]);    
  38.             i--;    
  39.         }    
  40.         p2++;    
  41.         p1++;    
  42.     }    
  43.     //比如一个例子,abcdefghijk    
  44.     //                    p1    p2    
  45.     //当执行到这里时,defghi a b c j k    
  46.     //p2+m出界 了,    
  47.     //r=n-p2=2,所以以下过程,要执行循环俩次。    
  48.         
  49.     //第一次:j 步步前移,abcjk->abjck->ajbck->jabck    
  50.     //然后,p1++,p2++,p1指a,p2指k。    
  51.     //               p1    p2    
  52.     //第二次:defghi j a b c k    
  53.     //同理,此后,k步步前移,abck->abkc->akbc->kabc。    
  54. }    
  55.     
  56. int main()       
  57. {       
  58.     string ch="abcdefghijk";       
  59.     rotate(ch,3);       
  60.     cout<<ch<<endl;       
  61.     return 0;          
  62. }      

方法二:
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. #include <iostream>    
  2. #include <string>    
  3. using namespace std;    
  4.     
  5. //颜沙,思路二之方案二,    
  6. //July、updated,2011.04.16。    
  7. void rotate(string &str, int m)    
  8. {    
  9.     if (str.length() == 0 || m < 0)    
  10.         return;    
  11.     
  12.     //初始化p1,p2    
  13.     int p1 = 0, p2 = m;       
  14.     int n = str.length();    
  15.     
  16.     // 处理m大于n    
  17.     if (m % n == 0)    
  18.         return;    
  19.         
  20.     // 循环直至p2到达字符串末尾    
  21.     while(true)    
  22.     {      
  23.         swap(str[p1], str[p2]);    
  24.         p1++;    
  25.         if (p2 < n - 1)    
  26.             p2++;    
  27.         else    
  28.             break;    
  29.     }    
  30.         
  31.     // 处理尾部,r为尾部循环左移次数    
  32.     int r = m - n % m;  // r = 1.    
  33.     while (r--)  //外循环执行一次    
  34.     {    
  35.         int i = p1;    
  36.         char temp = str[p1];    
  37.         while (i < p2)  //内循环执行俩次    
  38.         {    
  39.             str[i] = str[i+1];    
  40.             i++;    
  41.         }       
  42.         str[p2] = temp;    
  43.     }    
  44.     //举一个例子    
  45.     //abcdefghijk    
  46.     //当执行到这里的时候,defghiabcjk    
  47.     //      p1    p2    
  48.     //defghi a b c j k,a 与 j交换,jbcak,然后,p1++,p2++    
  49.     //        p1    p2    
  50.     //       j b c a k,b 与 k交换,jkcab,然后,p1++,p2不动,    
  51.         
  52.     //r = m - n % m= 3-11%3=1,即循环移位1次。    
  53.     //          p1  p2    
  54.     //       j k c a b    
  55.     //p1所指元素c实现保存在temp里,    
  56.     //然后执行此条语句:str[i] = str[i+1]; 即a跑到c的位置处,a_b    
  57.     //i++,再次执行:str[i] = str[i+1],ab_    
  58.     //最后,保存好的c 填入,为abc,所以,最终序列为defghi jk abc。    
  59.     //July、updated,2011.04.17晚,送走了她。    
  60. }    
  61.     
  62. int main()    
  63. {    
  64.     string ch="abcdefghijk";    
  65.     rotate(ch,3);    
  66.     cout<<ch<<endl;    
  67.     return 0;       
  68. }    

注意:上文中都是假设m<n,且如果鲁棒点的话令m=m%n,这样m允许大于n。另外,各位要记得处理指针为空的情况。      

还可以看下这段代码:

  1. /*  
  2.  * myinvert2.cpp  
  3.  *  
  4.  *  Created on: 2011-5-11  
  5.  *      Author: BigPotato  
  6.  */    
  7. #include<iostream>    
  8. #include<string>    
  9. #define positiveMod(m,n) ((m) % (n) + (n)) % (n)    
  10.     
  11. /*  
  12.  *左旋字符串str,m为负数时表示右旋abs(m)个字母  
  13.  */    
  14. void rotate(std::string &str, int m) {    
  15.     if (str.length() == 0)    
  16.         return;    
  17.     int n = str.length();    
  18.     //处理大于str长度及m为负数的情况,positiveMod可以取得m为负数时对n取余得到正数    
  19.     m = positiveMod(m,n);    
  20.     if (m == 0)    
  21.         return;    
  22.     //    if (m % n <= 0)    
  23.     //        return;    
  24.     int p1 = 0, p2 = m;    
  25.     int round;    
  26.     //p2当前所指和之后的m-1个字母共m个字母,就可以和p2前面的m个字母交换。    
  27.     while (p2 + m - 1 < n) {    
  28.         round = m;    
  29.         while (round--) {    
  30.             std::swap(str[p1], str[p2]);    
  31.             p1++;    
  32.             p2++;    
  33.         }    
  34.     }    
  35.     //剩下的不足m个字母逐个交换    
  36.     int r = n - p2;    
  37.     while (r--) {    
  38.         int i = p2;    
  39.         while (i > p1) {    
  40.             std::swap(str[i], str[i - 1]);    
  41.             i--;    
  42.         }    
  43.         p2++;    
  44.         p1++;    
  45.     }    
  46. }    
  47.     
  48. //测试    
  49. int main(int argc, char **argv) {    
  50.     //    std::cout << ((-15) % 7 + 7) % 7 << std::endl;    
  51.     //    std::cout << (-15) % 7 << std::endl;    
  52.     std::string ch = "abcdefg";    
  53.     int len = ch.length();    
  54.     for (int m = -2 * len; m <= len * 2; m++) {    
  55.         //由于传给rotate的是string的引用,所以这里每次调用都用了一个新的字符串    
  56.         std::string s = "abcdefg";    
  57.         rotate(s, m);    
  58.         std::cout << positiveMod(m,len) << ": " << s << std::endl;    
  59.     }    
  60.      
  61.     return 0;    
  62. }   

思路三、递归转换法

    本文最初发布时,网友留言bluesmic说:楼主,谢谢你提出的研讨主题,很有学术和实践价值。关于思路二,本人提一个建议:思路二的代码,如果用递归的思想去简化,无论代码还是逻辑都会更加简单明了。

    就是说,把一个规模为N的问题化解为规模为M(M<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的右旋转。

updated:

Ys:

Bluesmic的思路没有问题,他的思路以前很少有人提出。思路是通过递归将问题规模变小。当字符串总长度为n,左侧要旋转的部分长度为m,那么当从左向右循环交换长度为m的小段直到剩余部分为m(n % m),此时m < m,不能直接交换了

此后,我们换一个思路,把该问题递归转化成规模大小为m +m,方向相反的同一问题。随着递归的进行,直到满足结束条件n % m==0

 

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

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,满足结束条件,返回结果

 

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

    代码如下,已测试正确(有待优化):

  1. //递归,    
  2. //感谢网友Bluesmic提供的思路    
  3.     
  4. //copyright@ yansha 2011.04.19    
  5. //July,updated,2011.04.20.    
  6. #include <iostream>    
  7. using namespace std;    
  8.     
  9. void rotate(string &str, int n, int m, int head, int tail, bool flag)    
  10. {    
  11.     //n 待处理部分的字符串长度,m:待处理部分的旋转长度    
  12.     //head:待处理部分的头指针,tail:待处理部分的尾指针    
  13.     //flag = true进行左旋,flag = false进行右旋    
  14.         
  15.     // 返回条件    
  16.     if (head == tail || m <= 0)    
  17.         return;    
  18.         
  19.     if (flag == true)    
  20.     {    
  21.         int p1 = head;    
  22.         int p2 = head + m;  //初始化p1,p2    
  23.             
  24.         //1、左旋:对于字符串abc def ghi gk,    
  25.         //将abc右移到def ghi gk后面,此时n = 11,m = 3,m’ = n % m = 2;    
  26.         //abc def ghi gk -> def ghi abc gk    
  27.         //(相信,经过上文中那么多繁杂的叙述,此类的转换过程,你应该是了如指掌了。)    
  28.             
  29.         int k = (n - m) - n % m;   //p1,p2移动距离,向右移六步    
  30.     
  31.         /*---------------------  
  32.         解释下上面的k = (n - m) - n % m的由来:  
  33.         yansha:  
  34.         以p2为移动的参照系:  
  35.         n-m 是开始时p2到末尾的长度,n%m是尾巴长度  
  36.         (n-m)-n%m就是p2移动的距离  
  37.         比如 abc def efg hi  
  38.         开始时p2->d,那么n-m 为def efg hi的长度8,  
  39.         n%m 为尾巴hi的长度2,  
  40.         因为我知道abc要移动到hi的前面,所以移动长度是  
  41.         (n-m)-n%m = 8-2 = 6。  
  42.         */    
  43.             
  44.         for (int i = 0; i < k; i++, p1++, p2++)    
  45.             swap(str[p1], str[p2]);    
  46.             
  47.         rotate(str, n - k, n % m, p1, tail, false);  //flag标志变为false,结束左旋,下面,进入右旋    
  48.     }    
  49.     else    
  50.     {    
  51.         //2、右旋:问题变成gk左移到abc前面,此时n = m’ + m = 5,m = 2,m’ = n % m 1;    
  52.         //abc gk -> a gk bc    
  53.             
  54.         int p1 = tail;    
  55.         int p2 = tail - m;    
  56.             
  57.         // p1,p2移动距离,向左移俩步    
  58.         int k = (n - m) - n % m;    
  59.             
  60.         for (int i = 0; i < k; i++, p1--, p2--)    
  61.             swap(str[p1], str[p2]);    
  62.             
  63.         rotate(str, n - k, n % m, head, p1, true);  //再次进入上面的左旋部分,    
  64.         //3、左旋:问题变成a右移到gk后面,此时n = m’ + m = 3,m = 1,m’ = n % m = 0;    
  65.         //a gk bc-> gk a bc。 由于此刻,n % m = 0,满足结束条件,返回结果。    
  66.     
  67.     }    
  68. }    
  69.     
  70. int main()    
  71. {    
  72.     int i=3;    
  73.     string str = "abcdefghijk";    
  74.     int len = str.length();    
  75.     rotate(str, len, i % len, 0, len - 1, true);    
  76.     cout << str.c_str() << endl;   //转化成字符数组的形式输出    
  77.     return 0;    
  78. }    

非常感谢。

    稍后,由下文,您将看到,其实上述思路二的本质即是下文将要阐述的stl rotate算法,详情,请继续往下阅读

 

思路四、循环移位法

    下面,我将再具体深入阐述下此STL 里的rotate算法,由于stl里的rotate算法,用到了gcd的原理,下面,我将 先介绍辗转相除法(又称欧几里得算法、gcd算法)的算法思路及原理。

    gcd,即辗转相除法,又称欧几里得算法,是求最大公约数的算法,即求两个正整数之最大公因子的算法。此算法作为TAOCP第一个算法被阐述,足见此算法被重视的程度。

    gcd算法:给定俩个正整数m,n(m>=n),求它们的最大公约数。(注意,一般要求m>=n,若m<n,则要先交换m<->n。下文,会具体解释)。

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

    此算法的证明,可参考计算机程序设计艺术第一卷:基本算法。证明,此处略。

    ok,下面,举一个例子,你可能看的更明朗点。
    比如,给定m=544,n=119,
      则余数r=m%n=544%119=68; 因r!=0,所以跳过上述步骤2,执行步骤3。;
      置m<-119,n<-68,=>r=m%n=119%68=51;
      置m<-68,n<-51,=>r=m%n=68%51=17;
      置m<-51,n<-17,=>r=m%n=51%17=0,算法结束,

    此时的n=17,即为m=544,n=119所求的俩个数的最大公约数

    再解释下上述gcd(m,n)算法开头处的,要求m>=n 的原因:举这样一个例子,如m<n,即m=119,n=544的话,那么r=m%n=119%544=119,
    因为r!=0,所以执行上述步骤3,注意,看清楚了:m<-544,n<-119。看到了没,尽管刚开始给的m<n,但最终执行gcd算法时,还是会把m,n的值交换过来,以保证m>=n。

    ok,我想,现在,你已经彻底明白了此gcd算法,下面,咱们进入主题,stl里的rotate算法的具体实现。//待续。

    熟悉stl里的rotate算法的人知道,对长度为n的数组(ab)左移m位,可以用stl的rotate函数(stl针对三种不同的迭代器,提供了三个版本的rotate)。但在某些情况下,用stl的rotate效率极差。

    对数组循环移位,可以采用的方法有(也算是对上文思路一,和思路二的总结):

      flyinghearts:
      ① 动态分配一个同样长度的数组,将数据复制到该数组并改变次序,再复制回原数组。(最最普通的方法)
      ② 利用ba=(br)^T(ar)^T=(arbr)^T,通过三次反转字符串。(即上述思路一,首先对序列前部分逆序,再对序列后部分逆序,再对整个序列全部逆序)
      ③ 分组交换(尽可能使数组的前面连续几个数为所要结果):
      若a长度大于b,将ab分成a0a1b,交换a0和b,得ba1a0,只需再交换a1 和a0。
      若a长度小于b,将ab分成ab0b1,交换a和b0,得b0ab1,只需再交换a 和b0。
      通过不断将数组划分,和交换,直到不能再划分为止。分组过程与求最大公约数很相似。
      ④ 所有序号为 (j+i *m) % n (表示每个循环链起始位置,i 为计数变量,m表示左旋转位数,n表示字符串长度),会构成一个循环链(共有gcd(n,m)个,gcd为n、m的最大公约数),每个循环链上的元素只要移动一个位置即可,最后整个过程总共交换了n次(每一次循环链,是交换n/gcd(n,m)次,总共gcd(n,m)个循环链。所以,总共交换n次)。

    stl的rotate的三种迭代器,即是,分别采用了后三种方法。

    在给出stl rotate的源码之前,先来看下我的朋友ys对上述第4种方法的评论:
    ys:这条思路个人认为绝妙,也正好说明了数学对算法的重要影响。

    通过前面思路的阐述,我们知道对于循环移位,最重要的是指针所指单元不能重复。例如要使abcd循环移位变成dabc(这里m=3,n=4),经过以下一系列眼花缭乱的赋值过程就可以实现:
    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)个循环链,对每个循环链分别进行一次内循环就行了。

    综合上述两种情况,可简单编写代码如下:

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

后来有网友针对上述的思路④,给出了下述的证明:
    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)个循环链。符号“|”是整除的意思。
以上的3点关于为什么一共有gcd(n, m)个循环链的证明,应该是来自qq3128739xx的,非常感谢这位朋友。

由于上述stl rotate源码中,方案④ 的代码,较复杂,难以阅读,下面是对上述第④ 方案的简单改写:

  1. //对上述方案4的改写。    
  2. //④ 所有序号为 (i+t*k) % n (i为指定整数,t为任意整数),....    
  3. //copyright@ hplonline && July 2011.04.18。    
  4. //July、sahala、yansha,updated,2011.06.02。    
  5. void my_rotate(char *begin, char *mid, char *end)    
  6. {       
  7.     int n = end - begin;       
  8.     int k = mid - begin;       
  9.     int d = gcd(n, k);       
  10.     int i, j;       
  11.     for (i = 0; i < d; i ++)       
  12.     {       
  13.         int tmp = begin[i];       
  14.         int last = i;       
  15.             
  16.         //i+k为i右移k的位置,%n是当i+k>n时从左重新开始。    
  17.         for (j = (i + k) % n; j != i; j = (j + k) % n)    //多谢laocpp指正。       
  18.         {       
  19.             begin[last] = begin[j];           
  20.             last = j;       
  21.         }           
  22.         begin[last] = tmp;       
  23.     }       
  24. }     

    对上述程序的解释:关于第二个for循环中,j初始化为(i+k)%n,程序注释中已经说了,i+k为i右移k的位置,%n是当i+k>n时从左重新开始。为什么要这么做呢?很简单,n个数的数组不管循环左移多少位,用上述程序的方法一共需要交换n次。当i+k>=n时i+k表示的位置在数组中不存在了,所以又从左边开始的(i+k)%n是下一个交换的位置。
  1. 好比5个学生,,编号从0开始,即0 1 2 3 4,老师说报数,规则是从第一个学生开始,中间隔一个学生报数。报数的学生编号肯定是0 2 4 1 3。这里就相当于i为0,k为2,n为5;
  2. 然后老师又说,编号为0的学生出列,其他学生到在他前一个报数的学生位置上去,那么学生从0 1 2 3 4=》2 3 4 _ 1,最后老师说,编号0到剩余空位去,得到最终排位2 3 4 0 1。此时的结果,实际上就是相当于上述程序中左移k=2个位置了。而至于为什么让 编号为0 的学生 出列。实际是这句:int last = i; 因为要达到这样的效果0 1 2 3 4 => 2 3 4 0 1,那么2 3 4 必须要移到前面去。怎么样,明白了么?。

关于本题,不少网友也给出了他们的意见,具体请参见此帖子微软100题,维护地址。 


思路五、三步翻转法

    对于这个问题,咱们换一个角度可以这么做:

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

不是么?ok,就拿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,即整个翻转。

我想,这下,你应该一目了然了。

    其次,在《编程珠玑》上也有这样一个类似的问题,它的解法同本思路一致,如下图所示:

然后,代码可以这么写:

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

《编程之美》中的题目要求只使用两个附加变量。王晓东编著的《算法设计与实验题解》中要求只用到O(1)的辅助空间。其它地方两本书的要求相同,都是O(n)的时间复杂度。两本书中的解法总结起来就是三种方法:(1)循环换位算法(2)三次反转算法(3)排列循环算法。这三种算法在王晓东的著作中都有实现代码。第一种算法是最原始的算法。第二种算法比较巧妙,即使用VU=reverse(reverse(U)reserve(V)),写成数学形式就是:

 。

于是使用三次反转也可实现。第三种方法与数学有较大关系,以下着重解释第三种方法,借此复习一下数学。

对于第三种方法,王晓东老师在著作中介绍了一条循环置换分解定理:对于给定数组A[0..N-1]向后循环换位N-K位运算,可分解为恰好gcd(K,N-K)个循环置换,且0,...,gcd(K,N-K)-1中的每个数恰属于一个循环置换。其中gcd(x,y)表示x和y的最大公因数。

我们从头开始分析这个问题,对于数组A[0..n-1],要将其向后循环移动k位元素。因为每个元素右移n位后又回到了原来的位置上,所以右移k位等于右移k mod n位。考虑每个元素右移k位后的最终位置,比如对于A[0],右移k位后在k mod n位置上,原来在k mod n位置上的元素右移k位后到了2*k mod n的位置上,把如此因为A[0]的移动而受到连环影响必须移动的位置列出来,就是下面这样一个位置序列:0,k,2*k,...,(t-1)k。其中每一项都是在模n的意义下的位置。t*k mod n 的结果是0。t是使得t*k mod n的结果为0的最小正整数。

这个位置序列实质上是模n加法群中由元素k生成的一个循环子群。由群论中的结论(该结论的证明见最后)知,循环子群(k)的周期为n / gcd(k,n),元数为n / gcd(k,n),其陪集个数为gcd(k,n)。换句话说,A[0..n-1]循环右移k位的结果是循环子群(k)以及它的所有陪集循环右移一位。例如,将A[0..5] = {1,2,3,4,5,6}循环右移4位,这里n = 6, k = 4, gcd(k, n) = 2。A[0]的最终位置是4,A[4]的最终位置是2,A[2]的最终位置是0,这样,位置0,4,2便是由k=4生成的循环群,周期为6 / gcd(4,6) = 6 / 2 = 3,这样的循环子群共有gcd(4,6) = 2个。

第三种方法的完整代码如下:

[cpp]  view plain copy
  1. // 数组的循环移位  
  2. #include <cstdio>  
  3.   
  4. int gcd(int m, int n) {  
  5.     int r;  
  6.     while(r = m % n) {  
  7.         m = n; n = r;  
  8.     }  
  9.     return n;  
  10. }  
  11.   
  12. void shiftArray(int A[], int n, int k) {  
  13.     // 因为左移的代码比右移的代码好实现的多,而右移k位  
  14.     // 等价于左移-k位,-k = n - k。以下代码是通过左移-k位来实现右移k位  
  15.     k = n - (k % n);  
  16.     for(int i = 0, cnt = gcd(n, k); i < cnt; i++) {  
  17.         int t = A[i], p = i, j = (k+i) % n;  
  18.         while(j != i) {  
  19.             A[p] = A[j]; p = j; j = (k + p) % n;  
  20.         }  
  21.         A[p] = t;  
  22.     }  
  23. }  
  24.   
  25. void printArray(int A[], int n) {  
  26.     for(int i = 0; i < n; i++) {  
  27.         printf("%-3d", A[i]);  
  28.         if((i+1)%10 == 0) printf("/n");  
  29.     }  
  30. }  
  31. int main() {  
  32.     int A[] = {1,2,3,4,5,6, 7};  
  33.     shiftArray(A, 7, 4);  
  34.     printArray(A, 7);  
  35.     return 0;  
  36. }  

上述所用到的那个群论结论的证明:

结论:设G是一个循环群,其中一个生成元素为a,若r和n的最大公约数为d,则a^r的周期为n / d。

 

在模n加法群中,1是一个生成元素,任意元素k=1*k,所以任意元素k生成的循环子群(k)的周期为n / gcd(k,n)。因为gcd(k,n)=gcd(k,n-k),所以也可以写成n / gcd(k, n-k)。



  • 8
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值