stl::rotate 数组循环移位

   这段时间想学下算法,于是乎一个朋友向我推荐了“程序员编程艺术”作者“v_JULY_v”写的博客,但是看到stl::rotate算法的时候,我研究了一早上才明白这个的原理,不知道我能不能说清楚,但是希望给没看懂这个部分的童鞋带来帮助!

  我的解释在引用博客文字之下!第一次写技术博客,如果对你有用的话,我会很高兴的(*^__^*) 嘻嘻……!

  此一下的解释源于http://blog.csdn.net/v_july_v/article/details/6322882

   下面,我将再具体深入阐述下此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.     }   

以上文字源于http://blog.csdn.net/v_july_v/article/details/6322882


刚开始的时候只是知道这个思想,但是他的内部到底是怎么移动的,我并不理解,通过我早上的举例实践发现他的循环移动是这样的,

请看下例:

现在有一个数组{c,d,b,a,c,c,d,a,b} 数组的长度是9,要求将数组的前6位数转移到数组的后面,现在,我们用rotate方法执行如下:

数组长度:9;最大公约数:3;循环链的个数为N/gcd(N,M)(N=9,M=6)=3;

则有以下循环

for(int j=0;j<3;j++)

{

  char tmp=str[j];

  for(int i=0;i<(3-1);i++)

    str[(j+i*m)%9]=str[(j+(i+1)*m)%9];

  str[(j+i*m)%9]=tmp;

}

第一次循环j=0;

i=0时

str[0]=str[6]

i=1时

str[6]=str[3]

i=2时跳出循环

str[3]=tmp;(tmp为str[j]因为j=0,则str[j]即为str[0] ,换而言之str[3]=str[0])

第二次循环j=1;

i=0时;

str[1]=str[7]

i=1时

str[7]=str[4]

i=2时跳出循环

str[4]=tmp=str[1]

.

.

.

循环到最后,得到想要的结果,由以上循环的特点你们能发现点什么么,对,其实每次内部循环(也就是i的那个循环)的时候,都把数组要转移字符串的M/gcd(N,M)个数组元素转移到该元素最后该在的位置,每次循环只是将要转移的字符串的M/gcd(N,M)个字符转移到相应的位置,每次转移都将N/gcd(N,M)个子串的第j个元素移动N/gcd(N,M)-1位;理所当然的,移动j*i次后,得到最后正确结果,你理解了没有?

 

转载于:https://www.cnblogs.com/fy1990/archive/2012/10/29/rotate.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值