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个不同的起点,做多次轮换就行了。
浙公网安备 33010602011771号