左旋转字符串(数组循环移位)

题目描述:

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

编程之美上有这样一个类似的问题,咱们先来看一下:

设计一个算法,把一个含有N个元素的数组循环右移K位,要求时间复杂度为O(N),
且只允许使用两个附加变量。

分析:

我们先试验简单的办法,可以每次将数组中的元素右移一位,循环K次。

abcd1234→4abcd123→34abcd12→234abcd1→1234abcd。

 1 RightShift(int* arr, int N, int K)
 2 {
 3      while(K--)
 4      {
 5           int t = arr[N - 1];
 6           for(int i = N - 1; i > 0; i --)
 7                arr[i] = arr[i - 1];
 8           arr[0] = t;
 9      }
10 }

虽然这个算法可以实现数组的循环右移,但是算法复杂度为O(K * N),不符合题目的要求,要继续探索。

假如数组为abcd1234,循环右移4位的话,我们希望到达的状态是1234abcd。
不妨设K是一个非负的整数,当K为负整数的时候,右移K位,相当于左移(-K)位。
左移和右移在本质上是一样的。

解法一:
大家开始可能会有这样的潜在假设,K<N。事实上,很多时候也的确是这样的。但严格来说,我们不能用这样的“惯性思维”来思考问题。
尤其在编程的时候,全面地考虑问题是很重要的,K可能是一个远大于N的整数,在这个时候,上面的解法是需要改进的。
仔细观察循环右移的特点,不难发现:每个元素右移N位后都会回到自己的位置上。因此,如果K > N,右移K-N之后的数组序列跟右移K位的结果是一样的。

进而可得出一条通用的规律:
右移K位之后的情形,跟右移K’= K % N位之后的情形一样,如代码清单2-34所示。
//代码清单2-34

 1 RightShift(int* arr, int N, int K)
 2 {
 3      K %= N;
 4      while(K--)
 5      {
 6           int t = arr[N - 1];
 7           for(int i = N - 1; i > 0; i --)
 8                arr[i] = arr[i - 1];
 9           arr[0] = t;
10      }
11 }

可见,增加考虑循环右移的特点之后,算法复杂度降为O(N^2),这跟K无关,与题目的要求又接近了一步。但时间复杂度还不够低,接下来让我们继续挖掘循环右移前后,数组之间的关联。

解法二:
假设原数组序列为abcd1234,要求变换成的数组序列为1234abcd,即循环右移了4位。比较之后,不难看出,其中有两段的顺序是不变的:1234和abcd,可把这两段看成两个整体。右移K位的过程就是把数组的两部分交换一下。
变换的过程通过以下步骤完成:
 逆序排列abcd:abcd1234 → dcba1234;
 逆序排列1234:dcba1234 → dcba4321;
 全部逆序:dcba4321 → 1234abcd。
伪代码可以参考清单2-35。
//代码清单2-35

 

 

 1 Reverse(int* arr, int b, int e)
 2 {
 3      for(; b < e; b++, e--)
 4      {
 5           int temp = arr[e];
 6           arr[e] = arr[b];
 7           arr[b] = temp;
 8      }
 9 }
10 
11 RightShift(int* arr, int N, int k)
12 {
13      K %= N;
14      Reverse(arr, 0, N – K - 1);
15      Reverse(arr, N - K, N - 1);
16      Reverse(arr, 0, N - 1);
17 }

 

这样,我们就可以在线性时间内实现右移操作了。

 

稍微总结下:
编程之美上,
(限制书中思路的根本原因是,题目要求:“且只允许使用两个附加变量”,去掉这个限制,思路便可如泉喷涌)
1、第一个想法 ,是一个字符一个字符的右移,所以,复杂度为O(N*K)
2、后来,它改进了,通过这条规律:右移K位之后的情形,跟右移K’= K % N位之后的情形一样
复杂度为O(N^2)
3、直到最后,它才提出三次翻转的算法,得到线性复杂度。

 

下面,你将看到,本章里我们的做法是:
1、三次翻转,直接线性
2、两个指针逐步翻转,线性
3、stl的rotate算法,线性

 

好的,现在,回到咱们的左旋转字符串的问题中来,对于这个左旋转字符串的问题,咱们可以如下这样考虑:

1.1、思路一:

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

不是么?ok,就拿abcdef 这个例子来说(非常简短的三句,请细看,一看就懂):
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 #include <stdio.h>  
 7 #include <string.h>  
 8   
 9 char * invert(char *start, char *end)  
10 {     
11     char tmp, *ptmp = start;      
12     while (start != NULL && end != NULL && start < end)    
13     {     
14         tmp = *start;     
15         *start = *end;        
16         *end = tmp;       
17         start ++;     
18         end --;   
19     }  
20     return ptmp;  
21 }  
22   
23 char *left(char *s, int pos)   //pos为要旋转的字符个数,或长度,下面主函数测试中,pos=3。  
24 {  
25     int len = strlen(s);  
26     invert(s, s + (pos - 1));  //如上,X->X^T,即 abc->cba  
27     invert(s + pos, s + (len - 1)); //如上,Y->Y^T,即 def->fed  
28     invert(s, s + (len - 1));  //如上,整个翻转,(X^TY^T)^T=YX,即 cbafed->defabc。  
29     return s;  
30 }  
31   
32 int main()  
33 {     
34     char s[] = "abcdefghij";      
35     puts(left(s, 3));  
36     return 0;  
37 }  

思路三:

3.1、数组循环移位
    下面,我将再具体深入阐述下此STL 里的rotate算法,由于stl里的rotate算法,用到了gcd的原理,下面,我将先介绍此辗转相除法,或欧几里得算法,gcd的算法思路及原理。

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

    gcd算法:给定俩个正整数m,n(m>=n),求它们的最大公约数。(注意,一般要求m>=n,若m<n,则要先交换m<->n。下文,会具体解释)。以下,是此算法的具体流程:
    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 (j 表示每个循环链起始位置,i 为计数变量,m表示左旋转位数,n表示字符串长度),会构成一个循环链(共有gcd(n,m)个,gcd为n、m的最大公约数),每个循环链上的元素只要移动一个位置即可,最后整个过程总共交换了n次(每一次循环链,是交换n/gcd(n,m)次,总共gcd(n,m)个循环链。所以,总共交换n次)。

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

    在给出stl rotate的源码之前,先来看下我的朋友ys对上述第④ 种方法的评论:
    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序列,是0, 3, 2, 1
        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的,非常感谢这位朋友。

3.2、以下,便是摘自sgi stl v3.3版中的stl_algo_h文件里,有关rotate的实现的代码:

 

  1 // rotate and rotate_copy, and their auxiliary functions  
  2 template <class _EuclideanRingElement>  
  3 _EuclideanRingElement __gcd(_EuclideanRingElement __m,  
  4                             _EuclideanRingElement __n)  
  5 {  //gcd(m,n)实现  
  6     while (__n != 0) {  
  7         _EuclideanRingElement __t = __m % __n;  
  8         __m = __n;  
  9         __n = __t;  
 10     }  
 11     return __m;   //....  
 12 }  
 13   
 14 //③ 分组交换(尽可能使数组的前面连续几个数为所要结果):  
 15 //若a长度大于b,将ab分成a0a1b,交换a0和b,得ba1a0,只需再交换a1 和a0。  
 16 //若a长度小于b,将ab分成ab0b1,交换a和b0,得b0ab1,只需再交换a 和b0。  
 17 //通过不断将数组划分,和交换,直到不能再划分为止。分组过程与求最大公约数很相似。  
 18 template <class _ForwardIter, class _Distance>  
 19 _ForwardIter __rotate(_ForwardIter __first,  
 20                       _ForwardIter __middle,  
 21                       _ForwardIter __last,  
 22                       _Distance*,  
 23                       forward_iterator_tag)   
 24 {  
 25     if (__first == __middle)  
 26         return __last;  
 27     if (__last  == __middle)  
 28         return __first;  
 29       
 30     _ForwardIter __first2 = __middle;  
 31     do {  
 32         swap(*__first++, *__first2++);  //  
 33         if (__first == __middle)  
 34             __middle = __first2;  
 35     } while (__first2 != __last);  
 36       
 37     _ForwardIter __new_middle = __first;  
 38     __first2 = __middle;  
 39       
 40     while (__first2 != __last)   
 41     {  
 42         swap (*__first++, *__first2++);  //  
 43         if (__first == __middle)  
 44             __middle = __first2;  
 45         else if (__first2 == __last)  
 46             __first2 = __middle;  
 47     }  
 48       
 49     return __new_middle;  
 50 }  
 51   
 52 //②利用ba=(br)^T(ar)^T=(arbr)^T,通过三次反转字符串。  
 53 //(即上述思路一,首先对序列前部分逆序,再对序列后部分逆序,再对整个序列全部逆序)  
 54 template <class _BidirectionalIter, class _Distance>  
 55 _BidirectionalIter __rotate(_BidirectionalIter __first,  
 56                             _BidirectionalIter __middle,  
 57                             _BidirectionalIter __last,  
 58                             _Distance*,  
 59                             bidirectional_iterator_tag)   
 60 {  
 61     __STL_REQUIRES(_BidirectionalIter, _Mutable_BidirectionalIterator);  
 62     if (__first == __middle)  
 63         return __last;  
 64     if (__last  == __middle)  
 65         return __first;  
 66       
 67     __reverse(__first,  __middle, bidirectional_iterator_tag());  //交换序列前半部分  
 68     __reverse(__middle, __last,   bidirectional_iterator_tag());  //交换序列后半部分  
 69       
 70     while (__first != __middle && __middle != __last)  
 71         swap (*__first++, *--__last);   //整个序列全部交换  
 72       
 73     if (__first == __middle)  //  
 74     {  
 75         __reverse(__middle, __last,   bidirectional_iterator_tag());  
 76         return __last;  
 77     }  
 78     else {  
 79         __reverse(__first,  __middle, bidirectional_iterator_tag());  
 80         return __first;  
 81     }  
 82 }  
 83   
 84 //④ 所有序号为 (i+t*k) % n (i为指定整数,t为任意整数),  
 85 //会构成一个循环链(共有gcd(n,k)个,gcd为n、k的最大公约数),  
 86 //每个循环链上的元素只要移动一个位置即可,总共交换了n次。  
 87 template <class _RandomAccessIter, class _Distance, class _Tp>  
 88 _RandomAccessIter __rotate(_RandomAccessIter __first,  
 89                            _RandomAccessIter __middle,  
 90                            _RandomAccessIter __last,  
 91                            _Distance *, _Tp *)   
 92 {  
 93     __STL_REQUIRES(_RandomAccessIter, _Mutable_RandomAccessIterator);  
 94     _Distance __n = __last   - __first;  
 95     _Distance __k = __middle - __first;  
 96     _Distance __l = __n - __k;  
 97     _RandomAccessIter __result = __first + (__last - __middle);  
 98       
 99     if (__k == 0)  
100         return __last;  
101       
102     else if (__k == __l) {  
103         swap_ranges(__first, __middle, __middle);  
104         return __result;  
105     }  
106       
107     _Distance __d = __gcd(__n, __k);    //令d为gcd(n,k)  
108       
109     for (_Distance __i = 0; __i < __d; __i++) {  
110         _Tp __tmp = *__first;  
111         _RandomAccessIter __p = __first;  
112           
113         if (__k < __l) {  
114             for (_Distance __j = 0; __j < __l/__d; __j++) {  
115                 if (__p > __first + __l) {  
116                     *__p = *(__p - __l);  
117                     __p -= __l;  
118                 }  
119                   
120                 *__p = *(__p + __k);  
121                 __p += __k;  
122             }  
123         }     
124         else {  
125             for (_Distance __j = 0; __j < __k/__d - 1; __j ++) {  
126                 if (__p < __last - __k) {  
127                     *__p = *(__p + __k);  
128                     __p += __k;  
129                 }  
130                   
131                 *__p = * (__p - __l);  
132                 __p -= __l;  
133             }  
134         }  
135           
136         *__p = __tmp;  
137         ++__first;  
138     }  
139       
140     return __result;  
141 }  

 

由于上述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+)%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 必须要移到前面去。怎么样,明白了么?。

 

第五节、总结 
     如nossiac所说,对于这个数组循环移位的问题,真正最靠谱的其实只有俩种:一种是上文的思路一,前后部分逆置翻转法,第二种是思路三,即stl 里的rotate算法,其它的思路或方法,都是或多或少在向这俩种方法靠拢。

 

以上摘自July博客

 

 

 

 

posted @ 2012-08-01 16:09  wolenski  阅读(1033)  评论(0)    收藏  举报