基於階乘數的全排列產生演算法,是另一種通過序列順序,輸出全排列的演算法。所謂階乘數,實際上和我們常用的2進位,8進位,10進位,16進位一樣,是一種數值的表示形式,所不同的是,上面這幾種進位數,相鄰位之間的進位是固定值,以10進位為例,第n位與第n+1位之間的進位是10,而階乘數,相鄰兩位之間的進位是變值,第n位與第n+1位之間的進位是(n+1)!。對於10進位數,每一位的取值範圍也是固定的0~9,而階乘數每一位的取值範圍為0~n。可以證明,任何一個數量,都可以由一個階乘數唯一表示。下面以23為例,說明其在各種進位中的表現形式
| |
2進位 |
8進位 |
10進位 |
16進位 |
階乘數 |
| 23 |
10111 |
27 |
23 |
17 |
3210 |
其中10進位23所代表的數量的計算方法為
D(23) = 2×10^1 + 3×10^0 = 2×10 + 3×1 = 23
階乘數3210所代表的數量的計算方法為
F(3210) = 3×3! + 2×2! + 1×1! + 0×0! = 3×6 + 2×2 + 1×1 + 1×0 = 23
對於階乘數而言,由於階乘的增長速度非常快,所以其可以表示的數值的範圍隨著位元的增長十分迅速,對於n位的階乘數而言,其表示的範圍從0~(n+1)!-1,總共(n+1)!個數。階乘數有很多性質這裡我們只介紹其和全排列相關的一些性質。
首先是加法操作,與普通十進位數的加法基本一樣,所不同的是對於第n位F[n](最低位從第0位開始),如果F[n]+1>n,那麼我們需要將F[n]置0,同時令F[n+1]+1,如果對於第n+1位,也導致進位,則向高位依次執行進位操作。這裡我們看一下F(3210)+1,對於第0位,有F[0]+1=0+1=1>0,所以F[0]=0(實際上階乘數的第0位一直是0),F[1]+1=1+1=2>1,F[1]=0,……,依次執行,各位都發生進位,最終結果F(3210)+1=F(10000)。
其次,對於n位的階乘數,每一個階乘數的各位的數值,正好對應了一個n排列各位的逆序關係。這裡以abcd為例。例如F(2110),其對應的排列的意思是,對於排列的第一個元素,其後有兩個元素比他小;第二個元素,後面有一個元素比他小;第三個元素,後面有一個元素比他小。最終根據F(2110)構建的排列為cbda。4位的階乘數,與4排列的對應關係如下表所示。
| 0000 |
abcd |
1000 |
bacd |
2000 |
cabd |
3000 |
dabc |
| 0010 |
abdc |
1010 |
badc |
2010 |
cadb |
3010 |
dacb |
| 0100 |
acbd |
1100 |
bcad |
2100 |
cbad |
3100 |
dbac |
| 0110 |
acdb |
1110 |
bcda |
2110 |
cbda |
3110 |
dbca |
| 0200 |
adbc |
1200 |
bdac |
2200 |
cdab |
3200 |
dcab |
| 0210 |
adcb |
1210 |
bdca |
2210 |
cdba |
3210 |
dcba |
由此,我們就可以利用階乘數與排列的對應關係構建集合的全排列,演算法如下。
- 對於n個元素的全排列,首先產生n位的階乘數F[0...n-1],並令F[0...n-1]=0。
- 每次對F[0...n-1]執行+1操作,所得結果,根據其與排列的逆序對應關係,產生排列。
- 直到到達F[0...n-1]所能表示的最大數量n!-1為止,全部n!個排列產生完畢。
這裡有一個問題需要解決,就是如何根據階乘數,及其與排列逆序的對應關係產生對應的排列,這裡給出一個方法,
- 以字典序最小的排列a[0...n-1]作為起始,令i從0到n-2。
- 如果F[i]=0,遞增i。
- 否則令t=a[i+F[i]],同時將a[i...i+F[i]-1]區間的元素,向後移動一位,然後令a[i]=t,遞增i。
下面說明一下如何根據階乘數F(2110)和初始排列abcd,構建對應的排列。首先,我們發現F[0]=2,所以我們要將a[0+2]位置的元素c放在a[0]位置,之前,先用臨時變數t記錄a[2]的值,然後將a[0...0+2-1]區間內的元素向後移動一位,然後令a[0]=t,得到cabd,i值增加1;然後有F[1]=1,所以我們要將a[1+1]=a[2]=b放在a[1]位置,同時將a[1]向後移動一位,得到排列cbad;然後有F[2]=1,所以將a[2+1]=a[3]=d放在a[2]位置,同時a[2]向後移動一位。最終得到cbda,排列產生結束。整個演算法代碼如下
inline int FacNumNext(unsigned int* facnum, size_t array_size){unsigned int i = 0;while(i < array_size){if(facnum[i] + 1 <= i){facnum[i] += 1;return 0;}else{facnum[i] = 0;++i;}}return 1;}/* * 根據階乘數所指定的逆序數根據原始字串構建排列輸出 */inline void BuildPerm(const char* array, size_t array_size, const unsigned int* facnum, char* out){char t;unsigned int i, j;memcpy(out, array, array_size * sizeof(char));for(i = 0; i < array_size - 1; ++i){j = facnum[array_size - 1 - i];if(j != 0){t = out[i + j];memmove(out + i + 1, out + i, j * sizeof(char));out[i] = t;}}}/* * 基於階乘數(逆序數)的全排列產生演算法 */void FullArray(char* array, size_t array_size){unsigned int facnum[array_size];char out[array_size];for(unsigned int i = 0; i < array_size; ++i){facnum[i] = 0;}BuildPerm(array, array_size, facnum, out);for(unsigned int i = 0; i < array_size; ++i){cout << out[i] << ' ';}cout << '\n';while(!FacNumNext(facnum, array_size)){BuildPerm(array, array_size, facnum, out);for(unsigned int i = 0; i < array_size; ++i){cout << out[i] << ' ';}cout << '\n';}}
用該演算法產生1234全排列,順序如,該圖來自與Wiki百科。
從產生排列順序的角度講,概演算法相較於字典序和最小變更有明顯優勢,但是在實際應用中,由於根據階乘數所定義的逆序構建排列是一個O(n^2)時間複雜度的過程,所以演算法的整體執行效率遜色不少。但是通過階乘數建立逆序數與排列對應關係的思路,還是十分精彩的,值得借鑒。