1. 题目
https://leetcode.cn/problems/subsets/
考察点:
2. 解法
解法有三种
Leetcode 78是一个关于子集合的问题,给定一个不重复的整数数组nums,返回所有可能的子集(幂集)。
有多种方法可以用Java实现,比如:
- 使用迭代法,每次遍历数组中的一个元素,将其加入到已有的子集中,并生成新的子集。
- 使用回溯法,每次选择或不选择数组中的一个元素,递归地构建子集,并将结果添加到列表中。
- 使用位运算法,每个子集可以用一个二进制数表示,其中第i位为1表示选择数组中的第i个元素,为0表示不选择。遍历所有可能的二进制数,将对应的子集添加到列表中
解法一:使用迭代法
思路
方法一的思路是利用数学上的集合运算,如果一个集合有n个元素,那么它的子集有2^n个,可以用二进制数表示。例如,如果集合是{1,2,3},那么它的子集有8个,分别是:
- 000: 空集
- 001: {1}
- 010: {2}
- 011: {1,2}
- 100: {3}
- 101: {1,3}
- 110: {2,3}
- 111: {1,2,3}
可以看到,每次从左到右遍历一个二进制位,就相当于选择或不选择当前元素。所以,我们可以用一个循环来遍历数组中的每个元素,每次将其加入到已有的子集中,并生成新的子集。例如,当遍历到第一个元素1时,我们将其加入到空集中,得到{1},然后将空集和{1}都添加到结果列表中。当遍历到第二个元素2时,我们将其加入到空集和{1}中,得到{2}和{1,2},然后将这两个新的子集也添加到结果列表中。以此类推,直到遍历完所有元素,就得到了所有的子集。
代码逻辑
具体实现
public List<List<Integer>> subsets(int[] nums) { List<List<Integer>> result = new ArrayList<>(); if (nums == null) { return result; } result.add(new ArrayList<>()); // 空集 for (int num : nums) { // 遍历数组中的每个元素 List<List<Integer>> temp = new ArrayList<>(); // 创建一个临时列表存储新生成的子集 for (List<Integer> list : result) { // 遍历已有的子集 List<Integer> newList = new ArrayList<>(list); // 复制一份 newList.add(num); // 加入当前元素 temp.add(newList); // 添加到临时列表中 } result.addAll(temp); // 将临时列表中的所有子集添加到结果列表中 } return result; }
解法二:使用回溯法
思路
使用回溯算法的思路是,
从空集开始,每次考虑数组中的一个元素,是否加入到当前的子集中,然后递归地处理剩余的元素。
当遍历完所有元素后,将当前的子集加入到解集中。
为了避免重复,可以先对数组进行排序,然后保证每次只从当前元素之后的元素中选择。
代码逻辑
代码的逻辑是这样的:
- 首先,对数组进行排序,方便去重。
- 然后,创建一个解集res,用来存放所有的子集,以及一个临时子集subset,用来存放当前的子集。
- 接着,调用一个回溯函数backtrack,传入数组nums,起始位置start,临时子集subset和解集res。
- 在回溯函数中,首先将当前的子集subset复制一份并加入到解集res中。
- 然后,从起始位置start开始遍历数组nums中的元素,对于每个元素nums[i],有两种选择:加入到当前子集中或不加入。
- 如果选择加入到当前子集中,就将nums[i]添加到subset的末尾,然后递归地调用回溯函数,传入数组nums,下一个起始位置i+1,临时子集subset和解集res。
- 如果选择不加入到当前子集中,就跳过这个元素,继续遍历下一个元素。
- 在递归返回后,需要回溯,即将刚刚添加到subset的末尾的元素移除,恢复到之前的状态。
- 这样,就可以遍历所有可能的子集,并将它们加入到解集中。
具体实现
class Solution { public List<List<Integer>> subsets(int[] nums) { // 排序数组,方便去重 Arrays.sort(nums); // 创建解集和临时子集 List<List<Integer>> res = new ArrayList<>(); List<Integer> subset = new ArrayList<>(); // 调用回溯函数 backtrack(nums, 0, subset, res); // 返回解集 return res; } // 回溯函数 private void backtrack(int[] nums, int start, List<Integer> subset, List<List<Integer>> res) { // 将当前子集加入到解集中 res.add(new ArrayList<>(subset)); // 遍历数组中的元素 for (int i = start; i < nums.length; i++) { // 将当前元素加入到子集中 subset.add(nums[i]); // 递归地处理剩余的元素,注意下一次的起始位置是i+1 backtrack(nums, i + 1, subset, res); // 回溯,将当前元素从子集中移除 subset.remove(subset.size() - 1); } } }
解法三:使用位运算法
思路
代码逻辑
具体实现
public List<List<Integer>> subsets(int[] nums) { List<List<Integer>> result = new ArrayList<>(); int n = nums.length; // 数组长度 int m = 1 << n; // 子集个数,等于2的n次方 for (int i = 0; i < m; i++) { // 遍历所有可能的二进制数 List<Integer> list = new ArrayList<>(); // 创建一个空列表存储当前子集 for (int j = 0; j < n; j++) { // 遍历数组中的每个元素 if ((i & (1 << j)) != 0) { // 如果二进制数的第j位为1,表示选择该元素 list.add(nums[j]); // 将该元素添加到列表中 } } result.add(list); // 将当前列表添加到结果列表中 } return result; }