算法-子集、排列、组合问题
子集
输入一个不包含重复元素的数组,求它的子集,函数签名如下
1 | public List<List<Integer>> subsets(int[] nums); |
比如输入了[1,2,3]
返回以下数据
1 | [ |
递归
第一个解法就是我们可以通过小规模问题推导出大规模问题的,即分治的方式来解决
空数组的子集
subsets([])=[[]]
1的子集
subsets([1])=[[],[1]]
2的子集
subsets([1,2])=[[],[1],[2],[1,2]]
3的子集
subsets([1,2,3])=[[],[1],[2],[3],[1,2],[1,3],[2,3],[1,2,3]
通过数学归纳法,我们可以发现,大规模的求子集问题可以通过小规模的求子集问题中推导得知
2的子集可以由1的子集再加上1的子集中每一个元素加上2求得
3的子集可以由2的子集再加上2的子集中每一个元素加上3求得
这不就是典型的递归和分治的思想吗
1 | public List<List<Integer>> subsets(int[] nums){ |
回溯
还可以使用回溯的思想来解决这道题,直接套用回溯的模板
1 | public List<List<Integer>> subsets(int[] nums) { |
nums=[1,2,3]
的决策树如下
绿色的数组代表的是选择列表
组合
输入两个数字 n, k
,算法输出 [1..n]
中 k 个数字的所有组合,方法签名如下
1 | List<List<Integer>> combine(int n, int k); |
这是一个很典型的回溯的问题
n限制了树的宽度,k限制了树的高度
1 | public List<List<Integer>> combine(int n, int k) { |
n = [1,2,3]
的时候的决策树是这样的
绿色的数组代表的是选择列表
可以发现这张决策树长得和求子集的时候的决策树很像
是这样的,求子集的时候是将每一个节点的选择都记录下来了
而求组合问题的时候,其实就是只要叶子节点的选择
排列
输入一个数组nums
,返回这个数组的所有排列方式,函数签名如下
1 | public List<List<Integer>> permute(int[] nums); |
nums=[1,2,3]
的决策树如下
1 | class Solution { |
排列问题中通过 contains
方法来排除在 track
中已经选择过的数字,从而得到选择列表,因为维护一个选择列表比较麻烦,所以用了这种推导的方式
组合问题通过维护一个 start
参数,来排除 start
索引之前的数字,从而得到选择列表,因为组合中[1,2]
和[2,1]
是算重复的