排列(Arrangement),簡單講是從N個不一樣元素中取出M個,按照必定順序排成一列,一般用A(M,N)表示。當M=N時,稱爲全排列(Permutation)。從數學角度講,全排列的個數A(N,N)=(N)*(N-1)*...*2*1=N!,但從編程角度,如何獲取全部排列?那麼就必須按照某種順序逐個得到下一個排列,一般按照升序順序(字典序)得到下一個排列。git
例如對於一個集合A={1,2,3,},首先獲取全排列a1: 1,2,3,;而後獲取下一個排列a2: 1,3,2,;按此順序,A的全排列以下:編程
a1: 1,2,3; a2: 1,3,2; a3: 2,1,3; a4: 2,3,1; a5: 3,1,2; a6: 3,2,1; 共6種。數組
1)下一個全排列(Next Permutation)less
對於給定的任意一種全排列,若是能求出下一個全排列的狀況,那麼求得全部全排列狀況就容易了。好在STL中的algorithm已經給出了一種健壯、高效的方法,下面進行介紹。ide
設目前有一個集合的一種全排列狀況A : 3,7,6,2,5,4,3,1,求取下一個排列的步驟以下:函數
/** Tips: next permuation based on the ascending order sort * sketch : * current: 3 7 6 2 5 4 3 1 . * | | | | * find i----+ j k +----end * swap i and k : * 3 7 6 3 5 4 2 1 . * | | | | * i----+ j k +----end * reverse j to end : * 3 7 6 3 1 2 4 5 . * | | | | * find i----+ j k +----end * */
具體方法爲:oop
a)從後向前查找第一個相鄰元素對(i,j),而且知足A[i] < A[j]。優化
易知,此時從j到end必然是降序。能夠用反證法證實,請自行證實。idea
b)在[j,end)中尋找一個最小的k使其知足A[i]<A[k]。.net
因爲[j,end)是降序的,因此必然存在一個k知足上面條件;而且能夠從後向前查找第一個知足A[i]<A[k]關係的k,此時的k必是待找的k。
c)將i與k交換。
此時,i處變成比i大的最小元素,由於下一個全排列必須是與當前排列按照升序排序相鄰的排列,故選擇最小的元素替代i。
易知,交換後的[j,end)仍然知足降序排序。由於在(k,end)中必然小於i,在[j,k)中必然大於k,而且大於i。
d)逆置[j,end)
因爲此時[j,end)是降序的,故將其逆置。最終得到下一全排序。
注意:若是在步驟a)找不到符合的相鄰元素對,即此時i=begin,則說明當前[begin,end)爲一個降序順序,即無下一個全排列,STL的方法是將其逆置成升序。
2)Next Permutation代碼
// STL next permutation base idea int next_permutation(int *begin, int *end) { int *i=begin, *j, *k; if (i==end || ++i==end) return 0; // 0 or 1 element, no next permutation for (i=end-1; i!=begin;) { j = i--; // find last increasing pair (i,j) if (!(*i < *j)) continue; // find last k which not less than i, for (k=end; !(*i < *(--k));); iter_swap(i,k); // now the range [j,end) is in descending order reverse(j,end); return 1; } // current is in descending order reverse(begin,end); return 0; }
上面僅僅是STL中next_permutation的主要思路,原版是C++迭代器版,這裏爲了便於理解,改爲了C的指針版本。
當返回爲1時,表示找到了下一全排列;返回0時,表示無下一全排列。注意,若是從begin到end爲降序,則代表全排列結束,逆置使其還原到升序。
3)使用next_permutation
如何獲取全部全排列狀況?STL中的代碼很是精妙,利用next_permutation的返回值,判斷是否全排列結束(不然將死循環)。對於給定的一個數組,打印其全部全排列只需以下:
// Display All Permutation void all_permutation(int arr[], int n) { sort(arr,arr+n); // sort arr[] in ascending order do{ for(int i=0; i<n; printf("%d ",arr[i++])); printf("\n"); }while(next_permutation(arr,arr+n)); }
若是一個數組arr[]中存在重複元素,next_permutation是否工做正常呢?注意第8和10行,STL使用「!(*i < *j)」進行判斷大小,若相等則繼續尋找,這樣就會跳太重複的元素,進而跳太重複的全排列(如:1,2,2; 和1,2,2)。有人會認爲直接使用「*i>=*j」更清晰,對於int這種進本數據類型而言,這並沒問題。然而,對於結構體甚至C++而言,元素是一個用戶自定義數據類型,如何判斷其大小?再退一步講,如何進行排序?STL追求健壯、高效和精妙,對於用戶自定義數據類型的排序,能夠增長函數指針或者仿函數(Functional),只須要給定「a<b」的方法(如less(a,b))便可。如需求「a>b」能夠轉化成「b<a」;求「a==b」能夠轉化成「!(a<b) && !(b<a)」;求「a>=b」能夠轉化成「!(a<b)」。所以,通常自定義比較器只須要給定less()便可(對於C++而言,即重載操做符operator<)。
有了全排列,那麼排列問題A(M,N)則解決了一半,直接從A中選擇選擇M個元素,而後對這M個元素進行全排列。其中前一步爲組合(Combination),記爲(M,N),感興趣的能夠本身解決。
4)前一個全排列(prev_permutation)
與next_permutation相似,STL也提供一個版本:
// STL prev permutation base idea int prev_permutation(int *begin, int *end) { int *i=begin, *j, *k; if (i==end || ++i==end) return 0; // 0 or 1 element, no prev permutation for (i=end-1; i!=begin;) { j = i--; // find last decreasing pair (i,j) if (!(*i > *j)) continue; // find last k which less than i, for (k=end; !(*i > *(--k));); iter_swap(i,k); // now the range [j,end) is in ascending order reverse(j,end); return 1; } // current is in ascending order reverse(begin,end); return 0; }
這裏再也不詳細介紹。
5)STL源碼next_permutation分析
前面說到STL很是健壯、高效和精妙,下面以next_permutation做分析:
// STL next_permutation template <class BidirectionalIterator> bool next_permutation( BidirectionalIterator first, // iterator, like the C point BidirectionalIterator last ) { if(first == last) return false; // no element BidirectionalIterator i = first; if(++i == last) return false; // only one element i = last; --i; // do not use i--, why? for(;;) { // no statemnet loop, why do not use line 29 ? BidirectionalIterator j = i; // do not use j=i--; why? --i; // find the last neighbor pair (i,j) which element i < j if(*i < *j) { BidirectionalIterator k = last; while(!(*i < *--k)); // find last k >= i iter_swap(i, k); // swap i and k reverse(j, last); // reverse [j,last) return true; } if(i == first) { reverse(first, last); // current is in descending order return false; } } }
STL中首先判斷是否爲空,若是爲空則直接返回false,由於沒有下一個全排列。是否能夠跟第11行調換呢?顯然不行。那麼是否能夠跟第10行調換呢?雖然這樣並不影響運行結果,可是對於爲空的狀況,多了對象的實例化(構造)和清理(析構)兩個過程。可見STL對高效的熾熱追求。
緊接着,第14行使用「--i;」而不是「i--;」,簡言之,前者是先自減再使用,後者是先使用再自減。在這裏雖然對結果也不影響,可是這兩種實現方法仍是有區別的。對於「i--;」來講,編譯器首先會將i的值拷貝到臨時變量中,而後對i進行自減,最後將臨時變量返回;對於「--i」來講,編譯器直接將i的值自減,而後將i的值返回。顯然,「--i」只執行了兩個指令操做,而「i--」執行了三個指令操做。因此能用「--i」的時候儘可能不要使用「i--」。(PS:目前編譯器已經十分智能了,對於上面的狀況,即使寫成「i--」仍然會按照「--i」進行編譯,但請記住,不要期望任何版本的編譯器都能幫你優化代碼!)
注意:第1七、18兩句,並無合併成一句,由於此時編譯器沒法進行合理優化,因此寫成兩句要比寫成一句的少了一個指令操做。具體以下:
// C source 1 | 2 int main(){ |int main(){ int i=0; | int i=0; int j=i--; | int j=i; | --i; return 0; | return 0; } |} // assembly without optimization | _main: 1 |_main: 2 pushl %ebp | pushl %ebp movl %esp, %ebp | movl %esp, %ebp andl $-16, %esp | andl $-16, %esp subl $16, %esp | subl $16, %esp call ___main | call ___main movl $0, 12(%esp) | movl $0, 12(%esp) movl 12(%esp), %eax | movl 12(%esp), %eax leal -1(%eax), %edx | movl %edx, 12(%esp) | movl %eax, 8(%esp) movl %eax, 8(%esp) | subl $1, 12(%esp) movl $0, %eax | movl $0, %eax leave | leave ret | ret .ident "GCC: (GNU) 4.8.3" | .ident "GCC: (GNU) 4.8.3"
所以,不要期望任何版本的編譯器都能幫你優化代碼!
而後看第16行的for語句,爲何不用while語句?從語法上講,「while(1)」與「for(;;)」是相同的,都是死循環。可是後者是一個無條件跳轉,即不須要條件判斷直接循環;而前者多了條件判斷,雖然這個條件判斷永遠爲真,可是多了一個機器指令操做。(PS:目前編譯器已經十分智能,對於這兩種寫法編譯結果都是無條件跳轉,並不須要額外的條件判斷,仍是那句話,不要期望任何版本的編譯器都能幫你優化代碼!)
儘管如此,第28行仍然須要條件判斷,何不寫在for中?拋開無條件跳轉的優點以外,這樣寫有什麼不一樣?仔細分析可知,若是循環到5次時,找到了知足條件的連續元素對(i,j),那麼第28行的條件判斷只執行了4次;若是將28行條件判斷寫在for中,則須要5次條件判斷。因而可知,STL源碼對健壯、高效和精妙的卓越追求!
此外,STL一樣提供了帶比較器的next_permutation:
template <class BidirectionalIterator, class BinaryPredicate> bool next_permutation( BidirectionalIterator _First, BidirectionalIterator _Last, BinaryPredicate _Comp );
這裏再也不進行分析。
注:本文涉及的源碼:permutation : https://git.oschina.net/eudiwffe/codingstudy/tree/master/src/permutation/permutation.c
STL permutation : https://git.oschina.net/eudiwffe/codingstudy/tree/master/src/permutation/permutation_stl.cpp