leetcode之回溯算法

一 组合问题
1. leetcode 77.组合

leetcode 77.组合(难度:中等)
题目描述:
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

public class 组合 {
    /**
     * 使用回溯算法:回溯算法其实也是暴力搜索,只不过有些题目想通过多层for循环去暴力搜索都写不出来,就只能使用回溯算法了
     *             例如本题中,如果k=2,那么可以通过两层for循环解决,但是k是一个参数,无法确定大小,再者如果k=50,那么也写不出50层for循环
     *             回溯算法一般用于解决:组合问题、排列问题、切割问题、子集问题、棋盘问题
     *             回溯算法本质也是递归,通常是递归一次,就得回溯一次,而回溯的操作一般也是递归之前的一个逆操作
     *             (比如递归之前往集合添加了一个元素,那么回溯操作就要移除这个元素)
     *             回溯问题都可以抽象成树形结构(N叉树),集合的大小就是树的宽度,递归的深度就是树的深度
     * @param n
     * @param k
     * @return
     */
    public List<List<Integer>> combine(int n, int k) {
        List<Integer> temp = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        backtracking(n, k, 1, temp, res);
        return res;
    }
    /**
     *
     * @param n
     * @param k
     * @param startIndex:代表每次for循环的子集起始下标,组合问题通常都要有这个参数
     * @param temp:保存中间结果,每次回溯时,都应该remove最后一个元素
     * @param res:结果列表
     */
    public void backtracking(int n, int k, int startIndex, List<Integer> temp, List<List<Integer>> res) {
        // 递归结束的条件:当temp集合的长度等于k时,递归终止
        if (temp.size() == k) {
            // 注意:不能直接执行res.add(temp);这行代码,这样会导致最终的结果都是空集合
            // 因为这行代码代表res集合的引用指向temp集合,而temp集合又是不断改变的,所以会导致最终的结果不正确

            List<Integer> list = new ArrayList<>();
            for (int i = 0; i < temp.size(); i++) {
                list.add(temp.get(i));
            }
            res.add(list);
            // 递归出口记得return,否则可能发生死循环
            return;
        }
        for (int i = startIndex; i <= n; i++) {
            // 递归前的操作
            temp.add(i);
            // 递归
            backtracking(n, k, i + 1, temp, res);
            // 回溯操作:递归前的逆操作
            temp.remove(temp.size() - 1);
        }
    }

    /**
     * 改进:对该回溯问题进行剪枝,提高效率(剪枝通常都是在for循环做操作)
     * 如果for循环选择的起始位置之后的元素个数已经不足我们需要的元素个数了,那么就没有必要搜索了
     * @param n
     * @param k
     * @return
     */
    public List<List<Integer>> combine1(int n, int k) {
        LinkedList<Integer> stack = new LinkedList<>();
        List<List<Integer>> res = new ArrayList<>();
        backtracking1(n, k, 1, stack, res);
        return res;
    }
    /**
     *
     * @param n
     * @param k
     * @param startIndex:代表每次for循环的子集起始下标
     * @param stack:保存中间结果,利用栈先进后出的特性,方便回溯
     * @param res:结果列表
     */
    public void backtracking1(int n, int k, int startIndex, LinkedList<Integer> stack, List<List<Integer>> res) {
        // 递归结束的条件
        if (stack.size() == k) {
            List<Integer> list = new ArrayList<>();
            for (int i = 0; i < stack.size(); i++) {
                list.add(0,stack.get(i));
            }
            res.add(list);
            return;
        }
        for (int i = startIndex; i <= n; i++) {
            // 还需要的元素个数
            int x = k - stack.size();
            // for循环选择的起始位置之后的元素个数
            int y = n - startIndex + 1;
            // 剪枝
            if (y < x) {
                break;
            }
            // 递归前的操作
            stack.push(i);
            // 递归
            backtracking1(n, k, i + 1, stack, res);
            // 回溯操作:递归前的逆操作
            stack.pop();
        }
    }
}
2. leetcode 39.组合总和

leetcode 39.组合总和(难度:中等)
题目描述:
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2:
输入:candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
提示:
1 <= candidates.length <= 30
1 <= candidates[i] <= 200
candidate 中的每个元素都是独一无二的。
1 <= target <= 500

public class 组合总和 {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<Integer> list = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        backtracking(candidates, target, 0, 0, list, res);
        return res;
    }
    public void backtracking(int[] candidates, int target, int sum, int startIndex, List<Integer> list, List<List<Integer>> res) {
        // 递归终止的条件:当sum大于等于target的时候递归终止(由于candidates 中的数字可以无限制重复被选取,因此最终sum是不会小于target的)
        if (sum > target) {
            return;
        }
        if (sum == target) {
            List<Integer> temp = new ArrayList<>();
            for (int i = 0; i < list.size(); i++) {
                temp.add(list.get(i));
            }
            res.add(temp);
            return;
        }
        // 遇到这一类相同元素不计算顺序的问题,我们在搜索的时候就需要 按某种顺序搜索。具体的做法是:每一次搜索的时候设置 下一轮搜索的起点 startIndex
        for (int j = startIndex; j < candidates.length; j++) {
            sum += candidates[j];
            list.add(candidates[j]);
            // 注意:由于每一个元素可以重复使用,下一轮搜索的起点依然是j(往常的写法是j+1,这里要特别注意)
            backtracking(candidates, target, sum, j, list, res);
            sum -= candidates[j];
            list.remove(list.size() - 1);
        }
    }

    /**
     * 改进:对candidates数组进行排序,可减少回溯的次数(对组合求和通常都是先对数组进行排序,方便剪枝)
     * @param candidates
     * @param target
     * @return
     */
    public List<List<Integer>> combinationSum1(int[] candidates, int target) {
        List<Integer> list = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(candidates);
        backtracking1(candidates, target, 0, 0, list, res);
        return res;
    }
    public void backtracking1(int[] candidates, int target, int sum, int startIndex, List<Integer> list, List<List<Integer>> res) {
        if (sum == target) {
            List<Integer> temp = new ArrayList<>();
            for (int i = 0; i < list.size(); i++) {
                temp.add(list.get(i));
            }
            res.add(temp);
            return;
        }
        for (int j = startIndex; j < candidates.length; j++) {
            sum += candidates[j];
            // 剪枝:由于数组是从小到大排好序的,因此如果sum加上前面的元素值已经超过了target,那么可以直接退出本层for循环了
            if (sum > target) {
                break;
            }
            list.add(candidates[j]);
            // 注意:由于每一个元素可以重复使用,下一轮搜索的起点依然是j(往常的写法是j+1,这里要特别注意)
            backtracking1(candidates, target, sum, j, list, res);
            sum -= candidates[j];
            list.remove(list.size() - 1);
        }
    }

    /**
     * 方法二:每选一个数,target就减去对应的值,例如要在[1,3,5,7]中找到和为4的集合,那么选了1之后,下一层就要在[1,3,5,7]中找到和为3的集合
     * @param candidates
     * @param target
     * @return
     */
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<Integer> list = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(candidates);
        backtracking2(candidates, target, 0, list, res);
        return res;
    }
    public void backtracking2(int[] candidates, int target, int startIndex, List<Integer> list, List<List<Integer>> res) {
        // 递归终止的条件:当target小于0,直接退出;当target等于0,添加到结果列表中
        if (target < 0) {
            return;
        }
        if (target == 0) {
            List<Integer> temp = new ArrayList<>();
            for (int i = 0; i < list.size(); i++) {
                temp.add(list.get(i));
            }
            res.add(temp);
            return;
        }
        // 遇到这一类相同元素不计算顺序的问题,我们在搜索的时候就需要 按某种顺序搜索。具体的做法是:每一次搜索的时候设置 下一轮搜索的起点 begin
        for (int j = startIndex; j < candidates.length; j++) {
            list.add(candidates[j]);
            // 注意:由于每一个元素可以重复使用,下一轮搜索的起点依然是j(往常的写法是j+1,这里要特别注意)
            backtracking2(candidates, target - candidates[j], j, list, res);
            list.remove(list.size() - 1);
        }
    }
}

3. leetcode 40.组合总和II

注意:组合总和II要和组合总和区分开来:
① 组合总和:candidates数组无重复元素、candidates 中的每个数字在每个组合中可以无限制重复被选取
② 组合总和II:candidates数组有重复元素、candidates 中的每个数字在每个组合中只能使用一次

leetcode 40.组合总和II(难度:中等)
题目描述:
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]

public class 组合总和II {
    /**
     * 不要遗漏了数组中可能出现重复元素的情况,会导致结果出现重复
     * 可以通过一个set集合进行去重,因为list1=[1,2,3],list2=[1,2,3],添加到set集合中,会去除掉一个重复的list
     * @param candidates
     * @param target
     * @return
     */
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<Integer> list = new ArrayList<>();
        Set<List<Integer>> res = new HashSet<>();
        // 对数组进行排序,可以剪枝减少回溯次数
        Arrays.sort(candidates);
        combinationSum2(candidates, target, 0, 0, list, res);
        List<List<Integer>> result = new ArrayList<>();
        Iterator<List<Integer>> iterator = res.iterator();
        while (iterator.hasNext()) {
            result.add(iterator.next());
        }
        return result;
    }
    public void combinationSum2(int[] candidates, int target, int sum, int startIndex, List<Integer> list, Set<List<Integer>> res) {
        if (sum == target) {
            List<Integer> temp = new ArrayList<>();
            for (int i = 0; i < list.size(); i++) {
                temp.add(list.get(i));
            }
            res.add(temp);
        }
        for (int j = startIndex; j < candidates.length; j++) {
            sum += candidates[j];
            // 剪枝
            if (sum > target) {
                break;
            }
            list.add(candidates[j]);
            // 因为candidates 中的每个数字在每个组合中只能使用一次,因此这里j要加一(要和组合总和这道题目区分开)
            combinationSum2(candidates, target, sum, j + 1, list, res);
            sum -= candidates[j];
            list.remove(list.size() - 1);
        }
    }

    /**
     * 改进:不需要使用set集合对结果列表进行去重,直接在遍历的过程中去重
     * 使用set集合去重效率太低了,因为是在求得结果列表之后再进行一遍去重,而我们完全可以在遍历的过程中就去重
     * 因为已经对数组进行排序了,所以如果当前元素值等于上一个元素值就可以直接跳过处理,避免重复
     * @param candidates
     * @param target
     * @return
     */
    public List<List<Integer>> combinationSum2II(int[] candidates, int target) {
        List<Integer> list = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        // 对数组进行排序,可以剪枝减少回溯次数
        Arrays.sort(candidates);
        backtrackingII(candidates, target, 0, 0, list, res);
        return res;
    }
    public void backtrackingII(int[] candidates, int target, int sum, int startIndex, List<Integer> list, List<List<Integer>> res) {
        if (sum == target) {
            List<Integer> temp = new ArrayList<>();
            for (int i = 0; i < list.size(); i++) {
                temp.add(list.get(i));
            }
            res.add(temp);
        }
        for (int j = startIndex; j < candidates.length; j++) {
            if (j > startIndex && candidates[j] == candidates [j - 1]) {
                continue;
            }
            sum += candidates[j];
            // 剪枝
            if (sum > target) {
                break;
            }
            list.add(candidates[j]);
            backtrackingII(candidates, target, sum, j + 1, list, res);
            sum -= candidates[j];
            list.remove(list.size() - 1);
        }
    }
}
4. leetcode 216.组合总和III

leetcode 216.组合总和III(难度:中等)
题目描述:
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]

public class 组合总和III {
    public List<List<Integer>> combinationSum3(int k, int n) {
        List<Integer> list = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        backtracking(k, n, 1, list, res);
        return res;
    }
    public void backtracking(int k, int n, int startIndex, List<Integer> list, List<List<Integer>> res) {
        // 递归终止的条件:list集合中的元素个数等于k时终止,并判断此时list集合中的元素之和是否等于n,是则添加到res集合中
        if (list.size() == k) {
            int sum = 0;
            List<Integer> temp = new ArrayList<>();
            for (int i = 0; i < list.size(); i++) {
                sum += list.get(i);
                temp.add(list.get(i));
            }
            if (sum == n) {
                res.add(temp);
            }
            return;
        }
        for (int j = startIndex; j <= 9; j++) {
            list.add(j);
            backtracking(k, n, j + 1, list, res);
            list.remove(list.size() - 1);
        }
    }

    /**
     * 改进:把sum提取出来,方便剪枝
     * @param k
     * @param n
     * @return
     */
    public List<List<Integer>> combinationSum3II(int k, int n) {
        List<Integer> list = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        backtrackingII(k, n, 1,0, list, res);
        return res;
    }
    public void backtrackingII(int k, int n, int startIndex, int sum, List<Integer> list, List<List<Integer>> res) {
        // 递归终止的条件:list集合中的元素个数等于k时终止,并判断此时list集合中的元素之和是否等于n,是则添加到res集合中
        if (list.size() == k) {
            if (sum == n) {
                List<Integer> temp = new ArrayList<>();
                for (int i = 0; i < list.size(); i++) {
                    temp.add(list.get(i));
                }
                res.add(temp);
            }
        }
        for (int j = startIndex; j <= 9; j++) {
            sum += j;
            // 剪枝:如果总和sum已经大于n了,就直接退出for循环
            if (sum > n) {
                break;
            }
            list.add(j);
            backtrackingII(k, n, j + 1, sum, list, res);
            list.remove(list.size() - 1);
            sum -= j;
        }
    }
}
5. leetcode 377.组合总和IV

leetcode 377.组合总和IV(难度:中等)
题目描述:
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例:
nums = [1, 2, 3]
target = 4
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
因此输出为 7。

注意:本题目虽然可以用回溯法解决,但是在力扣上提交时会超出时间限制,无法通过,原因就是target很大时,会导致结果是一个非常大的数
(之后有更好的解法再补充上来)

public class 组合总和IV {
    int result = 0;
    public int combinationSum4(int[] nums, int target) {
        Arrays.sort(nums);
        backtracking(nums, target, 0);
        return result;
    }
    public void backtracking(int[] nums, int target, int sum) {
        if (sum == target) {
            result++;
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
            if (sum > target) {
                break;
            }
            backtracking(nums, target, sum);
            sum -= nums[i];
        }
    }
}
6. leetcode 17.电话号码的字母组合

leetcode 17.电话号码的字母组合(难度:中等)
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
输入:“23”
输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”].
说明:
尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
在这里插入图片描述

public class 电话号码的字母组合 {
    // 数字i代表的字母对应numbers[i](题目说字符串只包含2-9,因此可以不考虑其他特殊情况)
    String[] numbers = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
    public List<String> letterCombinations(String digits) {
        List<Character> ch = new ArrayList<>();
        List<String> res = new ArrayList<>();
        if (digits == null || digits.length() == 0) {
            return res;
        }
        backtracking(digits, 0, ch, res);
        return res;
    }
    // index用来表示遍历到参数字符串digits的第几个数字(index从0开始)
    public void backtracking(String digits, int index, List<Character> ch, List<String> res) {
        // 递归终止的条件:当字符列表ch的长度等于参数字符串digits的长度
        // 递归出口记得加上return,否则会死循环
        if (ch.size() == digits.length()) {
            String s = "";
            for (int i = 0; i < ch.size(); i++) {
                s += ch.get(i);
            }
            res.add(s);
            return;
        }
        int temp = digits.charAt(index) - '0';
        for (int i = 0; i < numbers[temp].length(); i++) {
            ch.add(numbers[temp].charAt(i));
            backtracking(digits, index + 1, ch, res);
            ch.remove(ch.size() - 1);
        }
    }
}
二 排列问题
1. leetcode 46.全排列

leetcode 46.全排列(难度:中等)
题目描述:
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

public class 全排列 {
    public List<List<Integer>> permute(int[] nums) {
        // 排列问题一般都要设置一个used数组,来判断数组中的元素是否已经被访问过
        boolean[] used = new boolean[nums.length];
        List<Integer> list = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        backtracking(nums, used, list, res);
        return res;
    }
    public void backtracking(int[] nums, boolean[] used, List<Integer> list, List<List<Integer>> res) {
        // 递归终止的条件:list集合的长度等于nums数组的长度
        if (list.size() == nums.length) {
            List<Integer> temp = new ArrayList<>();
            for (int i = 0; i < list.size(); i++) {
                temp.add(list.get(i));
            }
            res.add(temp);
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (used[i] == true) {
                continue;
            }
            used[i] = true;
            list.add(nums[i]);
            backtracking(nums, used, list, res);
            used[i] = false;
            list.remove(list.size() - 1);
        }
    }
}
2. leetcode 47.全排列II

leetcode 47.全排列II(难度:中等)
题目描述:
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
示例 2:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
提示:
1 <= nums.length <= 8
-10 <= nums[i] <= 10

public class 全排列II {
    public List<List<Integer>> permuteUnique(int[] nums) {
        boolean[] used = new boolean[nums.length];
        List<Integer> list = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        // 对数组进行排序,方便去重
        Arrays.sort(nums);
        backtracking(nums, used, list, res);
        return res;
    }
    public void backtracking(int[] nums, boolean[] used, List<Integer> list, List<List<Integer>> res) {
        // 递归终止的条件:list集合的长度等于nums数组的长度
        if (list.size() == nums.length) {
            List<Integer> temp = new ArrayList<>();
            for (int i = 0; i < list.size(); i++) {
                temp.add(list.get(i));
            }
            res.add(temp);
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            // 注意:这里跳过此次循环的条件还需要加上used[i - 1] == false,因为如果used[i - 1] == true是不可以跳过的
            // 这个需要自己在纸上画出一颗树形结构来方便理解,并且这里要和组合问题中的组合总和II区分开
            if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
                continue;
            }
            if (used[i] == true) {
                continue;
            }
            used[i] = true;
            list.add(nums[i]);
            backtracking(nums, used, list, res);
            used[i] = false;
            list.remove(list.size() - 1);
        }
    }
}
三 切割问题
1. leetcode 131.分割回文串

leetcode 131.分割回文串(难度:中等)
题目描述:
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。
示例:
输入: “aab”
输出:
[
[“aa”,“b”],
[“a”,“a”,“b”]
]

public class 分割回文串 {
    public List<List<String>> partition(String s) {
        List<String> list = new ArrayList<>();
        List<List<String>> res = new ArrayList<>();
        backtracking(s, 0, list, res);
        return res;
    }
    public void backtracking(String s, int startIndex, List<String> list, List<List<String>> res) {
        // 递归终止的条件:startIndex代表遍历到字符串s的哪个位置,当startIndex大于等于字符串s的长度时,说明找到一个切割组合了
        if (startIndex >= s.length()) {
            List<String> temp = new ArrayList<>();
            for (int i = 0; i < list.size(); i++) {
                temp.add(list.get(i));
            }
            res.add(temp);
            return;
        }
        for (int i = startIndex; i < s.length(); i++) {
            // 判断s的子串[startIndex,i]是不是回文串,是则添加到list集合中,否则直接跳过该循环
            boolean flag = isPalindrome(s.substring(startIndex, i + 1));
            if (flag) {
                list.add(s.substring(startIndex, i + 1));
            } else {
                continue;
            }
            backtracking(s, i + 1, list, res);
            list.remove(list.size() - 1);
        }
    }
    /**
     * 判断一个字符串是不是回文串
     * @param s
     * @return
     */
    public boolean isPalindrome(String s) {
        int i = 0;
        int j = s.length() - 1;
        while (i < j) {
            if (s.charAt(i) == s.charAt(j)) {
                i++;
                j--;
            } else {
                return false;
            }
        }
        return true;
    }
}
2. leetcode 93.复原IP地址

leetcode 93.复原IP地址(难度:中等)
题目描述:
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效的 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效的 IP 地址。
示例 1:
输入:s = “25525511135”
输出:[“255.255.11.135”,“255.255.111.35”]
示例 2:
输入:s = “0000”
输出:[“0.0.0.0”]
示例 3:
输入:s = “1111”
输出:[“1.1.1.1”]
示例 4:
输入:s = “010010”
输出:[“0.10.0.10”,“0.100.1.0”]
示例 5:
输入:s = “101023”
输出:[“1.0.10.23”,“1.0.102.3”,“10.1.0.23”,“10.10.2.3”,“101.0.2.3”]

public class 复原IP地址 {
    public List<String> restoreIpAddresses(String s) {
        List<String> list = new ArrayList<>();
        List<String> res = new ArrayList<>();
        backtracking(s, 0, list, res);
        return res;
    }
    public void backtracking(String s, int startIndex, List<String> list, List<String> res) {
        // 递归终止的条件:因为要将字符串分割成四段,所以当list集合长度等于三,递归终止
        // 注意:并不是等到list集合长度等于四递归才终止,而是当集合长度等于三时,再判断剩余字符串是否有效,有效则加到res中
        if (list.size() == 3) {
            // 判断最后一段是否有效
            if (isValid(s.substring(startIndex))) {
                String str = "";
                for (int i = 0; i < list.size(); i++) {
                    str += list.get(i);
                    str += ".";
                }
                str += s.substring(startIndex);
                res.add(str);
            }
            return;
        }
        for (int i = startIndex; i < s.length(); i++) {
            // 判断s的子串[startIndex, i]是否有效,无效则直接break退出for循环,因为后面的肯定也不符合
            if (!isValid(s.substring(startIndex, i + 1))) {
                break;
            }
            list.add(s.substring(startIndex, i + 1));
            backtracking(s, i + 1, list, res);
            list.remove(list.size() - 1);
        }
    }
    /**
     * 判断字符串s代表的整数是否有效
     * (每个整数位于 0 到 255 之间组成,且不能含有前导 0,并且不能含有其他非数字字符)
     * @param s
     * @return
     */
    public boolean isValid(String s) {
        if (s == null || s.length() == 0) {
            return false;
        }
        // 判断是否含有前导0
        if (s.length() > 1 && s.charAt(0) == '0') {
            return false;
        }
        // 遍历整个字符串,判断是否存在非数字字符
        for (int i = 0; i < s.length(); i++) {
            if (!(s.charAt(i) >= '0' && s.charAt(i) <= '9')) {
                return false;
            }
        }
        // 将字符串转换成整数,看看值的范围是否在0-255(如果值超过Integer范围会异常)
        try {
            int x = Integer.parseInt(s);
            if (x >= 0 && x <= 255) {
                return true;
            } else {
                return false;
            }
        } catch (Exception e) {
            return false;
        }
    }
}
四 子集问题
1. leetcode 78.子集

leetcode 78.子集(难度:中等)
题目描述:
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例2:
输入:nums = [0]
输出:[[],[0]]

public class 子集 {
    /**
     * 子集问题和组合以及切割问题的区别在于:当抽象成一颗树形结构时,组合和切割问题收集的是树的叶子节点,而子集问题收集的是树的每一个节点
     * 因此,组合和切割问题是在满足递归终止的条件时再将结果添加到最终结果集中,而子集问题是在每次递归函数开始的时候就将结果添加到最终结果集中
     * 由于不能有重复的子集,因此也需要定义一个startIndex
     *
     * 求取子集问题,不需要任何剪枝!因为子集就是要遍历整棵树
     * @param nums
     * @return
     */
    public List<List<Integer>> subsets(int[] nums) {
        List<Integer> list = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        backtracking(nums, 0, list, res);
        return res;
    }
    public void backtracking(int[] nums, int startIndex, List<Integer> list, List<List<Integer>> res) {
        // 注意:这种处理情况要和组合以及切割问题区分开(空集也算子集)
        List<Integer> temp = new ArrayList<>();
        for (int i = 0; i < list.size(); i++) {
            temp.add(list.get(i));
        }
        res.add(temp);
        // 递归终止的条件:当startIndex大于等于数组长度时
        if (startIndex >= nums.length) {
            return;
        }
        for (int i = startIndex; i < nums.length; i++) {
            list.add(nums[i]);
            backtracking(nums, i + 1, list, res);
            list.remove(list.size() - 1);
        }
    }
}
2. leetcode 90.子集II

leetcode 90.子集II(难度:中等)
题目描述:
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]

public class 子集II {
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        List<Integer> list = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        // 对数组进行排序,方便去除重复的情况
        Arrays.sort(nums);
        backtracking(nums, 0, list, res);
        return res;
    }
    public void backtracking(int[] nums, int startIndex, List<Integer> list, List<List<Integer>> res) {
        // 注意:这种处理情况要和组合以及切割问题区分开(空集也算子集)
        List<Integer> temp = new ArrayList<>();
        for (int i = 0; i < list.size(); i++) {
            temp.add(list.get(i));
        }
        res.add(temp);
        // 递归终止的条件:当startIndex大于等于数组长度时
        if (startIndex >= nums.length) {
            return;
        }
        for (int i = startIndex; i < nums.length; i++) {
            if (i > startIndex &&nums[i] == nums[i - 1]) {
                continue;
            }
            list.add(nums[i]);
            backtracking(nums, i + 1, list, res);
            list.remove(list.size() - 1);
        }
    }
}
3. leetcode 491.递增子序列

leetcode 491.递增子序列(难度:中等)
题目描述:
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。
示例:
输入: [4, 6, 7, 7]
输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
说明:
给定数组的长度不会超过15。
数组中的整数范围是 [-100,100]。
给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况

public class 递增子序列 {
    /**
     * 本题求递增子序列,是不能对原数组进行排序的,因为排完序的数组都是自增子序列了。
     * 所以就不能用排序的方法来进行去重,这里我们借助set集合去重
     *
     * 改进:使用set集合效率会比较低,数组,set,map都可以做哈希表,而且数组干的活,map和set都能干,但如果数值范围小的话能用数组尽量用数组。
     * 题目中说了,数值范围[-100,100],所以完全可以用数组来做哈希
     * 因此,我们可以将原先每一层定义的一个set集合替换成定义一个长度为201的数组,int[] arr = new int[201];每个元素默认值为0
     * 在本层访问过,则修改值为1即可
     * @param nums
     * @return
     */
    public List<List<Integer>> findSubsequences(int[] nums) {
        List<Integer> list = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        backtracking(nums, 0, list, res);
        return res;
    }
    public void backtracking(int[] nums, int startIndex, List<Integer> list, List<List<Integer>> res) {
        // 子序列问题也类似于子集问题,需要收集整颗树的所有节点,并不仅仅是叶子节点
        // 由于题目要求递增子序列的长度至少是2,因此只有当list集合的长度大于等于2才可以加入到结果集合中
        if (list.size() >= 2) {
            List<Integer> temp = new ArrayList<>();
            for (int i = 0; i < list.size(); i++) {
                temp.add(list.get(i));
            }
            res.add(temp);
        }
        // 递归终止的条件:当startIndex大于等于数组长度时(可加可不加,因为startIndex >= nums.length时,for循环也不会执行)
        if (startIndex >= nums.length) {
            return;
        }
        // 使用set集合去重:如果某个元素已经在本层出现过了,直接跳过
        // 注意:这里定义的set集合只负责本层,记录本层元素是否重复使用
        // 由于递归调用,每次调用都会在虚拟栈中开辟一块独立的空间来存放本地调用的一些数据信息;因此每层声明的set集合都是独立的,互不影响
        Set<Integer> set = new HashSet<>();
        for (int i = startIndex; i < nums.length; i++) {
            if (set.contains(nums[i])) {
                continue;
            }
            set.add(nums[i]);
            // 如果当前元素小于list集合的最后一个元素,不满足递增,那么也直接跳过
            if (list.size() != 0 && nums[i] < list.get(list.size() - 1)) {
                continue;
            }
            list.add(nums[i]);
            backtracking(nums, i + 1, list, res);
            list.remove(list.size() - 1);
        }
    }
}
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值