給定一個數組a1,a2,a3,...an,b1,b2,b3..bn,最終把它置換成a1,b1,a2,b2,...an,bn。
分析:
本題是完美洗牌問題的變形。
完美洗牌問題: 給定一個數組a1,a2,a3,...an,b1,b2,b3..bn,最終把它置換成b1,a1,b2,a2,...bn,an。
我們先解決完美洗牌問題。
為方便起見,我們考慮的是一個下標從1開始的數組,下標範圍是[1..2n]。 我們看一下每個元素最終去了什麼地方。
前n個元素 a1 -> a2 a2->a4.... 第i個元素去了 第(2 * i)的位置。
後n個元素a(n + 1)->a1, a(n + 2)->a3... 第i個元素去了第 ((2 * (i - n) ) - 1) = (2 * i - (2 * n + 1)) = (2 * i) % (2 * n + 1) 個位置。
統一一下,任意的第i個元素,我們最終換到了 (2 * i) % (2 * n + 1)的位置,這個模數很神奇,不會產生0。所有最終的位置編號還是從1到2n。
(1) 完美洗牌演算法1
於是,如果允許我們再用一個數組的話,我們直接把每個元素放到該放得位置就好了。於是產生了最簡單的方法pefect_shuffle1,它的時間複雜度是O(n),空間複雜度也是O(n)。
代碼:
// 時間O(n),空間O(n) 數組下標從1開始void pefect_shuffle1(int *a,int n) {int n2 = n * 2, i, b[N]; for (i = 1; i <= n2; ++i) { b[(i * 2) % (n2 + 1)] = a[i]; } for (i = 1; i <= n2; ++i) { a[i] = b[i]; }}
(2) 完美洗牌演算法2——分治的力量。
考慮分治法,假設n也是偶數。 我們考慮把數組拆成兩半:
我們唯寫數組的下標:
原始數組(1..2*n)
也就是(1..n)(n + 1.. 2 *n)
前半段長度為n,後半段長度也為n,我們把前半段的後n / 2個元素(n / 2 + 1..n) 與 後半段的前n / 2 個元素 (n + 1..n + n / 2 ) 交換,得到 :
新的前n個元素A : (1..n / 2)(n + 1..n + n / 2 )
新的後n個元素B : (n / 2 + 1.. n) (n + n / 2 + 1..n)
因為n是偶數,我們得到了A,B兩個子問題。問題轉化為了求解n' = n / 2的兩個問題。
當n是奇數怎麼辦?我們可以把前半段多出來的那個元素先拿出來,後面所有元素前移,再把當時多出的那個元素放到末尾,這樣數列最後兩個元素已經滿足要求了。於是只考慮前2 * (n - 1)個元素就可以了,於是轉換成了(n - 1)的問題。
為了說明問題,我們還是用a, b 分別說明一下。假設n = 4是個偶數,我們要做的數列是:
a1, a2,a3,a4,b1,b2,b3,b4
我們先要把前半段的後2個元素(a3,a4)與後半段的前2個元素(b1,b2)交換,得到a1,a2,b1,b2,a3,a4,b3,b4。
於是,我們分別求解子問題A (a1,a2,b1,b2)和子問題B (a3,a4,b3,b4)就可以了。
如果n = 5,是奇數數怎麼辦?
我們原始的數組是a1,a2,a3,a4,a5,b1,b2,b3,b4,b5
我們先把a5拎出來,後面所有元素前移,再把a5放到最後,變為:
a1,a2,a3,a4,b1,b2,b3,b4,b5,a5
可見這時最後兩個元素b5,a5已經是我們要的結果了,所以我們只要考慮n=4就可以了。
那麼複雜度怎麼算?
每次,我們交換中間的n個元素,需要O(n)的時間,n是奇數的話,我們還需要O(n)的時間先把後兩個元素調整好,但這步影響總體時間複雜度。所以,無論如何都是O(n)的時間複雜度。
於是我們有 T(n) = T(n / 2) + O(n) 這個就是跟歸併排序一樣的複雜度式子,最終複雜度解出來T(n) = O(nlogn)。空間的話,我們就在數組內部折騰的,所以是O(1)。(當然沒有考慮遞迴的棧的空間)
代碼:
//時間O(nlogn) 空間O(1) 數組下標從1開始void perfect_shuffle2(int *a,int n) {int t,i; if (n == 1) { t = a[1]; a[1] = a[2]; a[2] = t; return; } int n2 = n * 2, n3 = n / 2; if (n % 2 == 1) { //奇數的處理 t = a[n]; for (i = n + 1; i <= n2; ++i) { a[i - 1] = a[i]; } a[n2] = t; --n; } //到此n是偶數 for (i = n3 + 1; i <= n; ++i) { t = a[i]; a[i] = a[i + n3]; a[i + n3] = t; } // [1.. n /2] perfect_shuffle2(a, n3); perfect_shuffle2(a + n, n3); }
(3) 完美洗牌演算法。
這個演算法源自一篇文章,文章很數學,可以只記結論就好了……
這個演算法的具體實現還是依賴於演算法1,和演算法2的。
首先,對於每一個元素,它最終都會到達一個位置,我們如果記錄每個元素應到的位置會形成圈。
為什麼會形成圈?
比如原來位置為a的元素最終到達b,而b又要達到c……,因為每個新位置和原位置都有一個元素,所以一條鏈
a->b->c->d……這樣下去的話,必然有一個元素會指向a,(因為中間那些位置b,c,d……都已經被其它元素指向了)。
這就是圈的成因。
比如 6個元素
原始是(1,2,3,4,5,6), 最終是(4,1,5,2,6,3),我們用a->b表示原來下標為a的元素,新下標為b了。
1->2
2->4
3->6
4->1
5->3
6->5
我們會發現1->2->4->1是個圈,3->6->5->3是另外一個圈。可以表示為(1,2,4) 和(3,6,5),演算法1已經告訴我們每個位置i的元素,都變為2 * i % (2 * n + 1),那麼我們只要知道圈裡的任意一個元素,順著走一遍就可以了,圈與圈是不相交的,所以這樣下來,我們就只走了O(n)步。
我們不能從1開始對每個元素都沿著圈走一圈。這是因為例如上例中(1,2,4)這個圈,我們只能從1,2, 4中的一個開始走一圈,而不能從1走一圈,再從2走一圈,再從4走一圈……因此我們可能還需要O(n)的空間記錄哪些元素所在的圈已經處理了,但是這樣做的時空複雜度等同於演算法1了。、(如果數組元素都是正數,我們可以把處理過的元素標記為負數,最後再取相反數,但是這樣做其實也是利用了額外的空間的)。那麼我們的關鍵問題是從每個圈裡選擇任意一個位置作代表,每個圈從這個位置位置走一圈。而空間複雜度要求我們最好不要提前把每個代表位置存下來。後文一個定理表明,我們在數組長度滿足一定條件情況下,每個圈中得最小元素可以簡單地表達出來。於是,我們稱每個圈的最小的位置叫做圈的頭部,用它來作圈的代表位置。如上例中圈(1,2,4)的頭部是1,(3,6,5)的頭部是3。如果我們知道了一個圈的頭部,用它做代表,沿著這個圈走就可以了。沿著圈走得算叫做cycle_leader,這部分代碼如下:
//數組下標從1開始,from是圈的頭部,mod是要模數的數 mod 應該為 2 * n + 1,時間複雜度O(圈長)void cycle_leader(int *a,int from, int mod) {int last = a[from],t,i; for (i = from * 2 % mod;i != from; i = i * 2 % mod) { t = a[i]; a[i] = last; last = t; } a[from] = last;}
那麼如何找到每個圈的頭部呢?引用一篇論文,名字叫:
A
Simple In-Place Algorithm for In-Shuffle.
Peiyush Jain, Microsoft Corporation.
利用數論知識,包括原根、甚至群論什麼的,論文給出了一個出色結論(*): 對於2 * n = (3^k - 1),這種長度的數組,恰好只有k個圈,並且每個圈的頭部是1,3,9,...3^(k - 1)。
這樣我們就解決了這種特殊的n作為長度的問題。那麼,對於任意的n怎麼辦?我們利用演算法2的思路,把它拆成兩部分,前一部分是滿足結論(*)。後一部分再單獨算。
為了把數組分成適當的兩部分,我們同樣需要交換一些元素,但這時交換的元素個數不相等,不能簡單地迴圈交換,我們需要更強大的工具——迴圈移。
假設滿足結論(*)的需要的長度是2 * m = (3^k - 1), 我們需要把n分解成m和n - m兩部分,按下標來說,是這樣:
原先的數組(1..m) (m + 1.. n) (n + 1..n + m)(n + m + 1..2 * n)
我們要達到的數組 (1..m)(n + 1.. n + m)(m + 1..n)(n + m + 1..2 * n)
可見,中間那兩段長度為(n - m)和m的段需要交換位置,這個相當於把(m + 1..n + m)的段迴圈右移m次,而迴圈右移是有O(長度)的演算法的, 主要思想是把前(n - m)個元素和後m個元素都翻轉一下,再把整個段翻轉一下。
迴圈移位的代碼:
//翻轉字串時間複雜度O(to - from)void reverse(int *a,int from,int to) {int t; for (; from < to; ++from, --to) { t = a[from]; a[from] = a[to]; a[to] = t; } }//迴圈右移num位 時間複雜度O(n)void right_rotate(int *a,int num,int n) { reverse(a, 1, n - num); reverse(a, n - num + 1,n); reverse(a, 1, n);}
再用a和b舉例一下,設n = 7這樣m = 4, k = 2
原先的數組是a1,a2,a3,a4,(a5,a6,a7),(b1,b2,b3,b4),b5,b6,b7。
結論(*)是說m = 4的部分可以直接搞定,
也就是說我們把中間加括弧的那兩段(a5,a6,a7) (b1,b2,b3,b4)交換位置,也就是把(a5,a6,a7,b1,b2,b3,b4)整體迴圈右移4位就可以得到:
(a1,a2,a3,a4,b1,b2,b3,b4)(a5,a6,a7,b5,b6,b7)
於是前m = 4個由演算法cycle_leading演算法直接搞定,n的長度減小了4。
所以這也是一種分治演算法。演算法流程:
輸入數組 a[1..2 * n]
step 1 找到 2 * m = 3^k - 1 使得 3^k <= 2 * n < 3^(k +1)
step 2 把a[m + 1..n + m]那部分迴圈移m位
step 3 對每個i = 0,1,2..k - 1,3^i是個圈的頭部,做cycle_leader演算法,數組長度為m,所以對2 * m + 1模數。
step 4 對數組的後面部分a[2 * m + 1.. 2 * n]繼續使用本演算法, 這相當於n減小了m。
時間複雜度分析:
(1) 因為迴圈不斷乘3的,所以時間複雜度O(logn)
(2) 迴圈移位O(n)
(3) 每個圈,每個元素只走了一次,一共2*m個元素,所以複雜度omega(m), 而m < n,所以 也在O(n)內。
(4) T(n - m)
因此 複雜度為 T(n) = T(n - m) + O(n) m = omega(n) 解得:總複雜度T(n) = O(n)。
演算法代碼:
//時間O(n),空間O(1)void perfect_shuffle3(int *a,int n) {int n2, m, i, k,t; for (;n > 1;) { // step 1 n2 = n * 2; for (k = 0, m = 1; n2 / m >= 3; ++k, m *= 3) ; m /= 2; // 2m = 3^k - 1 , 3^k <= 2n < 3^(k + 1) // step 2 right_rotate(a + m, m, n); // step 3 for (i = 0, t = 1; i < k; ++i, t *= 3) { cycle_leader(a , t, m * 2 + 1); } //step 4 a += m * 2; n -= m; } // n = 1 t = a[1]; a[1] = a[2]; a[2] = t; }
以上解決了完美洗牌問題,對於這個題,我們可以在完美洗牌之前把a的前n項與後n項交換掉,再調用perfect_shuffle3演算法。
代碼:
//時間複雜度O(n),空間複雜度O(1),數組下標從1開始,調用perfect_shuffle3void shuffle(int *a,int n) { int i,t; for (i = 1; i <= n; ++i) { t = a[i]; a[i] = a[i + n]; a[i + n] = t; } perfect_shuffle3(a,n);}
或者我們還可以先調用pefect_shuffle3演算法,再對每組的兩個相鄰元素做交換。
代碼:
//時間複雜度O(n),空間複雜度O(1),數組下標從1開始,調用perfect_shuffle3void shuffle(int *a,int n) { int i,t,n2 = n * 2; perfect_shuffle3(a,n); for (i = 2; i <= n2; i += 2) { t = a[i - 1]; a[i - 1] = a[i]; a[i] = t; }}
無論採取哪種方法,都額外增加O(n)的時間複雜度,總時間複雜度不變,仍然是O(n),空間複雜度是O(1)。