STL中rotate算法的理解

STL中rotate针对不同的迭代器有3种不同的实现,在http://blog.csdn.net/v_JULY_v/article/details/6322882中也有描述,这里以学习为目的,将这STL中rotate的RandomAccessIterator的算法整理一下

以下内容参考自:http://www.cnblogs.com/atyuwen/archive/2009/11/08/rotate.html

这里的rotate操作,也就是指循环移位。比如将串“ABCDEFG”以D为中心旋转,就相当将该串向左循环移位,直到第一个元素为D为止,最后得到新串“DEFGABC”。要想方便的完成rotate操作,一个常见的技巧是这样的:先将前半部分反转,再将后半部分反转,最后再将整个串反转即可(这里的前半部分与后半部分是以旋转中心来划分的)。还是以串“ABCDEFG”以D为中心旋转为例,以D为分割点,将先半部分与后半部分分别反转后,得“CBAGFED”,最后将整个串反转即得“DEFGABC”。这个算法在很多书上都提到过,相信大家一定都很熟悉了。那么其效率如何呢,假设串的长度为t,那么完成整个旋转过程需要约t次swap操作,也即是说需要大概3t次赋值,同时只需要常量的临时空间。因为其实现简单,所以还是差强人意。所以SGI STL在处理双向迭代器容器时也正是使用了该算法。但是进一步观察STL源码可以发现,在处理存储在拥有随机访问能力的容器中的串时,SGI STL却是采用了另外一种算法,而这个算法的原理在《STL源码剖析》中恰恰被hjj无视了,所以我在这里再简单地梳理一下。

上面只涉及到三个函数:__rotate、__gcd、__rotate_cycle。后两个函数都比较容易理解:__gcd没什么好说的,XXX嘛,当然是求最大公约数了。__rotate_cycle,是从某个初始元素开始,依次将其替换成其后相隔固定距离的元素。如果后面没有足够的偏移距离了,则又返回头部继续计算(相当于求模)。直到最后形成一个置换圈为止。

现在来仔细观察函数__rotate,这个函数实际上也不复杂,才区区三行:先是求出偏移距离和串总长的最大公约数,然后循环n次,分别以串的前n个元素为起点进行__rotate_cycle操作,over。但这怎么就保证了能正确地完成对输入串的rotate操作呢?

这就涉及到数论中的一个小定理:若有两个正整数m、n,且gcd(m,n)=d,那么序列{m%n, 2m%n, 3m%n,..., nm%n}一定是{0, d, 2d,..., n-d}的某个排列并重复出现d次,其中%号代表求模操作。比如若m=6, n=8,d=gcd(m,n)=2,那么{6%8, 12%8, 18%8,..., 48%8}即为{0,2,4,6}的某个排列并重复两次,事实上也正是{6,4,2,0, 6,4,2, 0}。特别地,若m、n互素,d=1,那么序列{m%n,2m%n,3m%n,...,(n-1)m%n}实际上就是{1, 2, 3,..., n-1}的某个排列。这个定理的证明过程可以很多书中找到(比如具体数学4.8节),这里不再详述。

对这个排列的理解是比较重要的,如果我们设置其排列从3开始那这个排列就是3,4,5,n-1,n, 0,1,2。所以STL中的rotate就是利用了这个原理,通过使用不同的起始值来,产生不同的序列。序列的共同点就是这个序列构成了一个环,并且相邻元素的index差距是gcd。 模仿的一个对数组进行rotate的实现: 

int gcd(int m, int n)
{
    int big, small;
    if(m>n) {big = m; small = n;}
    else {big = n; small=m;}
    while(small !=0)
    {
        int t = big%small;
        big = small;
        small = t;
    }

    return big;
}

/*
initial is the starting point of the cycle;
shift is the distance of the replacement of element from its current position. 也就是说,shift是元素要移动的距离 
*/
void myrotate_cycle(int *iarray, int first, int last, int initial, int shift)
{
    int t = iarray[initial];
    int ptr1 = initial;
    int ptr2 = initial+shift;
    while(ptr2!=initial)
    {
        iarray[ptr1] = iarray[ptr2];
        ptr1 = ptr2;
        if(ptr2<(last-shift))
            ptr2+=shift;
        else
            ptr2=first + shift- (last -ptr2); // roll back
    }
    iarray[ptr1] = t;
}

void myrotate(int *iarray, int first, int middle, int last)
{
    int n = gcd(last-first, middle-first);
    while(n--)
        myrotate_cycle(iarray,first,last,first+n,middle-first); // only the starting point is changing
}


int main(int argc, char **argv)
{

    int a[]= {01,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
    myrotate(a,0,3,16);
}

了解这个引理后,就很容易看出__rotate函数的内涵了。若第一步求得的最大公约数n为1,那么只需一次__rotate_cycle就可以遍历到所有的元素,并将每个元素正确的替换为其后相距某个距离的元素,于是也就完成了循环左移操作。若n大于1,那么每一次__rotate_cycle只能将t/n的元素正确的左移,其中t为串的总长度,而这些被移动的元素是以d为等间距的,所以循环n次,并分别以串的前n个元素为起点进行__rotate_cycle操作,就能保证将所有的元素都移动到正确的位置上。

 

背景知识:

gcd,即辗转相除法,又称欧几里得算法,是求最大公约数的算法,即求两个正整数之最大公因子的算法。

Reference:http://www.cnblogs.com/atyuwen/archive/2009/11/08/rotate.html

原文地址:https://www.cnblogs.com/whyandinside/p/2616199.html