全排列的生成算法有不少種,有遞歸遍例,也有循環移位法等等。C++/STL中定義的next_permutation和prev_permutation函數則是很是靈活且高效的一種方法,它被普遍的應用於爲指定序列生成不一樣的排列。本文將詳細的介紹prev_permutation函數的內部算法。git
按照STL文檔的描述,next_permutation函數將按字母表順序生成給定序列的下一個較大的序列,直到整個序列爲減序爲止。prev_permutation函數與之相反,是生成給定序列的上一個較小的序列。兩者原理相同,僅遍例順序相反,這裏僅以next_permutation爲例介紹算法。算法
下文內容都基於一個假設,即序列中不存在相同元素。對序列大小的比較作出定義:兩個長度相同的序列,從二者的第一個元素開始向後比較,直到出現一個不一樣元素(也可能就是第它們的第一個元素),該元素較大的序列爲大,反之序列爲小;若一直到最後一個元素都相同,那麼兩個序列相等。函數
設當前序列爲pn,下一個較大的序列爲pn+1,那麼不存在pm,使得pn < pm < pn+1。spa
給定任意非空序列,生成下一個較大或較小的序列。code
根據上述概念易知,對於一個任意序列,最小的序列是增序,最大的序列爲減序。那麼給定一個pn要如何才能生成pn+1呢?先來看下面的例子:orm
咱們用<a1 a2 ... am>來表示m個數的一種序列。設序列pn=<3 6 4 2>,根據定義可算得下一個序列pn+1=<4 2 3 6>。觀察pn能夠發現,其子序列<6 4 2>已經爲減序,那麼這個子序列不可能經過交換元素位置得出更大的序列了,所以必須移動最高位3(即a1)的位置,且要在子序列<6 4 2>中找一個數來取代3的位置。子序列<6 4 2>中6和4都比3大,但6大於4。若是用6去替換3獲得的序列必定會大於4替換3獲得的序列,所以只能選4。將4和3的位置對調後造成排列<4 6 3 2>。對調後獲得的子序列<6 3 2>仍保持減序,即這3個數可以生成的最大的一種序列。而4是第1次做爲首位的,須要右邊的子序列最小,所以4右邊的子序列應爲<2 3 6>,這樣就獲得了正確的一個序列pn+1=<4 2 3 6>。blog
下面概括分析該過程。假設一個有m個元素的序列pn,其下一個較大序列爲pn+1。遞歸
1) 若pn最右端的2個元素構成一個增序子序列,那麼直接反轉這2個元素使該子序列成爲減序,便可獲得pn+1。文檔
2) 若pn最右端一共有連續的s個元素構成一個減序子序列,令i = m - s,則有pn(i) < pn(i+1),其中pn(i)表示排列pn的第i個元素。例如pn=<1 2 5 4 3>,那麼pn的右端最多有3個元素構成一個減序子集<5 4 3>,i=5-3=2,則有pn(i)=2 < 5=pn(i+1)。所以若將pn(i)和其右邊的子集s {pn(i+1), pn(i+2), ..., pn(m)}中任意一個元素調換必能獲得一個較大的序列(不必定是下一個)。要保證是下一個較大的序列,必須保持pn(i)左邊的元素不動,並在子集s {pn(i+1), pn(i+2), ..., pn(m)}中找出全部比pn(i)大的元素中最小的一個pn(j),即不存在pn(k) ∈ s且pn(i) < pn(k) < pn(j),而後將兩者調換位置。如今只要使新子集{pn(i+1), pn(i+2), ..., pn(i), ...,pn(m)}成爲最小序列即獲得pn+1。注意到新子集仍保持減序,那麼此時直接將其反轉便可獲得pn+1 {pn(1), pn(2), ..., pn(j), pn(m), pn(m-1), ..., pn(i), ..., pn(i+2), pn(i+1)}。數學
最好的狀況爲pn的最右邊的2個元素構成一個最小的增序子集,交換次數爲1,複雜度爲O(1),最差的狀況爲1個元素最小,而右面的全部元素構成減序子集,這樣須要先將第1個元素換到最右,而後反轉右面的全部元素。交換次數爲1+(n-1)/2,複雜度爲O(n)。由於各類排列等可能出現,因此平均複雜度即爲O(n)。
1. 可否直接算出集合{1, 2, ..., m}的第n個排列?
設某個集合{a1, a2, ..., am}(a1<a2<...<am)構成的某種序列pn,基於以上分析易證得:若as<at,那麼將as做爲第1個元素的全部序列必定都小於at做爲第1個元素的任意序列。同理可證得:第1個元素肯定後,剩下的元素中若as'<at',那麼將as'做爲第2個元素的全部序列必定都小於做爲第2個元素的任意序列。例如4個數的集合{2, 3, 4, 6}構成的序列中,以3做爲第1個元素的序列必定小於以4或6做爲第1個元素的序列;3做爲第1個元素的前題下,2做爲第2個元素的序列必定小於以4或6做爲第2個元素的序列。
推廣可知,在肯定前i(i<n)個元素後,在剩下的m-i=s個元素的集合{aq1, aq2, ..., aq3}(aq1<aq2<...<aqm)中,以aqj做爲第i+1個元素的序列必定小於以aqj+1做爲第i+1個元素的序列。由此可知:在肯定前i個元素後,一共可生成s!種連續大小的序列。
根據以上分析,對於給定的n(必有n<=m!)能夠從第1位開始向右逐位地肯定每一位元素。在第1位不變的前題下,後面m-1位一共能夠生成(m-1)!中連續大小的序列。若n>(m-1)!,則第1位不會是a1,n中能夠容納x個(m-1)!即表明第1位是ax。在肯定第1位後,將第1位從原集合中刪除,獲得新的集合{aq1, aq2, ..., aq3}(aq1<aq2<...<aqm),而後令n1=n-x(m-1)!,求這m-1個數中生成的第n1個序列的第1位。
舉例說明:如7個數的集合爲{1, 2, 3, 4, 5, 6, 7},要求出第n=1654個排列。
(1654 / 6!)取整得2,肯定第1位爲3,剩下的6個數{1, 2, 4, 5, 6, 7},求第1654 % 6!=214個序列;
(214 / 5!)取整得1,肯定第2位爲2,剩下5個數{1, 4, 5, 6, 7},求第214 % 5!=94個序列;
(94 / 4!)取整得3,肯定第3位爲6,剩下4個數{1, 4, 5, 7},求第94 % 4!=22個序列;
(22 / 3!)取整得3,肯定第4位爲7,剩下3個數{1, 4, 5},求第22 % 3!=4個序列;
(4 / 2!)得2,肯定第5爲5,剩下2個數{1, 4};因爲4 % 2!=0,故第6位和第7位爲增序<1 4>;
所以全部排列爲:3267514。
2. 給定一種排列,如何算出這是第幾個排列呢?
和前一個問題的推導過程相反。例如3267514:
後6位的全排列爲6!,3爲{1, 2, 3 ,4 , 5, 6, 7}中第2個元素(從0開始計數),故2*720=1440;
後5位的全排列爲5!,2爲{1, 2, 4, 5, 6, 7}中第1個元素,故1*5!=120;
後4位的全排列爲4!,6爲{1, 4, 5, 6, 7}中第3個元素,故3*4!=72;
後3位的全排列爲3!,7爲{1, 4, 5, 7}中第3個元素,故3*3!=18;
後2位的全排列爲2!,5爲{1, 4, 5}中第2個元素,故2*2!=4;
最後2位爲增序,所以計數0,求和得:1440+120+72+18+4=1654
next_Permutation Code 以下
1 class Solution { 2 public: 3 void nextPermutation(vector<int> &num) { 4 // Start typing your C/C++ solution below 5 // DO NOT write int main() function 6 int i; 7 vector<int>::iterator iter = num.end(); 8 int maxNum = INT_MIN; 9 10 //step 1, find the first number which violate the increase form right to left, we call it AAA 11 // take 5 6 7 8 4 3 2 1 for a example, find AAA = 7; 12 for(i = num.size() - 2; i >= 0; i--) 13 { 14 if (num[i] < num[i+1]) 15 break; 16 } 17 18 if (i >= 0) //i found 19 { 20 int j; 21 int minNum = INT_MAX; 22 //step 2, find the first number which is large than AAA form right to left, we call it BBB 23 // take 5 6 7 8 4 3 2 1 for a example, find BBB = 8; 24 for(j = num.size() - 1; j >= 0; j--) 25 { 26 if (num[j] > num[i]) 27 break; 28 } 29 30 //step 3, swap AAA and BBB 31 // take 5 6 7 8 4 3 2 1 for a example, exchanged array is 5 6 8 7 4 3 2 1 32 int t = num[i]; 33 num[i] = num[j]; 34 num[j] = t; 35 36 //step 4, reverse all digit after AAA's position 37 // take 5 6 7 8 4 3 2 1 for a example, exchanged array is 5 6 8 7 4 3 2 1, then reverse 7 4 3 2 1 38 // the output array is 5 6 8 1 2 3 4 7 39 int k = i + 1; 40 j = num.size() - 1; 41 while(k < j) 42 { 43 t = num[k]; 44 num[k] = num[j]; 45 num[j] = t; 46 47 k++; 48 j--; 49 } 50 } 51 else 52 //sort(num.begin(), num.end()); 53 { 54 int k = 0; 55 int j = num.size() - 1; 56 while(k < j) 57 { 58 int t = num[k]; 59 num[k] = num[j]; 60 num[j] = t; 61 62 k++; 63 j--; 64 } 65 } 66 } 67 };
總結:
next_permutation的實現過程以下:
首先,從最尾端開始往前尋找兩個相鄰的元素,令第一個元素是i, 第二個元素是ii,且知足i<ii; (找破壞了遞增的那個元素AAA,位置m)
而後,再從最尾端開始往前搜索,找出第一個大於i的元素,設其爲j;(找比AAA大的元素BBB)
而後,將i和j對調,再將ii及其後面的全部元素反轉。(交換AAA 和BBB,而後reverse m右邊的元素)
這樣獲得的新序列就是「下一個排列」。
prev_permutation的實現過程以下:
首先,從最尾端開始向前尋找兩個相鄰的元素,令第一個元素爲i,第二個元素爲ii,且知足i>ii(找破壞了遞減的那個元素AAA,位置m)
而後,從最尾端開始往前尋找第一個小於i的元素,令它爲j(找比AAA小的元素BBB)
而後,將i和j對調,再將ii及其以後的全部元素反轉。(交換AAA 和BBB,而後reverse m右邊的元素)
這樣獲得的序列就是該排列的上一個排列。