帶重複元素以及不帶重複元素的全排列

被一塊石頭絆倒兩次是真的丟人——不寫引子不舒服斯基

遞歸思想

遞歸的思想中有幾個很重要的特性,對於使用遞歸求解的問題,把握好這幾個因素就能把代碼寫好了。
1. 終止條件。遞歸的方法有一個最終的終止條件,這個條件滿足題目所提出的要求,而且不會無限的循環下去。
2. 子問題。問題中的子問題可以再次遞歸調用方法求解。這就是分治思想的運用了。在沒有滿足返回條件之前,每一步和前一步都只是在狀態上有不同。

不帶重複元素的

題目

給定一個數字列表,返回其所有可能的排列。

注意事項

你可以假設沒有重複數字。

樣例

給出一個列表[1,2,3],其全排列爲:

[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

解題思路

不帶重複元素這個題是很早之前做的了。思路也大概就是寫一個遞歸方法,在方法中判斷是否已經全排列,是的話集合加一併返回,不是的話就循環。

很簡單的遞歸全排列思路,甚至可以直接點地記下來。

public class Permute {
    public static List<List<Integer>> permute(int[] nums){
        List<List<Integer>> res = new ArrayList<>();
        ArrayList<Integer> list = new ArrayList<>();

        per(res,list,nums);

        return res;
    }

    public static void per(List res,List list,int[] nums){

        if(list.size() == nums.length){
            res.add(new ArrayList(list));
            return ;
        }

        for(int i = 0;i < nums.length; i++){
            if(list.contains(nums[i]))
                continue;
            list.add(nums[i]);
            per(res,list,nums);
            list.remove(list.size()-1);
        }

    }
}

帶重複元素的

題目

給出一個具有重複數字的列表,找出列表所有不同的排列。

給出列表 [1,2,2],不同的排列有:

[
  [1,2,2],
  [2,1,2],
  [2,2,1]
]

遞歸解法

和上面的解法基本上一樣,只是要多加一個visited數組來記錄這個數據在這一次逐漸深入的遞歸中是否被使用過,遞歸返回時改回未被使用。因爲允許列表元素重複,所以進行一次普通的全排列後,可能會出現兩個相同的排列,爲了避免出現重複的排列,所以要進行一次contains的判斷。

public class PermuteUnique {

    public List<List<Integer>> permuteUnique(int[] nums){
        List<List<Integer>> ret = new ArrayList<>();
        List<Integer> temp = new ArrayList<>();

        int[] visited = new int[nums.length];
        permute(0,nums.length,ret,temp,nums,visited);

        return ret;
    }

    /** * 用來做遞歸的子方法,可以當模版參考 * * @param k 當前長度 * @param n 每個的長度,nums.length * @param ret 返回的那個list * @param temp 每一次遞歸用的list * @param nums 初始數組 * @param visited 元素是否被訪問過 */
    public void permute(int k,int n,List ret,List temp,int[] nums,int[] visited){
        if(k == n && !ret.contains(temp)){
            ret.add(new ArrayList(temp));
        }
        if(k > n){
            return;
        }
        else{
            for(int i = 0;i < n;i++){
                int pre = nums[i];
                if(visited[i] == 0){
                    temp.add(pre);
                    visited[i] = 1;
                    permute(k + 1,n,ret,temp,nums,visited);
                    visited[i] = 0;
                    temp.remove(temp.size() - 1);
                }
            }
        }
    }
}

這一份代碼中還有一些地方是可以優化的,比如那個參數n就是完全多於的東西,按照上一份代碼其實這個參數是可以不用的,包括k也是,明明就是一個list.size的事,但是當時寫代碼的時候本着跑起來再說的想法,沒有考慮這些問題,雖然之後發現了,但是本着提醒自己的想法,就把沒改的版本貼到了這個博客中。

非遞歸解法

這個,偷個懶,以後再說。

指針問題

說到指針,按理說學了這麼幾年已經不應該再在這種事上糊塗了,但是還是犯了錯,做之前全排列的那個題時愣是沒想通,好在今天做這個題的時候反應過來了,所以藉此機會記錄一下。

上面的遞歸代碼中,當排序好的List要放入總的集合中時,之前的我是這樣操作的。

if(k == n && !ret.contains(temp)){
    //stupid
    ret.add(temp);

用這種add,執行後你會發現結果會大錯特錯。而應該換用:

if(k == n && !ret.contains(temp)){
    ret.add(new ArrayList(temp));
}

對,沒錯。

使用上面那個錯誤操作所存進集合中的並不是一個對象,而是指向這個List對象的指針。這就引起了問題:add進集合中的永遠是同一個元素,而且,在帶重複元素這道題中,需要進行一次是否已經contains的判斷,這個會一直爲true,讓你甚至連重複元素都加不進去。寫代碼驗證一下這個想法:

List<List<Integer>> ret = new ArrayList<>();
List<Integer> temp = new ArrayList<>();

temp.add(5);
ret.add(temp);
System.out.println(ret.contains(temp));
temp.add(7);
System.out.println(ret.contains(temp));
ret.add(temp);

Iterator it = ret.iterator();
while(it.hasNext()){
    System.out.println(it.next());
}

執行結果爲:

true
true
[5, 7]
[5, 7]

證明確實如此。所以每次進行add操作時,要new一個新的對象用來存放已經排好序的部分。

整體感想

多看,多寫,多總結。

点赞