設計一個演算法,把一個含有N個元素的數組迴圈右移K位,要求:時間複雜度為O(N),且只允許使用兩個附加變數。
最直接的想法是“一步到位”,盡量避免資料移動或者交換,於是有像下面的這樣的代碼:
template<typename T>void shiftArrRightCir(T* arr, const int N, int k){ k %=N; const int divN =N/k; for (int i = 0; i < k; i++) { T tmp=arr[i]; for (int j = 0; j < divN; j ++) { arr[(N +i -j*k)%N]= arr[(N +i -j*k -k)%N]; } arr[(i +k)%N] =tmp; } }
調試了下之後發現一個問題,只有 k 與 n 剛好整除時才運行正常,否則會有 n%k 個資料沒有更新或者丟失,於是再上網,看到有高人是這樣處理的: http://www.dewen.org/q/6262
template<typename T>void shiftArrRightCir(T* arr, const int n, int k){ k %= n; //求出N和k的最大公約數(歐幾裡得輾轉相除法) int g1,g2; g1 = n; g2 = k; while(g2 != 0) { g1 = g1 % g2; g1 = g1 ^ g2; g2 = g1 ^ g2; g1 = g1 ^ g2; } //複用變數g1,g2做為迴圈變數 for(g1--;g1>=0;g1--) { for(g2 = g1; (g2 + n - k) % n != g1; g2 = (g2 + n - k) % n) { arr[g2] = arr[g2] ^ arr[(g2 + n - k) % n]; arr[(g2 + n - k) % n] = arr[g2] ^ arr[(g2 + n - k) % n]; arr[g2] = arr[g2] ^ arr[(g2 + n - k) % n]; } }}
原理:迴圈移位時,每個元素m移到(m+k) % n的位置上,而(m+k)%n移到(m+2k)%n的位置上……
當n與k互質時,可以證明m, m+k, m+2k, ..., m + (n-1)k模n的結果互不相等,剛好構成一個迴圈;否則,會構成(n,k)(n與k的最大公約數)個迴圈。分別處理這(n,k)個迴圈就可以得到結果。
對應地,給上面的一段代碼稍做改動,並加上注釋,是不是要好理解多了呢?
template<typename T>void shiftArrRightCir(T* arr, const int n, int k){ k %= n; //求出N和k的最大公約數(歐幾裡得輾轉相除法) int g1 = n; int g2 = k; while(g2 != 0) //餘數為零時中止演算法,g1就是最大公約數 { g1 = g1 % g2; //較大的數對較小的數取餘 g1 = g1 ^ g2; //異或法交換 g1 g2的值 g2 = g1 ^ g2; g1 = g1 ^ g2; } //複用變數g1,g2做為迴圈變數 for(--g1; g1>=0; --g1) { g2 =g1; int z =(g2 + n - k) % n; while(z != g1) //是否達到這一輪的起點 { //異或法交換 z g2 所指的值 arr[g2] = arr[g2] ^ arr[z]; arr[z] = arr[g2] ^ arr[z]; arr[g2] = arr[g2] ^ arr[z]; //修正下標值,指向下一次 要交換的資料對 g2 = (g2 + n - k) % n; z = (g2 + n - k) % n; } }}
到了這裡,不禁想,這種思路能否與前面的“一步到位”想法相結合呢? 省去多次交換,效率豈不更是高效?
template<typename T>void shiftArrRightCir(T* arr, const int n, int k){ k %= n; //求出N和k的最大公約數(歐幾裡得輾轉相除法) int g1 = n; int g2 = k; while(g2 != 0) //餘數為零時中止演算法,g1就是最大公約數 { g1 = g1 % g2; //較大的數對較小的數取餘 g1 = g1 ^ g2; //異或法交換 g1 g2的值 g2 = g1 ^ g2; g1 = g1 ^ g2; } //複用變數g1,g2做為迴圈變數 for(--g1; g1>=0; --g1) { T tmp=arr[g1]; //暫時儲存起點值 g2 =g1; //g2 此時起總是指向資料 接收/移入 方下標 int g3 =(g2 + n - k) % n; // g3 總是指向下一個要 移出 的資料下標 while(g3 != g1) //是否達到這一輪的起點 { arr[g2] = arr[g3]; //修正下標值,指向下一次 要移動的資料 g2 = g3; g3 = (g3 + n - k) % n; } arr[g2]=tmp; //移動起點 }}
調試了一下,基本通過,爽!!!
當然也有人使用三次逆序方法達到了目的,應該是相對容易理解的高效方法:
Reverse(int *arr, int b, int e) //逆序排列 { for( ; b < e; b++, e--) //從數組的前、後一起遍曆 { int temp = arr[e]; arr[e] = arr[b]; arr[b] = temp; } } RightShift(int *arr, int N, int K) { K = K % N ; Reverse(arr, 0, N-K-1); //前面N-K部分逆序 Reverse(arr, N-K, N-1); //後面K部分逆序 Reverse(arr, 0, N-1); //全部逆序 }
還有其它使用遞迴分治什麼思路的,代碼比較冗長,沒興趣關注,呵呵
一個問題,上述方法,如果要改造為左移,適應性強嗎?
直接在 k %=n, 之後增加 k =n -k,然後再右移就好?