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, 所以請大家指出!