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]更高效。

 

posted @ 2020-12-27 20:59  不学无墅_NKer  阅读(102)  评论(0编辑  收藏  举报