《編程珠璣》筆記:數組循環左移

問題描述:數組元素循環左移,將包含 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

  1. 當 X 和 Y 的長度相等時,直接交換兩個數組塊 XY -> YX 便可。
  2. 當 X 包含的元素個數較多時,將 X 分拆爲兩部分 X1 和 X2,其中 X1 的長度等於 Y 的長度,交換 X1 和 Y:X1X2Y -> YX2X1,此時 Y 位於循環左移以後(塊交換以後)所應處於的位置。
  3. 當 Y 包含的元素個數較多時,將 Y 分拆爲兩部分 Y1 和 Y2,其中 Y2 的長度等於 X 的長度,交換 X 和 Y2: XY1Y2 -> Y2Y1X,此時 X 位於循環左移以後(塊交換以後)所應處於的位置(後面成爲最終位置)。

在第 2 種狀況(第 3 種狀況)操做完成以後,問題已經被約減,繼續交換對X2X1 (第3種狀況下是 Y1Y2),便可解決問題。這就是遞歸的解決思路了。這是《編程珠璣》上對這個方法的簡單描述。看到這裏就一頭霧水,查閱《The Science of Programming》的18.1節還有這裏算是搞明白了,在此整理下。性能

先看一個簡單示例,用上面的思路將包含 7 個元素的數組循環左移 2 位,數組元素值依次爲 0, 1, 2, 3, 4, 5, 6,圖片來自這裏測試

圖中紅色的表示較短的塊。觀察執行過程能夠發現這樣幾個點:spa

  1. 將數組分紅左右兩塊以後,短塊與長塊的一個子塊進行交換以後,這個短塊中的元素便位於最終位置,後續操做再也不修改該部分。
  2. 與這個短塊進行交換的子塊位於長塊(執行一次交換以後,長塊相應縮短)中遠離短塊的一端。
  3. 當兩個塊長度相等,交換這兩個等長的塊後程序執行完畢。

首先須要可以交換兩個等長的數組塊的功能,函數實現以下。

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. i > j 時,交換的兩部分是從下標 r-i 和下標 r 開始的 j 個元素。
  2. i < j 時,交換的兩部分時從下標 r-i 和下標 r+j-i 開始的 i 個元素。
  3. arr[0 : r-i-1] 以及 arr[r+j : n-1] 已經位於最終位置。
  4. 左側待處理的 i 個元素老是 arr[r-i : r-1],右側待處理的 j 個元素老是 arr[r : r+j-1]。

據此,則有以下實現。

 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 算法運行效率更爲穩定,算法原理更容易理解,代碼實現也更爲簡潔。

相關文章
相關標籤/搜索