[算法]——全排列(Permutation)以及next_permutation


排列(Arrangement),簡單講是從N個不同元素中取出M個,按照一定順序排成一列,通常用A(M,N)表示。當M=N時,稱為全排列(Permutation)。從數學角度講,全排列的個數A(N,N)=(N)*(N-1)*...*2*1=N!,但從編程角度,如何獲取所有排列?那么就必須按照某種順序逐個獲得下一個排列,通常按照升序順序(字典序)獲得下一個排列。

例如對於一個集合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)

對於給定的任意一種全排列,如果能求出下一個全排列的情況,那么求得所有全排列情況就容易了。好在STL中的algorithm已經給出了一種健壯、高效的方法,下面進行介紹。

設目前有一個集合的一種全排列情況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
 * */

具體方法為:

a)從后向前查找第一個相鄰元素對(i,j),並且滿足A[i] < A[j]。

易知,此時從j到end必然是降序。可以用反證法證明,請自行證明。

b)在[j,end)中尋找一個最小的k使其滿足A[i]<A[k]。

由於[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”進行編譯,但請記住,不要指望任何版本的編譯器都能幫你優化代碼!)

注意:第17、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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM