1. 程式人生 > >C語言實現排列組合

C語言實現排列組合

首先看遞迴實現,由於遞迴將問題逐級分解,因此相對比較容易理解,但是需要消耗大量的棧空間,如果執行緒棧空間不夠,那麼就執行不下去了,而且函式呼叫開銷也比較大。

(1) 全排列:

全排列表示把集合中元素的所有按照一定的順序排列起來,使用P(n, n) = n!表示n個元素全排列的個數。

例如:{1, 2, 3}的全排列為:

123;132;

213;231;

312;321;

共6個,即3!=321=6。

這個是怎麼算出來的呢?

首先取一個元素,例如取出了1,那麼就還剩下{2, 3}。

然後再從剩下的集合中取出一個元素,例如取出2,那麼還剩下{3}。

以此類推,把所有可能的情況取一遍,就是全排列了,如圖:

排列組合演算法

知道了這個過程,演算法也就寫出來了:

將陣列看為一個集合,將集合分為兩部分:0~s和s~e,其中0~s表示已經選出來的元素,而s~e表示還沒有選擇的元素。

perm(set, s, e){順序從s~e中選出一個元素與s交換(即選出一個元素)呼叫perm(set, s +1, e)直到s>e,即剩餘集合已經為空了,輸出set}

c語言程式碼如下:

void perm(int list[],int s,int e,void(*cbk)(int list[])){int i;if(s > e){(*cbk)(list);}else{for(i = s; i <= e; i++)
{ swap(list, s, i); perm(list, s +1, e, cbk); swap(list, s, i);}}}

其中:

void swap(int* o,int i,int j){int tmp = o[i];
    o[i]= o[j];
    o[j]= tmp;}void cbk_print(int* subs){
    printf("{");for(int i =0; i < LEN; i++){
printf("%d", subs[i]);(i == LEN -1)? printf(""): printf(", ");} printf("}\n");}

(2)組合:

組合指從n個不同元素中取出m個元素來合成的一個組,這個組內元素沒有順序。使用C(n, k)表示從n個元素中取出k個元素的取法數。

C(n, k) = n! / (k! * (n-k)!)

例如:從{1,2,3,4}中取出2個元素的組合為:

121314232434

方法是:先從集合中取出一個元素,例如取出1,則剩下{2,3,4}

然後從剩下的集合中取出一個元素,例如取出2

這時12就構成了一個組,如圖。

組合

從上面這個過程可以看出,每一次從集合中選出一個元素,然後對剩餘的集合(n-1)進行一次k-1組合。

comb(set, subset, n, k){反向從集合中選出一個元素,將這個元素放入subset中。呼叫comb(set, subset, n-1, k-1)直到只需要選一個元素為止}

C語言程式碼如下:

void combine(int s[],int n,int k,void(*cbk)(int* subset,int k)){if(k ==0){
        cbk(subset, k);return;}for(int i = n; i >= k; i--){
        subset[k-1]= s[i-1];if(k >1){
            combine(s, i-1, k-1, cbk);}else{
            cbk(subset, subset_length);}}}

任何遞迴演算法都可以轉換為非遞迴演算法,只要使用一個棧模擬函式呼叫過程中對引數的儲存就行了,當然,這樣的方法沒有多少意思,在這裡就不講了。下面要說的是用其它思路實現的非遞迴演算法:

(1)全排列:

首先來看一段程式碼:

#include<iostream>#include<algorithm>usingnamespace std;int main (){int myints[]={1,2,3};

  cout <<"The 3! possible permutations with 3 elements:\n";

  sort (myints,myints+3);do{
    cout << myints[0]<<" "<< myints[1]<<" "<< myints[2]<< endl;}while( next_permutation (myints,myints+3));return0;}

這段程式碼是從STL Permutation上考下來的,要注意的是第10行,首先對陣列進行了排序。

第14行的next_permutation()是STL的函式,它的原理是這樣的:生成當前列表的下一個相鄰的字典序列表,裡面的元素只能交換位置,數值不能改變。

什麼意思呢?

123的下一個字典序是132,因為132比123大,但是又比其他的序列小。

演算法是:

(1) 從右向左,找出第一個比右邊數字小的數字A。

(2) 從右向左,找出第一個比A大的數字B。

(3) 交換A和B。

(4) 將A後面的串(不包括A)反轉。

就完成了。

好,現在按照上面的思路,寫出next_permutation函式:

template<class T>bool next_perm(T * start, T *end){//_asm{int 3}if(start ==end){returnfalse;}else{
        T * pA = NULL,* pB;
        T tmp =*end;// find A.for(T * p =end; p >= start; p--){if(*p < tmp){
                pA = p;break;}else{
                tmp =*p;}}if(pA == NULL){returnfalse;}// find B.for(T * p =end; p >= start; p--){if(*p >*pA){
                pB = p;break;}}// swap A, B.
        tmp =*pA;*pA =*pB;*pB = tmp;// flip sequence after Afor(T *p = pA+1,*q =end; p < q; p++, q--){
            tmp =*p;*p =*q;*q = tmp;}returntrue;}}

(2)組合:01交換法

使用0或1表示集合中的元素是否出現在選出的集合中,因此一個0/1列表即可表示選出哪些元素。

例如:[1 2 3 4 5],選出的元素是[1 2 3]那麼列表就是[1 1 1 0 0]。

演算法是這樣的:

comb(set, n, k){(1)從左到右掃描0/1列表,如果遇到“10”組合,就將它轉換為”01”.(2)將上一步找出的“10”組合前面的所有1全部移到set的最左側。(3)重複(1)(2)直到沒有“10”組合出現。}

程式碼如下:

template<class T>void combine(