一、模型:
① 現有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