LeetCode47. 全排列 II
思路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]更高效。