問題描述:數組元素循環左移,將包含 num_elem 個元素的一維數組 arr[num_elem] 循環左移 rot_dist 位。可否僅使用數十個額外字節的存儲空間,在正比於num_elem的時間內完成數組的旋轉?算法
一:Bentley's Juggling Alogrithm編程
移動變量 arr[0] 到臨時變量 tmp,移動 arr[rot_dist] 到 arr[0],arr[2rot_dist] 到 arr[rot_dist],依此類推,直到返回到取 arr[0] 中的元素,此時改成從 tmp 取值,程序結束。數組
這個方法須要保證:1. 可以遍歷全部的數組元素;2. arr[0] (即 tmp 的值)在最後一步賦給某個合適的數組元素。緩存
當 num_elem 和 rot_dist 互素的時候上述條件天然知足,不然不知足。從代數的觀點來看,當 num_elem 和 rot_dist 互素的時候上述遍歷規則將 0, ... , num_elem 個元素進行了輪換,而當 num_elem 和 rot_dist 不互素時(記最大公約數爲 common_divisor),上述遍歷規則將構成 common_divisor 個不相交的輪換,對 num_elem/common_divisor 個元素進行輪換。app
1 unsigned gcd(unsigned m, unsigned n) 2 { 3 unsigned remainder; 4 while(n > 0) { 5 remainder = m % n; 6 m = n; 7 n = remainder; 8 } 9
10 return m; 11 } 12
13 // left rotate @arr containing @num_elem elements by @rot_dist positions 14 // Bentley's Juggling Algorithm from Programming Pearls
15 template<typename _Type>
16 void array_left_rotation_juggling(_Type *arr, int num_elem, int rot_dist) 17 { 18 if(rot_dist == 0 || rot_dist == num_elem) 19 return; 20
21 _Type tmp; 22 int i, j, k; 23
24 int common_divisor = gcd(num_elem, rot_dist); 25 for(i = 0; i < common_divisor; ++i) { 26 tmp = arr[i]; 27 j = i, k = (j + rot_dist) % num_elem; 28 while(k != i) { 29 arr[j] = arr[k]; 30 j = k; 31 k = (k + rot_dist) % num_elem; 32 } 33 arr[j] = tmp; 34 } 35 }
二:Gries and Mills Block Swapping
函數
將包含 num_elem 個元素的數組循環左移 rot_dist 位,等價於交換兩個數組塊 arr[0, rot_dist-1] 和 arr[rot_dist, num_elem-1] 的位置(即 Block Swapping問題),用 X,Y 來表示這兩個數組塊。oop
在第 2 種狀況(第 3 種狀況)操做完成以後,問題已經被約減,繼續交換對X2X1 (第3種狀況下是 Y1Y2),便可解決問題。這就是遞歸的解決思路了。這是《編程珠璣》上對這個方法的簡單描述。看到這裏就一頭霧水,查閱《The Science of Programming》的18.1節還有這裏算是搞明白了,在此整理下。性能
先看一個簡單示例,用上面的思路將包含 7 個元素的數組循環左移 2 位,數組元素值依次爲 0, 1, 2, 3, 4, 5, 6,圖片來自這裏。測試
圖中紅色的表示較短的塊。觀察執行過程能夠發現這樣幾個點:spa
首先須要可以交換兩個等長的數組塊的功能,函數實現以下。
1 // swap two blocks of equal length 2 // there must be no overlap between two blocks 3 template<typename _Type> 4 void swap_equal_blocks(_Type *arr, int beg1, int beg2, int num) 5 { 6 while(num-- > 0) 7 std::swap(arr[beg1++], arr[beg2++]); 8 }
由於須要考慮兩個塊的長度,分別記左右兩個塊的長度位 i 和 j,即左側有 i 個元素仍待處理,右側有 j 個元素待處理,i + j 表示此時尚未位於最終位置的元素個數。下圖畫出了初始狀態以及 i > j 和 i < j 兩種狀況下的執行一次交換以後的狀態。圖中用 r 指代 rot_dist,用 n 指代 num_elem,灰色部分表示已經位於最終位置。
根據上圖能夠概括出以下幾個關係:
據此,則有以下實現。
1 template<typename _Type> 2 void array_left_rotation_blockswapping(_Type *arr, int num_elem, int rot_dist) 3 { 4 if(rot_dist == 0 || rot_dist == num_elem) 5 return; 6 7 int i = rot_dist, j = num_elem - rot_dist; 8 while(i != j) { // could be dead loop when rot_dist equals to 0 or num_elem 9 // Invariant: 10 // arr[0 : rot_dist-i-1] is in final position 11 // arr[rot_dist-i : rot_dist-1] is the left part, length i 12 // arr[rot_dist : rot_dist+j-1] is the right part, length j 13 // arr[rot_dist+j : num_elem-1] is in final position 14 if(i > j) { 15 swap_equal_blocks(arr, rot_dist-i, rot_dist, j); 16 i -= j; 17 } 18 else { 19 swap_equal_blocks(arr, rot_dist-i, rot_dist+j-i, i); 20 j -= i; 21 } 22 } 23 swap_equal_blocks(arr, rot_dist-i, rot_dist, i); 24 }
三:Reversal Algorithm
這個方法的最好說明就是 Doug Mcllroy 用雙手給出的示例了,見下圖。圖中將數組循環左移了 5 位。
對應到這裏的表示就是用三次反轉實現循環左移。實現代碼以下:
// reverse the elements @arr[@low : @high] void reverse(char *arr, int low, int high) { while(low < high) std::swap(arr[low++], arr[high--]); } template<typename _Type> void array_left_rotation_reversal(_Type *arr, int num_elem, int rot_dist) { if(rot_dist == 0 || rot_dist == num_elem) return; reverse(arr, 0, rot_dist-1); reverse(arr, rot_dist, num_elem-1); reverse(arr, 0, num_elem-1); }
關於性能:這三種方法的時間複雜度均爲 O(n)。在這裏,Victor J. Duvanenko 用Intel C++ Composer XE 2011在 Intel i7 860 (2.8GHz, with TurboBoost up to 3.46GHz) 對三種算法進行的實際的性能測試,結果顯示 Gries-Mills 算法運行時間最短,Reversal 算法居第二位,但與 Gries-Mills 算法在運行時間上相差不多。可是 Reversal 有一個優點是在屢次的測試種,Reversal 算法的運行時間很是穩定(即屢次所測時間的標準差很小),而 Juggling 算法在運行時間和性能穩定性方面均教差。Duvanenko 具體分析了致使這一結果的緣由:Gries-Mills 算法和 Reversal 算法的良好表現是因爲它們的緩存友好的內存讀取模式,而 Juggling 算法的內存讀取模式不是緩存友好的("The Gries-Mills and Reversal algorithms preformed well due to their cache-friendly memeory access patterns. The Juggling algorithm preformed the fewest memory accesses, but came in 5x slower dut to its cache-unfriendly memory access pattern.)。總之,Juggling 算法較其餘兩個算法的執行效率不好,而 Reversal 算法和 Gries-Mills 算法相比,具備基本相同的運行時間,可是 Reversal 算法運行效率更爲穩定,算法原理更容易理解,代碼實現也更爲簡潔。