排列組合是算法常用的基本工具,如何在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一遍。


