LeetCode47. 全排列 II
阿新 • • 發佈:2020-12-27
思路1:回溯搜尋全排列,使用Set暴力去重。
☆☆☆思路2:回溯搜尋 + 剪枝。
對原陣列排序,保證相同的數字都相鄰,然後每次填入的數一定是這個數所在重複數集合中「從左往右第一個未被填過的數字」
程式碼1:回溯搜尋 + Set去重
程式碼1.1 ——交換位置確定數字(耗時:30ms)
class Solution { public List<List<Integer>> permuteUnique(int[] nums) { List<List<Integer>> res = new ArrayList<>();if (nums == null || nums.length == 0) return res; Set<List<Integer>> set = new HashSet<>(); dfs(nums, 0, set); res.addAll(set); return res; } private void dfs(int[] nums, int index, Set<List<Integer>> set) { if (index == nums.length) { List<Integer> list = new ArrayList<>(); for (int num : nums) { list.add(num); } set.add(list); } for (int i = index; i < nums.length; i++) { swap(nums, index, i); dfs(nums, index + 1, set); swap(nums, index, i); } }private void swap(int[] nums, int a, int b) { int temp = nums[a]; nums[a] = nums[b]; nums[b] = temp; } }
程式碼1.2 ——設定標記陣列(耗時:40ms)
class Solution { public List<List<Integer>> permuteUnique(int[] nums) { Set<List<Integer>> set = new HashSet<>(); boolean[] visited = new boolean[nums.length]; dfs(nums, visited, new ArrayList<>(), set); List<List<Integer>> res = new ArrayList<>(set); return res; } private void dfs(int[] nums, boolean[] visited, List<Integer> list, Set<List<Integer>> set) { if (list.size() == nums.length) { set.add(new ArrayList<>(list)); return; } for (int i = 0; i < nums.length; i++) { if (visited[i]) continue; visited[i] = true; list.add(nums[i]); dfs(nums, visited, list, set); visited[i] = false; list.remove(list.size() - 1); // 注意remove傳入的引數是index } } }
程式碼2:回溯搜尋 + 剪枝
程式碼2.1——交換位置確定數字(耗時:1ms)
class Solution { public List<List<Integer>> permuteUnique(int[] nums) { List<List<Integer>> res = new ArrayList<>(); dfs(nums, 0, res); return res; } private void dfs(int[] nums, int index, List<List<Integer>> res) { if (index == nums.length) { List<Integer> list = new ArrayList<>(); for (int num : nums) { list.add(num); } res.add(list); return; } for (int i = index; i < nums.length; i++) { // 搜尋前 先判斷是否已經被選過 if (canSwap(nums, index, i)) { swap(nums, index, i); dfs(nums, index + 1, res); swap(nums, index, i); } } } /** * 如果當前準備選的下標是cur,而在index至cur-1中出現過相同的數字, * 說明數字肯定已經選過了。 */ private boolean canSwap(int[] nums, int start, int end) { for (int k = start; k < end; k++) { if (nums[k] == nums[end]) { return false; } } return true; } private void swap(int[] nums, int a, int b) { int tmp = nums[a]; nums[a] = nums[b]; nums[b] = tmp; } }
程式碼2.2 ——設定標記陣列(耗時:1ms)
class Solution { public List<List<Integer>> permuteUnique(int[] nums) { List<List<Integer>> res = new ArrayList<>(); boolean[] visited = new boolean[nums.length]; Arrays.sort(nums); // 排序保證相同數字都相鄰 dfs(nums, 0, visited, new ArrayList<>(), res); return res; } private void dfs(int[] nums, int index, boolean[] visited, List<Integer> list, List<List<Integer>> res) { if (index == nums.length) { res.add(new ArrayList<>(list)); return; } for (int i = 0; i < nums.length; i++) { if (visited[i]) continue; // 排序保證了相同數字都相鄰,每次填入的數是這個數所在重複數集合中「從左往右第一個未被填過的數字」 // 對 !visited[i-1] 的理解很關鍵 if (i > 0 && nums[i] == nums[i - 1] && !visited[i-1]) continue; visited[i] = true; list.add(nums[i]); dfs(nums, index + 1, visited, list, res); visited[i] = false; list.remove(list.size() - 1); } } }
難點: !vis[i - 1] (前一個元素還未使用過)的理解
如果前一個相同元素未被使用過,則不使用當前元素。那麼每次填入的數一定是這個數所在重複集合中最左邊那個。
當前值等於前一個值,有兩種情況:
1. nums[i-1] 沒用過,為false。 說明回溯到了同一層,此時接著用nums[i]會與nums[i-1]重複。
2. nums[i-1] 用過了,為true。 說明此時在nums[i-1]的下一層,相等不會重複。
用 !vis[i-1]是判斷填入perm同一個位置時,這個數是否被使用過,如果是false代表填入過(因為回溯時被撤銷標記了)
雖然使用vis[i-1]也能AC,但!vis[i-1]更高效。