在《STL源码剖析》中,对于rotate算法没有很好地解释,__rotate_cycle是如何发挥作用的,其中很值得研究。
SGI STL的rotate函数是将一个序列[first,last)的前半部分[first,middle)和后半部分[middle,last)进行调换。整个算法分为3种不同实现。对于书中所说的最后一种算法,本质就是序列的循环移位。见下图:
将[first,middle)循环移动到序列末尾,[middle,last)移动到序列最前面,即可完成rotate功能。
但STL的rotate实现中采用了__rotate_cycle()函数,此函数对序列中一组元素向前移动middle-first长度。采用while循环调用__rotate_cycle()共移动了gcd(m,n)组。见下图:
__rotate_cycle函数每次移动一种颜色的分组,例如第一次移动红色的元素,第二次移动绿色的元素。每个元素都被向前移动middle-first长度。
__rotate_cycle函数第一次移动分组: m%n,2m%n,3m%b,......nm%n;(m<n)
第二次移动分组:m%n+1,2m%n+1,......nm%n+1;
第三次移动分组:m%n+2,2m%n+2,......nm%n+2;
依次类推,每个被移动的元素,都向前移动了m距离,移动后仍在当前分组中。
下面是算法用到的一个关键定理:
假设两个正整数m,n,m、n互质,则序列{m%n,2m%n,3m%n,......nm%n }一定覆盖{0,1,2,3......n-1}所有值。(证明见文章末尾)
定理推广形式:
假设两个正整数m,n,m、n的最大公约数为d=gcd(m,n),则序列{m%n,2m%n,3m%n,......nm%n }一定覆盖{0,d,2d,3d,......n-d}所有值。
由于m,n可被d整除,因此长度为n的序列可以分成若干长度为d的小序列。如下图:
现在可以清楚地看到,__rotate_cycle函数移动的分组,对应序列中的位置有:
第一分组:{0, d, 2d,... n-d}。
第二分组:{1, d+1, 2d+1...... n-d+1}。
第三分组:{2, d+2, 2d+2...... n-d+2}。
......
共需要移动多少分组,才能完整覆盖序列? 显然需要d组,这就是为什么需要调用gcd(m,n)次__rotate_cycle的原因。
凡是被__rotate_cycle访问的元素,都被向前移动了m距离。当序列中所有元素都被向前移动m距离后,即完成了一次rotate操作。
下面是定理的证明
引理:假设正整数m,n,m、n互质,对于任意正整数0<k1、k2<=n,k1 != k2 和 k1m%n != k2m%n互为充要条件。
需要证明 k1 != k2时,k1m%n != k2m%n
k1m%n != k2m%n时,k1 != k2。
采用反证法:假设k1 != k2时,有k1m%n == k2m%n。
由于0<k1、k2<=n,则-n<k1-k2<n; 根据取模性质:
k1m%n - k2m%n = (k1 - k2)m%n = 0;
即(k1-k2)m能够整除n。因此(k1-k2)m = np(p为整数)。由于m、n互为质数,其最小公倍数为mn,因此k1-k2>=n或k1-k2 == 0;
由于-n<k1-k2<n,且k1 != k2,因此产生矛盾。假设不成立。
反之证明 k1m%n != k2m%n时,k1 != k2。不再过多叙述。
利用引理证明定理,可知{m%n,2m%n,3m%b,......nm%n }中元素两两不相等,共有n个。每个元素小于n的整数,可能是{0,1,2,3......n-1},因此只能覆盖所有值。
证明定理推广形式:
利用取模性质:m = d*m'、n=d*n', 有m%n = d(m'%n');
对于任何正整数,必有
m = d * m';
n = d *n' ; 其中m'和n'互为质数。
m%n = d(m'%n'),因此序列{m%n,2m%n,3m%b,......nm%n }可表示为:
{d(m'%n'), d(2m'%n'), ...... d(n'm'%n'),d((n'+1)m'%n'),......d(n'dm'%n')}。其中(n'+1)m'%n' = m'%n',相当于
{m%n,2m%n,3m%b,......n‘m%n }循环出现了d次。
覆盖范围为d*{0,1,2,3,......n'-1},即为{0,d,2d,......n'd-d} 为{0,d,2d,......n-d}。