排列組合是算法常用的基本工具,如何在c語言中實現排列組合呢?思路如下:
首先看遞歸實現,由於遞歸將問題逐級分解,因此相對比較容易理解,但是需要消耗大量的棧空間,如果線程棧空間不夠,那么就運行不下去了,而且函數調用開銷也比較大。
(1) 全排列:
全排列表示把集合中元素的所有按照一定的順序排列起來,使用P(n, n) = n!表示n個元素全排列的個數。
例如:{1, 2, 3}的全排列為:
123;132;
213;231;
312;321;
共6個,即3!=3*2*1=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; }
cbk是回調函數,可以寫成:
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個元素的組合為:
12;13;14;
23;24;
34
方法是:先從集合中取出一個元素,例如取出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> using namespace 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) ); return 0; }
這段代碼是從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) { return false; } 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) { return false; } // 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 A for (T *p = pA+1, *q = end; p < q; p++, q--) { tmp = *p; *p = *q; *q = tmp; } return true; } }
(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(T set[], int n, int k, void (*cbk)(T set[])) { unsigned char * vec = new unsigned char[n]; T * subset = new T[k]; // build the 0-1 vector. for(int i = 0; i < n; i++) { if (i < k) vec[i] = 1; else vec[i] = 0; } // begin scan. bool has_next = true; while (has_next) { // get choosen. int j = 0; for (int i = 0; i < n; i++) { if (vec[i] == 1) { subset[j++] = set[i]; } } cbk(subset); has_next = false; for (int i = 0; i < n - 1; i++) { if (vec[i] == 1 && vec[i + 1] == 0) { vec[i] = 0; vec[i + 1] = 1; // move all 1 to left-most side. int count = 0; for (int j = 0; j < i; j++) { if (vec[j] == 1) count ++; } if (count < i) { for (int j = 0; j < count; j++) { vec[j] = 1; } for (int j = count; j < i; j++) { vec[j] = 0; } } has_next = true; break; } } } delete [] vec; delete [] subset; }
至於其中的道理,n個位置上有k個1,按照算法移動,可以保證k個1的位置不重復,且覆蓋n一遍。