July 算法習題 - 字符串4(全排列和全組合)

求字符串的全排列git

字符串的全排列

設計一個算法,輸出一個字符串字符的全排列。
好比,String = "abc"
輸出是"abc","bac","cab","bca","cba","acb"github

算法思想

從集合依次選出每個元素,做爲排列的第一個元素,而後對剩餘的元素進行全排列,如此遞歸處理;算法

好比:首先我要打印abc的全排列,就是第一步把a 和bc交換(獲得bac,cab),這須要一個for循環,循環裏面有一個swap,交換以後就至關於無論第一步了,進入下一步遞歸,因此跟一個遞歸函數, 完成遞歸以後把交換的換回來,變成原來的字串
遞歸方法1(July 方法):數組

abc 爲例子:
1. 固定a, 求後面bc的全排列: abc, acb。 求完後,a 和 b交換; 獲得bac,開始第二輪
2. 固定b, 求後面ac的全排列: bac, bca。 求完後,b 和 c交換; 獲得cab,開始第三輪
3. 固定c, 求後面ba的全排列: cab, cba
 即遞歸樹: 
     str:   a         b         c
         ab ac       ba bc          ca cb
     result:     abc acb      bac bca          cab cba
 

clipboard.png

public static void Permutation(char[] s, int from, int to) {
        if(to<=1)
            return;
        if(from == to){
            System.out.println(s);
        }
        else{
            for(int i=from;i<=to;i++){
                swap(s,i,from);
                Permutation(s,from+1,to);
                swap(s,from,i);
                }
        }
    }

    public static void swap(char[] s, int i, int j) {
        char temp = s[i];
        s[i] = s[j];
        s[j] = temp;
    }

遞歸方法2:
與上面算法區別:
本算法須要一個額外的存儲空間存放結果(buffer),固定第一個位置是哪一個元素的時候,是經過一個循環,而後看原始字符串上,每個位置是什麼元素。July的作法沒有結果的buffer,都是在一個字符串上進行的操做。第一個swap的做用就是,依次拿起始字符和後面的每個字符交換,這樣就能遍歷第一個位置上的全部可能字符app

推薦一個youtube講解的視頻函數

n個數的全排列,一共有n!種狀況. (n個位置,第一個位置有n種,當第一個位置固定下來以後,第二個位置有n-1種狀況...)測試

全排列的過程:spa

  • 選擇第一個字符設計

  • 得到第一個字符固定下來以後的全部的全排列code

    • 選擇第二個字符

    • 得到第一+ 二個字符固定下來以後的全部的全排列

從這個過程可見,這是一個遞歸的過程。

還有一點須要注意是:
以前遞歸過程選擇的字符,下一次不能再被選: 第一個位置選了a, 其餘位置就不能選a了
解決方法是1. 掃描以前選擇的字符 或者 2.建立一個與字符串等長的boolean數組,標記該位置對於的字符是否已經選擇。若選擇,則標記true; 若未選擇,則標記false.

public class Permutation {
    public static void permute(String str){
        int length = str.length();
        boolean[] used = new boolean[length];
        StringBuffer output = new StringBuffer(length);

        permutation(str,length,output,used,0);

    }

    // @para
    // position : 下一個放置的元素位置,因此調入時候是0
    // 
    static void permutation(String str, int length, StringBuffer output, boolean[] used, int position){
        // end of the recursion
        if(position == length){
            System.out.println(output.toString());
            return;
        }
        else{
            for(int i=0;i<length;i++){
                // skip already used characters
                if(used[i])
                    continue;
                // add fixed character to output, and mark it as used
                output.append(str.charAt(i));
                used[i] = true;

                // permute over remaining characters starting at position+1
                // recursion
                permutation(str,length,output,used,position+1);
                // remove fixed character from output and unmark it
                output.deleteCharAt(output.length()-1);
                used[i] = false;
            }
        }
    }

我的認爲這個算法不如第一個遞歸方法,由於須要額外的空間;可是兩者的時間複雜度是相同的,都是O(n!)

字符串的全組合

輸入三個字符 a、b、c,則它們的組合有a b c ab ac bc abc。固然咱們仍是能夠借鑑全排列的思路,利用問題分解的思路,最終用遞歸解決。不過這裏介紹一種比較巧妙的思路 —— 基於位圖。
假設原有元素n個,最終的組合結果有2^n - 1. 可使用2^n - 1個位,1表示取該元素,0表示不取。 因此a表示001,取ab是011。
001,010,011,100,101,110,111。對應輸出組合結果爲:a,b,ab,c,ac,bc,abc
所以能夠循環 1~2^n-1(字符串長度),而後輸出對應表明的組合便可。

public static void Combination(char [] s){
        if(s.length == 0){
            return;
        }
        int len = s.length;
        int n = 1<<len;
        //從1循環到2^len-1
        for(int i=0;i<n;i++){
            StringBuffer sb = new StringBuffer();
            //查看第一層循環裏面的任意一種取值當中的哪一位是1[好比ab,011], 若是是1,對應的字符就存在,打印當前組合。 
            for(int j=0;j<len;j++){
                if( (i & (1<<j)) != 0) // 對應位上爲1,則輸出對應的字符
                {
                    sb.append(s[j]);
                }
            }
            System.out.print(sb + " ");
        }   
    }
for(int j=0;j<len;j++){
        if( (i & (1<<j)) != 0)
        }

j = 0, 1<<j 爲將第一位置1
j = 1, 1<<j 爲將第二位置1
j = 2, 1<<j 爲將第三位置1

有限制的組合

Leetcode

Given two integers n and k, return all possible combinations of k numbers out of 1 ... n.
For example,
If n = 4 and k = 2, a solution is:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

解題思路

基於位操做,這裏咱們主要藉助一個二進制操做 「 求最小的、比 x 大的整數 M,使得 M 與 x 的二進制表示中有相同數目的 1」,若是這個操做已知,那麼咱們能夠設置一個初始整數 bit,bit 的低位第 1~k 個二進制位爲 1,其他二進制位爲 0,bit 的二進制表示一種組合,而後調用上述操做求得下一個 bit,bit 的最大值爲:bit 從低位起第 n-k+1~n 位等於 1,其他位等於 0,即 (1<<n) - (1<<(n-k)

public static List<List<Integer>> combine(int n, int k) {
        if(n == 0 | k>n){
            return null;
        }
        int len = n;
        int nbit = 1<<len;
        int kbit = 1<<k;
        int inbit = 1<<n - 1<<(n-k);
        List<List<Integer>> result = new ArrayList<List<Integer>>();
        //從1循環到2^len-1
        for(int i=kbit-1; i<= inbit; i = nextn(i)){
            List<Integer> list = new ArrayList<Integer>();
            for(int j=0;j<len;j++){
                if( (i & (1<<j)) != 0) // 對應位j上爲1,則輸出對應的字符
                {
                    list.add(j+1);
                }
            }
            result.add(list);       
        }   
        return result;
    }
    // 返回最小的,比N大的整數M,使M與N的二進制有相同數目的1
    public static int nextn(int k){
        int x = k & (-k);
        int t = k+x;
      return t | ((k^t)/x)>>2;
    }

附錄: 位操做

求整數的二進制表示中有多少個 1

方法1

應用了n&=(n-1)能將 n 的二進制表示中的最右邊的 1 翻轉爲 0 的事實。只須要不停地執行 n&=(n-1),直到 n 變成 0 爲止,那麼翻轉的次數就是原來的 n 的二進制表示中 1 的個數,其代碼以下:

public int count1Bits(int n){
        int count = 0;
        while(n!=0){
            count++;
            n = n & (n-1);
        }
        return count;
    }

NextN

給定一個正整數 N,求最小的、比 N 大的正整數 M,使得 M 與 N 的二進制表示中有相同數目的 1

方法1: 簡單枚舉
從 N+1 開始枚舉,對每一個數都測試其二進制表示中的 1 的個數是否與 N 的二進制表示中 1 的個數相等,遇到第一次相等時就中止

public int GetNextN(int n){
        int k = count1Bits(n);
        do{
            n++;
        }while(count1Bits(n) != k);
        return n;
    }

方法2: O(1)時間高效方法
參考

public int NextN(int n){
    int x = n&(-n);
    int t = n + x;
    int ans = t | ((n^t)/x)>>2;
    return ans;
}

想更一進步的支持我,請掃描下方的二維碼,你懂的~
圖片描述

相關文章
相關標籤/搜索