這裡的rotate操作,也就是指迴圈移位。比如將串“ABCDEFG”以D為中心旋轉,就相當將該串向左迴圈移位,直到第一個元素為D為止,最後 得到新串“DEFGABC”。
要想方便的完成rotate操作,一個常見的技巧是這樣的:先將前半部分反轉,再將後半部分反轉,最後再將整個串反轉即可 (這裡的前半部分與後半部分是以旋轉中心來劃分的)。還是以串“ABCDEFG”以D為中心旋轉為例,以D為分割點,將先半部分與後半部分分別反轉後,得 “CBAGFED”,最後將整個串反轉即得“DEFGABC”。
這個演算法在很多書上都提到過,相信大家一定都很熟悉了。那麼其效率如何呢,假設串的長度為 t,那麼完成整個旋轉過程需要約t次swap操作,也即是說需要大概3t次賦值,同時只需要常量的臨時空間。因為其實現簡單,所以還是差強人意。所以 SGI STL在處理雙向迭代器容器時也正是使用了該演算法。
但是進一步觀察STL源碼可以發現,在處理儲存在擁有隨機訪問能力的容器中的串時,SGI STL卻是採用了另外一種演算法,而這個演算法的原理在《STL源碼剖析》中恰恰被hjj無視了,所以我在這裡再簡單地梳理一下。
首先,先帖出這個演算法的原始碼:
template < class RandomAccessIterator, class Distance > void __rotate(RandomAccessIterator first, RandomAccessIterator middle, RandomAccessIterator last, Distance * , random_access_iterator_tag) { Distance n = __gcd(last - first, middle - first); while (n -- ) __rotate_cycle(first, last, frist + n, middle - first, value_type(first)); } template < class EuclideanRingElement > EuclideanRingElement __gcd(EuclideanRingElement m, EuclideanRingElement n) { while (n != 0 ) { EuclideanRingElement t = m % n; m = n; n = t; } return m; } template < class _RandomAccessIterator, class Distance, class T > void __rotate_cycle(RandomAccessIterator first, RandomAccessIterator last, RandomAccessIterator initial, Distance shift, T * ) { T value = * initial; RandomAccessIterator ptr1 = initial; RandomAccessIterator ptr2 = ptr1 + shift; while (ptr2 != initial) { * ptr1 = * ptr2; ptr1 = ptr2; if (last - ptr2 > shift) ptr2 += shift; else ptr2 = first + (shift - (last - ptr2)); } * ptr1 = value; }
上面只涉及到三個函數:__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節),這裡不再詳述。
瞭解這個引理後,就很容易看出__rotate函數的內涵了。若第一步求得的最大公約數n為1,那麼只需一次__rotate_cycle就可以遍 曆到所有的元素,並將每個元素正確的替換為其後相距某個距離的元素,於是也就完成了迴圈左移操作。若n大於1,那麼每一次__rotate_cycle只 能將t/n的元素正確的左移,其中t為串的總長度,而這些被移動的元素是以d為等間距的,所以迴圈n次,並分別以串的前n個元素為起點進行 __rotate_cycle操作,就能保證將所有的元素都移動到正確的位置上。
在這個新的演算法中,每次__rotate_cycle需要t/n+1次賦值,n次迴圈,所以總共只需要t+n次賦值操作,顯然是要比前面所說的三次反轉的演算法快上許多。
比如考慮當串a = { 1, 2, 3, 4, 5} 迴圈左移2位,即期望得到串{ 3, 4, 5, 1, 2},那麼該演算法的賦值過程如下:
tmp = a[0] -> tmp = 1
a[0] = a[2] ->{ 3, 2, 3, 4, 5}
a[2] = a[4] ->{ 3, 2, 5, 4, 5}
a[4] = a[1] ->{ 3, 2, 5, 4, 2}
a[1] = a[3] ->{ 3, 4, 5, 4, 2}
a[3] = tmp ->{ 3, 4, 5, 1, 2}
這裡因為2與5互素,所以6次賦值就已搞定,而如果是用三次翻轉的演算法則需要大約3*5次賦值(實際上為12次)。
http://www.cnblogs.com/atyuwen/archive/2009/11/08/rotate.html