1. 程式人生 > >18 獲取給定的序列的所有排列, 組合

18 獲取給定的序列的所有排列, 組合

前言

本博文部分圖片, 思路來自於劍指offer 或者程式設計珠璣

問題描述

這裡寫圖片描述

這裡寫圖片描述

思路

這裡有兩個問題, 一個是求所有的字元的全排列, 一個是求所有字元的組合

對於問題一, 書上給了一種解法
思路 : 對於一個原始序列, 第一次交換第一個字元 和第一個字元以及後面的字元, 然後遞迴進入交換第二個字元階段[同樣是交換第二個字元 以及之後的字元], 這樣直到交換完所有的字元之後, 打印出序列, 接著返回上一層遞迴, 交換回剛才的交換的兩個字元, 進入下一個交換物件的交換
對於[a, b, c, d], 這樣一個序列來說, 第一個字元存在4種交換可能, 第二個字元存在3種, … 最後共有 “4 * 3 * 2 * 1” 種交換序列

接下來一種思路是我實現的比較傳統的思路 : 將備選字元放在一個容器, 從中抽取一個數據[不放回], 然後進行遞迴, 直到抽出的元素個數達到了我們的需求
同樣 對於第一次抽取可能存在4種可能, 第二次抽取可能存在3種可能, … 最後共有 “4 * 3 * 2 * 1” 種序列

對於問題二

思路 : 我們建立一個抽取規約 : 在抽取了一個元素之後, 我們只能抽取這個元素之後的其他元素[相比於上面傳統的思路 選擇的餘地主要是少了抽取的元素之前的元素]

示範一下 : 比如 “a, b, c”, 假設我們第一次抽取了”b”, 對於問題一的思路, 剩下的備選元素為”a, c”, 然而 對於問題二, 剩下的備選元素為”c”, 因為如果在抽取”b”前面的資料”a”的時候, 可能會造成與第一次抽取”a”的某個組合重疊, so 建立這個規約的目的就是為了防止各個組成元素相同的排列的重複

參考程式碼

/**
 * file name : Test11CharCompletePermutation.java
 * created at : 9:23:24 AM Jun 8, 2015
 * created by 970655147
 */

package com.hx.test05;

public class Test11CharCompletePermutation {

    // 1. 給定一個char[] 要求打印出全排列
    // 2. 打印出所有的組合
    public static void main(String[] args) {

        char
[] chars = new char[] {'a', 'b', 'c', 'd' }; Deque<Character> deque = new LinkedList<Character>(); charCompletePermutation(chars, deque, chars.length); // charCompletePermutation02(chars, 0); Log.horizon(); deque.clear(); charCombination(chars, deque); } // 思路 : 使用一個棧 [或者一個數組 [如果使用陣列的話, 直接在下面push的地方改成響應的索引賦值, 然後去掉pop即可] ]來儲存排列 // 每次從chars中拿走一個字元, 然後把剩下的字元傳入charCompletePermutation 進行遞迴 public static void charCompletePermutation(char[] chars, Deque<Character> deque, int getN) { if(chars == null) { return ; } if(getN == 0 ) { Log.log(deque); } for(int i=0; i<chars.length; i++) { deque.push(chars[i]); charCompletePermutation(copyExcept(chars, i), deque, getN-1); deque.pop(); } } // 交換start 和start之後的資料 // 遞迴交換start+1 和start+1之後的資料 // 第一位有chars.length中可能, 第二位有chars.length-1種可能, ... public static void charCompletePermutation02(char[] chars, int start) { if(start == chars.length) { Log.log(chars); return ; } for(int i=start; i<chars.length; i++) { swap(chars, start, i); charCompletePermutation02(chars, start+1); swap(chars, i, start); } } // 計算字元序列的全組合 // 從getN屬於[1, chars.length] 獲取chars中getN個字元的組合 // 獲取小於(chars.length/2)個數的組合的時候 使用charCombinationForN方法 // 獲取大於(chars.length/2)個數的組合的時候 使用charCombinationForHalfLater方法 public static void charCombination(char[] chars, Deque<Character> deque) { for(int i=1; i<=chars.length/ 2; i++ ) { charCombinationForN(chars, deque, i); } for(int i=chars.length/2+1; i<=chars.length; i++) { charCombinationForHalfLater(chars, chars, deque, chars.length-i); } } // 適用於計算sourceChars中的長度小於(sourceChars.length/2)的組合 // 計算從chars中取出getN個字元的序列 // 從chars中取出一個字元, 然後遞迴在其之後的資料中取出getN-1個字元 private static void charCombinationForN(char[] chars, Deque<Character> deque, int getN) { if(getN > chars.length ) { return ; } // 注意 : 這裡應該是退出條件之一, 使用完序列之後 應該return if(getN == 0) { Log.log(deque); return ; } // .. 最多到(chars.length-getN)開始 int end = chars.length - getN; for(int i=0; i<=end; i++) { deque.push(chars[i]); charCombinationForN(copyAfter(chars, i), deque, getN-1); deque.pop(); } } // 適用於計算sourceChars中的長度大於(sourceChars.length/2)的組合 // 計算從chars中取出(chars.length-compeltementyN)個字元的序列 // 從chars中取出一個字元, 然後遞迴在其之後的資料中取出(compeltementyN-1)個字元 private static void charCombinationForHalfLater(char[] sourceChars, char[] chars, Deque<Character> deque, int compeltementyN) { if(compeltementyN > chars.length ) { return ; } // 注意 : 這裡應該是退出條件之一, 使用完序列之後 應該return if(compeltementyN == 0) { // Log.log(deque); Log.logWithoutLn("["); for(char ch : sourceChars) { if(! deque.contains(ch)) { Log.logWithoutLn(ch + ", "); } } Log.logWithoutLn("]"); Log.enter(); return ; } // .. 最多到(chars.length-getN)開始 int end = chars.length - compeltementyN; for(int i=0; i<=end; i++) { deque.push(chars[i]); charCombinationForHalfLater(sourceChars, copyAfter(chars, i), deque, compeltementyN-1); deque.pop(); } } // 複製除了exceptIdx 之外的其他元素 private static char[] copyExcept(char[] chars, int excpetIdx) { if((excpetIdx < 0) || (excpetIdx >= chars.length) ) { return chars; } char[] newChars = new char[chars.length-1]; System.arraycopy(chars, 0, newChars, 0, excpetIdx); System.arraycopy(chars, excpetIdx+1, newChars, excpetIdx, chars.length-excpetIdx-1); return newChars; } // 注意 : 不包括from // 複製chars中from之後的元素 private static char[] copyAfter(char[] chars, int from) { if((from < 0) || (from >= chars.length) ) { return chars; } char[] newChars = new char[chars.length-from-1]; System.arraycopy(chars, from+1, newChars, 0, newChars.length); return newChars; } // 交換陣列中給定的兩個索引的資料 private static void swap(char[] arr, int idx01, int idx02) { char tmp = arr[idx01]; arr[idx01] = arr[idx02]; arr[idx02] = tmp; } }

效果截圖

這裡寫圖片描述

總結

對於後兩者的思路, 更好是使用一個boolean[] 來標記各個索引對應的資料是否可抽取, 但是 這裡為了易讀, 就這樣copyExcept/ copyAfter了

注 : 因為作者的水平有限,必然可能出現一些bug, 所以請大家指出!