Java組合算法(m個n選1)

一、模型:

①    現有8個小球,對小球進行編號,依次爲a、b、c、……、g、h。

②    將編號後的8個小球分成三組,分組情況如下:

  ■    第一組:[a, b, c]

  ■    第二組:[d, e]

  ■    第三組:[f, g, h]

③    從每組中選出一個小球,對選出的三個小球進行組合

問題:問一個有多少種不重複的組合方式,並列出詳細的組合方式。

以上是一個典型的數學組合問題,因爲是從每組中選出一個小球,所以每組的選法就有組元素個數種選法,所以組合種數應爲18=3×2×3。具體的組合如下:

01: a d f
02: a d g
03: a d h
04: a e f
05: a e g
06: a e h
07: b d f
08: b d g
09: b d h
10: b e f
11: b e g
12: b e h
13: c d f
14: c d g
15: c d h
16: c e f
17: c e g
18: c e h

上面是純數學、純人工組合出來的,效率太低下了。如果使用Java語言進行編程,打印出這18組組合結果,又該如何實現呢?

二、循環迭代式的組合

可能很多程序員立馬會想到,這個簡單,不就三個數字(或List)嗎,三個嵌套循環不就出來了!那麼就來看看具體的實現。

@Test
public void testCompositeUseIteration() {
	List<String> listA = new ArrayList<String>();
	listA.add("a");
	listA.add("b");
	listA.add("c");
		
	List<String> listB = new ArrayList<String>();
	listB.add("d");
	listB.add("e");
		
	List<String> listC = new ArrayList<String>();
	listC.add("f");
	listC.add("g");
	listC.add("h");
		
	int index = 0;
	for (String itemA : listA) {
		for (String itemB : listB) {
			for (String itemC : listC) {
				index++;
				String str = index + ": \t" + itemA + " " + itemB + " " + itemC;
				System.out.println(str);
			}
		}
	}
}

上面這段代碼可以正確的打印出18種不重複的組合方式。

這種方法解決簡單的m個n選1是沒有任何問題的,但在實際應用中,m值並不是一直是3(m值即嵌套for循環的個數),有可能會更大,甚至m值會經常變化,比如m=10或m=20,難道就要寫10個或20個for嵌套循環嗎?顯然,for嵌套循環方法肯定不能滿足實現應用的需求,更爲致命的是,當m值發生變化時,必須要修改代碼,然後重新編譯、發佈,針對已經上線的生產系統,這也是不允許的。

三、可變組數的高級迭代組合

再來分析下前面的18組組合結果,其實是有規律可循的。

首先是要算出總的組合種數,這個很容易;然後按照從左到右、不重複的組合原則,就會得到一個元素迭代更換頻率,這個數很重要,從左至右,每組的迭代更換頻率是不一樣的,但同組裏的每個元素的迭代更換頻率是一樣的。

說實話,用文字來描述這個規律還真是有些困難,我在紙上畫了畫,就看圖來領會吧!

找到了規律,那麼寫代碼就不是問題了,具體實現如下(有興趣的朋友可以將關鍵代碼封裝成方法,傳入一個List<List<E>>的參數即可返回組合結果):

/**
* 組合記號輔助類
* @author xht555
 * @Create 2015-1-29 17:14:12
 */
private class Sign {
	/**
	 * 每組元素更換頻率,即迭代多少次換下一個元素 */
	public int whenChg;
	/**
	 * 每組元素的元素索引位置 */
	public int index;
}

@Test
public void testComposite(){
	List<String> listA = new ArrayList<String>();
	listA.add("a");
	listA.add("b");
	listA.add("c");
	
	List<String> listB = new ArrayList<String>();
	listB.add("d");
	listB.add("e");
	
	List<String> listC = new ArrayList<String>();
	listC.add("f");
	listC.add("g");
	listC.add("h");
	
	// 這個list可以任意擴展多個
	List<List<String>> list = new ArrayList<List<String>>();
	list.add(listA);	// 3
	list.add(listB);	// 2
	list.add(listC);	// 3
	//list.add(listD);
	//list.add(listE);
	//list.add(listF);
	
	int iterateSize = 1;// 總迭代次數,即組合總種數
	for (int i = 0; i < list.size(); i++) {
		// 每個List的n選1選法種數
		// 有興趣的話可以擴展n選2,n選3,... n選x
		iterateSize *= list.get(i).size();
	}
	
	int median = 1;	// 當前元素與左邊已定元素的組合種數
	Map<Integer, Sign> indexMap = new HashMap<Integer, Sign>();
	for (int i = 0; i < list.size(); i++) {
		median *= list.get(i).size();
		Sign sign = new Sign();
		sign.index = 0;
		sign.whenChg = iterateSize/median;
		indexMap.put(i, sign);
	}
	
	System.out.println("條目總數: " + iterateSize);
	Set<String> sets = new HashSet<String>();
	
	int i = 1;	// 組合編號
		
	long t1 = System.currentTimeMillis();
	while (i <= iterateSize) {
		String s = "i: " + i + "\t";
		
		// m值可變
		for (int m = 0; m < list.size(); m++) {
			int whenChg = indexMap.get(m).whenChg; 	// 組元素更換頻率
			int index = indexMap.get(m).index;		// 組元素索引位置

			s += list.get(m).get(index) + "[" + m + "," + index + "]" + " ";
			
			if (i%whenChg == 0) {
				index++;
				// 該組中的元素組合完了,按照元素索引順序重新取出再組合
				if (index >= list.get(m).size()) {
					index = 0;
				}
					
				indexMap.get(m).index = index;
			}
		}
			
		System.out.println(s);
		sets.add(s);
		i++;
	}
	
	System.out.println("Set條目總數: " + sets.size());
	long t2 = System.currentTimeMillis();
	System.err.println(String.format("%s ms", t2 - t1));
}

運行結果如下:

條目總數: 18
i: 1	a[0,0] d[1,0] f[2,0] 
i: 2	a[0,0] d[1,0] g[2,1] 
i: 3	a[0,0] d[1,0] h[2,2] 
i: 4	a[0,0] e[1,1] f[2,0] 
i: 5	a[0,0] e[1,1] g[2,1] 
i: 6	a[0,0] e[1,1] h[2,2] 
i: 7	b[0,1] d[1,0] f[2,0] 
i: 8	b[0,1] d[1,0] g[2,1] 
i: 9	b[0,1] d[1,0] h[2,2] 
i: 10	b[0,1] e[1,1] f[2,0] 
i: 11	b[0,1] e[1,1] g[2,1] 
i: 12	b[0,1] e[1,1] h[2,2] 
i: 13	c[0,2] d[1,0] f[2,0] 
i: 14	c[0,2] d[1,0] g[2,1] 
i: 15	c[0,2] d[1,0] h[2,2] 
i: 16	c[0,2] e[1,1] f[2,0] 
i: 17	c[0,2] e[1,1] g[2,1] 
i: 18	c[0,2] e[1,1] h[2,2] 
Set條目總數: 18
3 ms

四、興趣擴展

有興趣的朋友可以做下述嘗試:

① m個n選x的組合實現;

② m個n選1的排列實現(先組後排);

排列會關注元素所在的位置(順序),例如,三個元素“a d f”的排列大概如下:

■    a d f

■    a f d

■    d a f

■    d f a

■    f a d

■    f d a

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章