排列-組合的代碼實現


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

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

perm

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

將數組看為一個集合,將集合分為兩部分: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就構成了一個組,如圖。

comb

從上面這個過程可以看出,每一次從集合中選出一個元素,然后對剩余的集合(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一遍。


免責聲明!

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



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