迷人的算法-排列組合

需求


最近工做中碰到一個需求:咱們的數據表有多個維度,任意多個維度組合後進行 group by 可能會產生一些」奇妙」的反應,因爲不肯定怎麼組合,就須要將全部的組合都列出來進行嘗試。html

抽象一下就是從一個集合中取出任意元素,造成惟一的組合。如 [a,b,c] 可組合爲 [a]、[b]、[c]、[ab]、[bc]、[ac]、[abc]java

要求以下:git

  • 組合內的元素數大於 0 小於等於 數組大小;
  • 組合內不能有重複元素,如 [aab] 是不符合要求的組合;
  • 組合內元素的位置隨意,即 [ab] 和 [ba] 視爲同一種組合;

看到這裏,就應該想到高中所學習的排列組合了,一樣是從集合中取出元素造成一個另外一個集合,若是集合內元素位置隨意,就是組合,從 b 個元素中取 a 個元素的組合有 種。而若是要求元素順序不一樣也視爲不一樣集合的話,就是排列,從 m 個元素取 n 個元素的排列有 種。github

我遇到的這個需求就是典型的組合,用公式來表示就是從元素個數爲 n 的集合中列出 種組合。算法

轉載隨意,文章會持續修訂,請註明來源地址:https://zhenbianshu.github.io 。數組

文中算法用 Java 實現。ide

從排列到組合-窮舉


對於這種需求,首先想到的固然是窮舉。因爲排列的要求較少,實現更簡單一些,若是我先找出全部排列,再剔除因爲位置不一樣而重複的元素,便可實現需求。假設須要從 [A B C D E] 五個元素中取出全部組合,那麼咱們先找出全部元素的全排列,而後再將相似 [A B] 和 [B A] 兩種集合去重便可。學習

咱們又知道 ,那麼咱們先考慮一種狀況 ,假設是 ,從 5 個元素中選出三個進行全排列。fetch

被選取的三個元素,每個均可以是 ABCDE 之一,而後再排除掉造成的集合中有重複元素的,就是 5 選 3 的全排列了。編碼

代碼是這樣:

private static Set<Set<String>> exhaustion() { List<String> m = Arrays.asList("a", "b", "c", "d", "e"); Set<Set<String>> result = new HashSet<>(); int count = 3; for (int a = 1; a < m.size(); a++) { for (int b = 0; b < m.size(); b++) { for (int c = 0; c < m.size(); c++) { Set<String> tempCollection = new HashSet<>(); tempCollection.add(m.get(a)); tempCollection.add(m.get(b)); tempCollection.add(m.get(c)); // 若是三個元素中有重複的會被 Set 排重,致使 Set 的大小不爲 3 if (tempCollection.size() == count) { result.add(tempCollection); } } } } return result; } 

對於結果組合的排重,我借用了 Java 中 HashSet 的兩個特性:

  • 元素惟一性,選取三個元素放到 Set 內,重複的會被過濾掉,那麼就能夠經過集合的大小來判斷是否有重複元素了,
  • 元素無序性,Set[A B] 和 Set[B A] 都會被表示成 Set[A B]。
  • 另外又因爲元素惟一性,被同時表示爲 Set[A B] 的多個集合只會保留一個,這樣就能夠幫助將全排列轉爲組合。

能夠注意獲得,上面程序中 count 參數是寫死的,若是須要取出 4 個元素的話就須要四層循環嵌套了,若是取的元素個取是可變的話,普通的編碼方式就不適合了。

注: 可變層數的循環能夠用 遞歸 來實現。

從排列到組合-分治


窮舉畢竟太過暴力,咱們來經過分治思想來從新考慮一下這個問題:

分治思想

分治的思想總的來講就是」大事化小,小事化了」,它將複雜的問題往簡單劃分,直到劃分爲可直接解決的問題,再從這個直接能夠解決的問題向上聚合,最後解決問題。

從 M 個元素中取出 N 個元素整個問題很複雜,用分治思想就能夠理解爲:

  • 首先,若是咱們已經從 M 中元素取出了一個元素,那麼集合中還剩下 M-1 個,須要取的元素就剩下 N-1 個。
  • 還很差解決的話,咱們假設又從 M-1 中取出了一個元素,集合中還剩下 M-2 個,須要取的元素只剩下 N-2 個。
  • 直到咱們可能取了有 M-N+1 次,須要取的元素只剩下一個了,再從剩餘集合中取,就是一個簡單問題了,很簡單,取法有 M-N+1 種。
  • 若是咱們解決了這個問題,已經取完最後一次了產生了 M-N+1 種臨時集合,再考慮從 M-N+2 個元素中取一個元素呢,又有 M-N+2 種可能。
  • 將這些可能聚合到一塊,直到取到了 N 個元素,這個問題也就解決了。

仍是從 5 個元素中取 3 個元素的示例:

  • 從 5 個元素中取 3 個元素是一個複雜問題,爲了簡化它,咱們認爲已經取出了一個元素,還要再從剩餘的 4 個元素中取出 2 個,求解公式爲:。
  • 從 4 個元素中取出 2 個依舊不易解決,那咱們再假設又取出了一個元素,接下來的問題是如何從 3 個元素中取一個,公式爲 。
  • 從 3 個元素中取 1 個已是個簡單問題了,有三種可能,再向上追溯,與四取1、五取一的可能性作乘,從而解決這個問題。

代碼實現

用代碼實現以下:

public class Combination { public static void main(String[] args) { List<String> m = Arrays.asList("a", "b", "c", "d", "e"); int n = 5; Set<Set<String>> combinationAll = new HashSet<>(); // 先將問題分解成 五取1、五取二... 等的全排列 for (int c = 1; c <= n; c++) { combinationAll.addAll(combination(m, new ArrayList<>(), c)); } System.out.println(combinationAll); } private static Set<Set<String>> combination(List<String> remainEle, List<String> tempCollection, int fetchCount) { if (fetchCount == 1) { Set<Set<String>> eligibleCollections = new HashSet<>(); // 在只差一個元素的狀況下,遍歷剩餘元素爲每一個臨時集合生成多個知足條件的集合 for (String ele : remainEle) { Set<String> collection = new HashSet<>(tempCollection); collection.add(ele); eligibleCollections.add(collection); } return eligibleCollections; } fetchCount--; Set<Set<String>> result = new HashSet<>(); // 差多個元素時,從剩餘元素中取出一個,產生多個臨時集合,還須要取 count-- 個元素。 for (int i = 0; i < remainEle.size(); i++) { List<String> collection = new ArrayList<>(tempCollection); List<String> tempRemain = new ArrayList<>(remainEle); collection.add(tempRemain.remove(i)); result.addAll(combination(tempRemain, collection, fetchCount)); } return result; } } 

其實現就是遞歸,關於遞歸和分治,有興趣能夠看一下隱藏篇: 遞歸和分治

直擊本質-位運算


從元素的全排列找全組合,比窮舉略好,但還不是最好的方法,畢竟它」繞了一次道」。

不少算法都能經過位運算巧秒地解決,其優點主要有兩點:一者位運算在計算機中執行效率超高,再者因爲位運算語義簡單,算法大多直指本質。

組合算法也能經過位運算實現。

思想

再次考慮全組合的需求,從 M 個元素中取任意個元素造成組合,組合內元素不能重複、元素位置無關。

以前的方法都是從結果組合是否知足要求來考慮問題,考慮組合是否有重複元素、是否已有一樣的組合等條件。若是換種思路,從待選元素上來考慮呢?

對於每一個元素來講,它的狀態就簡單得多了,要麼被放進組合,要麼不放進組合。每一個元素都有這麼兩種狀態。若是從 5 個元素中任意取 N 個元素造成組合的話,用二進制位來表示每一個元素是否被放到組合裏,就是:

A  B  C  D  E
0  0  0  0  1   [E] = 1

A  B  C  D  E
0  0  0  1  0   [D] = 2

A  B  C  D  E
0  0  0  1  1   [DE] = 3
...

看到這裏,應該就很是清楚了吧,每種組合均可以拆解爲 N 個二進制位的表達形式,而每一個二進制組合同時表明着一個十進制數字,因此每一個十進制數字都就能表明着一種組合。

十進制數字的數目咱們很簡單就能算出來,從00000... 到 11111... 一共有 種,排除掉全都不被放進組合這種可能,結果有種。

代碼實現

下面是 Java 代碼的實現:

public class Combination { public static void main(String[] args) { String[] m = {"A", "B", "C", "D", "E"}; Set<Set<String>> combinationAll = combination(m); System.out.println(combinationAll); } private static Set<Set<String>> combination(String[] m) { Set<Set<String>> result = new HashSet<>(); for (int i = 1; i < Math.pow(2, m.length) - 1; i++) { Set<String> eligibleCollections = new HashSet<>(); // 依次將數字 i 與 2^n 按位與,判斷第 n 位是否爲 1 for (int j = 0; j < m.length; j++) { if ((i & (int) Math.pow(2, j)) == Math.pow(2, j)) { eligibleCollections.add(m[j]); } } result.add(eligibleCollections); } return result; } } 

小結


排列和組合算法在實際應用中很常見,並且他們的實現方法也很是具備參考意義。總的來講:排列用遞歸、組合用位運算。

關於本文有什麼疑問能夠在下面留言交流,若是您以爲本文對您有幫助,歡迎關注個人 微博 或 GitHub 。您也能夠在個人 博客REPO 右上角點擊 Watch 並選擇 Releases only 項來 訂閱 個人博客,有新文章發佈會第一時間通知您。

相關文章
相關標籤/搜索