劍指leetcode— (待更新)回溯法全排列

題目描述:給定一個 沒有重複 數字的序列,返回其所有可能的全排列。

示例:
輸入: [1,2,3]
輸出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

解法一:
回溯法:一種通過探索所有可能的候選解來找出所有的解的算法。如果候選解被確認不是一個解的話(或者至少不是最後一個解),回溯算法會通過在上一步進行一些變化拋棄該解,即回溯並且再次嘗試。

搜索回溯
可以先參考下面這篇(扒一扒回溯的褲子,寫的很清晰)
直接上代碼

class Solution {
    List<List<Integer>> res = new LinkedList<>();
/* 主函數,輸入一組不重複的數字,返回它們的全排列 */
List<List<Integer>> permute(int[] nums) {
    // 記錄「路徑」
    LinkedList<Integer> track = new LinkedList<>();
    backtrack(nums, track);
    return res;
}
// 路徑:記錄在 track 中
// 選擇列表:nums 中不存在於 track 的那些元素
// 結束條件:nums 中的元素全都在 track 中出現
void backtrack(int[] nums, LinkedList<Integer> track) {
    // 觸發結束條件
    if (track.size() == nums.length) {
        res.add(new LinkedList(track));
        return;
    }
    for (int i = 0; i < nums.length; i++) {
        // 排除不合法的選擇
        if (track.contains(nums[i]))
            continue;
        // 做選擇
        track.add(nums[i]);
        // 進入下一層決策樹
        backtrack(nums, track);
        // 取消選擇
        track.removeLast();
    }
}
}

這一代碼的缺點在於track.contains(nums[i]),需要使用O(n)時間複雜度,所以可以使用其他辦法解決。
這裏可以通過使用一個比爾數組來確定nums中的數是否已經加入路徑了。
具體實現如下`

class Solution {
    List<List<Integer>> res = new LinkedList<>();
/* 主函數,輸入一組不重複的數字,返回它們的全排列 */
List<List<Integer>> permute(int[] nums) {
    // 記錄「路徑」
    LinkedList<Integer> track = new LinkedList<>();
    **int len = nums.length;
    **boolean[] used = new boolean[len];
    **backtrack(nums, track,used);
    return res;
}
// 路徑:記錄在 track 中
// 選擇列表:nums 中不存在於 track 的那些元素
// 結束條件:nums 中的元素全都在 track 中出現
void backtrack(int[] nums, LinkedList<Integer> track,boolean [] used) {
    // 觸發結束條件
    if (track.size() == nums.length) {
        res.add(new LinkedList(track));
        return;
    }
    for (int i = 0; i < nums.length; i++) {
        // 排除不合法的選擇
        **if (!used[i]){
        **// 做選擇
        track.add(nums[i]); 
        **used[i]=true;
        // 進入下一層決策樹
        **backtrack(nums, track,used);
       ** used[i]=false;
         // 取消選擇
        track.removeLast();
        }
    }
}
}

文中加了星號就是需要修改的地方,這樣改過後,時間效率提高了一半。

上面的做法中要麼是使用了時間複雜度爲O(n)的contains(nums[i]),要麼就是使用了,used標記數組來處理已經填過的數字,但是標記數組也是增加了我們算法的空間複雜度。其實也是可以不使用標記數組的。
以下解法來自leetcode官方題解

我們可以將題目給定的 n個數的數組 nums[] 劃分成左右兩個部分,左邊的表示已經填過的數,右邊表示待填的數,我們在遞歸搜索的時候只要動態維護這個數組即可。

具體來說,假設我們已經填到第 first個位置,那麼 nums[] 數組中 [0,first−1]是已填過的數的集合,[first,n−1]是待填的數的集合。我們肯定是嘗試用 [first,n−1]裏的數去填第 first個數,假設待填的數的下標爲 i ,那麼填完以後我們將第 i個數和第 first個數交換,即能使得在填第 first+1個數的時候 nums[]數組的[0,first]部分爲已填過的數,[first+1,n−1]爲待填的數,回溯的時候交換回來即能完成撤銷操作。

但是這樣有一個缺點,形成的全排列並不是按照字典序排列的,如果題目要求是按照全排列輸出,最好還是使用標記數組和其他辦法

java代碼實現

class Solution{
	public void backtrack(int n,ArrayList<Integer> output,List<List<Integer>> res,int first){
	if(first==n)
	res.add(new ArrayList<Integer>(output));
	for(int i=first;i<n;i++){
	Collections.swap(output,first,i);
	backtrack(n,output,res,first+1);
	Collections.swap(output,first,i);
	}
}
public List<List<Integer>> permute(int[] nums) {
    List<List<Integer>> res = new LinkedList();
    ArrayList<Integer> output = new ArrayList<Integer>();
    for (int num : nums)
      output.add(num);
    int n = nums.length;
    backtrack(n, output, res, 0);
    return res;
  }
}

方法二:
交換的思想
這裏建議先參考下一個排列
思路大致如下:根據給定的初始值,依次按照字典序順序推理出下一個排列,直到無法得到下一個排列爲止。

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        Arrays.sort(nums);//因爲需要得到的是所有的排列,所以需要先對數組進行排序
        List<List<Integer>> res= new ArrayList<>();
        List <Integer> result=new ArrayList<>();
                for(int i=0;i<nums.length;i++)
                result.add(nums[i]);
                res.add(result);
        while(hasnext(nums))  //判斷是否還可以產生下一個排列,如果可以進入循環
        {
                next(nums);
                result= new ArrayList<>();
                for(int i=0;i<nums.length;i++)
                result.add(nums[i]);
                res.add(result);
        }
        return res;
    }
    void swap (int [] nums,int a,int b)
    {
        int t=nums[a];
        nums[a]=nums[b];
        nums[b]=t;
    }
   boolean hasnext(int  [] nums)
    {
        for(int i=nums.length-2;i>=0;i--)
        {
            if(nums[i]<nums[i+1])//如果數組內存在順序數就說明還存在下一個排列
            return true;
        }
        return false;
    }
    void next(int nums[])
    {
        int i=nums.length-2;
        while(i>=0&&nums[i]>=nums[i+1])
        i--;//找到第一個(從右邊數)下標i,使得nums【i】<nums【i+1】
        int j=nums.length-1;
        if(i>=0)
        {
            while(j>=0&&nums[j]<=nums[i])
            j--;   //從右往左,找到第一個比nums【i】大的數多的下標j
        }
         swap(nums,i,j);//交換兩者位置
        reverse(nums,i+1);//翻轉i+1包括自身到結尾數組中得數字
}
     void reverse(int [] nums,int start)//翻轉函數
    {
    int i=start;
    int j=nums.length-1;
    while(i<j)
    {
    swap(nums,i,j);
    i++;
    j--;
    }
        }
 }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章