博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

【算法】rotate函数

Posted on 2011-05-24 15:10  李大嘴  阅读(3487)  评论(0)    收藏  举报

forward_iterator_tag
这种类型的迭代器只能像前移动,所以算法实现上很纠结。
大体过程是不断交换[first, mid) 和 [mid, last) 两个区间的元素。
没有多少复杂的数学推导,想清楚过程就行了,故不赘述。
交换作为单位操作,时间消耗是n。
(注意,这三个算法的空间复杂度都是O(1),时间O(n)。所以不谈复杂度,谈具体消耗,并且要指明单位操作。)

bidirectional_iterator_tag
这种类型的迭代器可以双向移动,于是它是支持reverse操作的。
估计大部分人在网上看到的面试题,都是讲的这套算法,如下:
reverse(begin, mid)
reverse(mid, end)
reverse(begin, end)
交换作为单位操作,时间消耗是2*n。

random_access_iterator_tag
赋值作为单位操作,时间消耗是 d + n,
其中,d=gcd(n, k)。
gcd 求得的最大公约数,表示需要几轮内循环。


可以知道,bidirectional版本最耗时,是forward版本的两倍。
random版本与n和k的具体值有关,但是d不会超过n,且一般都非常小。
random版本用的不是交换,而是轮换,所以单位操作是幅值。
从实测可以看到,耗时大约为forward版本的1/3。

》》random_access_iterator

下面分析一个我改写的简化版

void my_rotate(int *begin, int *mid, int *end){
    int n = end - begin ;
    int k = mid - begin ;
    int d = __gcd(n, k) ;
    int i, j ;
    // (i + k * j) % n % d == i % d
    for ( i = 0 ; i < d ; i ++ ){
        int tmp = begin[i] ;
        int last = i ;
        for ( j = (i + k) % n ; j != i ; j = (j + k) % n){
            begin[last] = begin[j] ;   
            last = j ;
        }   
        begin[last] = tmp ;
    }
}

先举个例子,取n=5,k=2。原序列:
0 1 2 3 4
rotate过后的序列:
2 3 4 0 1
这里,原序列中的某个元素,rotate后的位置是可以直接确定的。
注意到我们只有O(1)的空间可以用,所以从0号元素开始,做下列操作:
(1)备份0号元素
(2)2 --> 0
(3)4 --> 2
(4)1 --> 4
(5)3 --> 1
(6)将备份的0号元素放到3

这个例子中,只用了一次轮换,就完了,但并非所有的输入都可以在一次轮换做完。
比如可以试下n=4,k=2。
第一次轮换从0开始,
0 1 2 3 --> 2 1 0 3
第二次轮换从1开始,
2 1 0 3 --> 2 3 0 1

我的简化版本中,有两重循环。
内层对应的就是单次轮换,外层对应的就是第i轮,i是起点。

》》证明

现在需要证明这个算法是完备的,关键的结论就是代码中飘红的这句注释:
// (i + k * j) % n % d == i % d
由于d=gdc(n, k),这个结论比较容易证明。

左边的(i + k * j) % n,反映的就是内层循环。
每个元素被它后面(模n)的第k个元素替换。
从前面举的例子来看,这样一轮替换有时候会完,有时候有剩。从前面的例子可以得出以下结论,最大公约数是可以计算出需要几次内循环的,一般互质的一次就够了,因为(i + k * j) % n循环到最后通过%运算又回到了起点,这时与第一次的起点是不同的,经过几次之后内循环之后就可以遍历完所有值。算法的过程就是将相差k的点向前挪。
现在就是推出剩了多少,并且怎么把剩的轮换也做了。
对lhs模d,可以发现得到了一个与j无关的式子,i%d
相当于把n个元素划分成了d组,一次轮换只使得i这组换到了rotate之后的位置。
所以需且只需选择d个不同的起点,做多次轮换就行了。