刷题参考代码随想录:https://programmercarl.com/
文章目录
- 回溯
- [77. 组合](https://leetcode.cn/problems/combinations/)
- 216. 组合总和 III](https://leetcode.cn/problems/combination-sum-iii/)
- [17. 电话号码的字母组合](https://leetcode.cn/problems/letter-combinations-of-a-phone-number/)
- [39. 组合总和](https://leetcode.cn/problems/combination-sum/)
- [40. 组合总和 II](https://leetcode.cn/problems/combination-sum-ii/)
- [131. 分割回文串](https://leetcode.cn/problems/palindrome-partitioning/)
- [93. 复原 IP 地址](https://leetcode.cn/problems/restore-ip-addresses/)
- [78. 子集](https://leetcode.cn/problems/subsets/)
- [90. 子集 II](https://leetcode.cn/problems/subsets-ii/)
- [491. 非递减子序列](https://leetcode.cn/problems/non-decreasing-subsequences/)
- [46. 全排列](https://leetcode.cn/problems/permutations/) 注意Used数组的使用
- [47. 全排列 II](https://leetcode.cn/problems/permutations-ii/)
- 其他
回溯
回溯算法的本质
https://leetcode.cn/problems/palindrome-partitioning/solutions/3066497/hui-su-suan-fa-de-ben-zhi-mo-ni-you-xian-fhcb/
-
模拟有限for循环的过程
-
可以这么理解:在回溯算法中,我们写的那个 for 循环嵌套着递归调用,相当于模拟了多个嵌套的 for 循环。让我举个例子来说明:假设我们的字符串是 “abc”。如果不使用递归来解决划分问题,我们可能需要写出多重嵌套的 for 循环,每一层迭代一个可能的分割点,比如:
-
for (int i = 0; i < s.length(); i++){ // 第一层:选择第一个分割 // ... for (int j = i+1; j < s.length(); j++){ // 第二层:选择第二个分割 // ... for (int k = j+1; k < s.length(); k++) { // 第三层:选择第三个分割(如果需要) // ... } } }
当然,当字符串长度不固定时,这种写法就不现实了。而递归的巧妙之处在于:
每一次递归调用其实都在模拟“一层新的 for 循环” ,它从当前的 startIndex 开始,一直到字符串末尾。
递归调用结束的条件(例如 startIndex == s.length())确保了递归不无限进行,而是找到一个划分方式后回溯,继续寻找其他可能。
换句话说,每一次 for 循环内部的递归调用,都会为剩余部分开启一个新的迭代过程,就像在无限制数量的 for 循环中不断展开选择。所以,我们通过递归和 for 循环的结合,可以实现一种灵活而高效的“无限嵌套”的遍历,枚举出所有可能的解。
回溯算法模板
- Carl给出的回溯算法模板:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
77. 组合
https://programmercarl.com/0077.%E7%BB%84%E5%90%88.html#%E6%80%9D%E8%B7%AF
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
回溯
https://leetcode.cn/problems/combinations/solutions/13436/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-ma-/
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res =new LinkedList<>();
Deque<Integer> path=new ArrayDeque<>();
dfs(res,path,n,k,1);//startindex代表的是起始的数字 组合的数字 以数字1开始
return res;
}
private void dfs(List<List<Integer>> res,Deque<Integer> path,int n,int k,int startIndex){//startIndex 就是防止出现重复的组合。
if(path.size()==k){//path是存储深度优先遍历时候 从根部到叶子结点路径的所有节点的值,如果达到树高(k) 就代表可以添加到最后的res数组中 (path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了)
res.add(new ArrayList<>(path));
return;
}
for(int i=startIndex;i<=n;i++){
path.add(i);//添加向下延伸的路径中的第一个节点的值
dfs(res,path,n,k,i+1);//深度优先 向下遍历 ,startindex变为i+1
path.removeLast();//回溯 删除已经加入path组的元素,之后添加新的元素 组成新的组(旧的组已经加入到res中)
// removeLast 是 回溯,撤销处理的节点
}
}
}
回溯法三部曲
- 递归函数的返回值以及参数
在这里要定义两个全局变量,一个用来存放符合条件单一结果,一个用来存放符合条件结果的集合。
代码如下:
vector<vector<int>> result; // 存放符合条件结果的集合 vector<int> path; // 用来存放符合条件结果
剪枝优化
当n=4 k=4的时候,startindex从2开始就是没有意义的遍历,1之后的startindex是i+1赋予的,所以可以通过限制i的范围来剪枝优化
(下图中是取4个数)
举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。
从1开始搜索都是合理的,可以是组合[1, 2, 3]。
从2开始搜索都是合理的,可以是组合[2, 3, 4]。
从3开始搜索不合理
i <= n - (k - path.size()) + 1 限制
216. 组合总和 III](https://leetcode.cn/problems/combination-sum-iii/)
找出所有相加之和为 n
的 k
个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。
示例 3:
输入: k = 4, n = 1
输出: []
解释: 不存在有效的组合。
在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。
提示:
2 <= k <= 9
1 <= n <= 60
代码
//77 返回范围 `[1, n]` 中所有可能的 `k` 个数的组合。
//216 返回范围 `[1, 9]` 中所有可能的 `k` 个数的组合,而且和为n。
class Solution {
public List<List<Integer>> combinationSum3(int k, int n) {
List<List<Integer>> res=new ArrayList<>();
if(n<k){//4个数之和为3不成立
return res;
}
Deque<Integer> path=new ArrayDeque<>();
dfs(k,n,res,path,1);
return res;
}
//77 返回范围 `[1, n]` 中所有可能的 `k` 个数的组合。
//216 返回范围 `[1, 9]` 中所有可能的 `k` 个数的组合,而且和为n。
private void dfs(int k, int n,List<List<Integer>> res,Deque<Integer> path,int startindex) {
if(path.size()==k){
int sum=0;
for(int num:path){
sum+=num;
}
if(sum==n){//相比77题就是加了筛选的条件 只有 长度为k 而且 和为n 的组合才可以加入res
res.add(new ArrayList<>(path));
}
}
for(int i=startindex;i<=9;i++){
path.add(i);
dfs(k,n,res,path,i+1);
path.removeLast();
}
}
}
17. 电话号码的字母组合
https://programmercarl.com/0017.%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E6%AF%8D%E7%BB%84%E5%90%88.html#%E6%80%9D%E8%B7%AF
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = ""
输出:[]
示例 3:
输入:digits = "2"
输出:["a","b","c"]
代码
理解本题后,要解决如下三个问题:
- 数字和字母如何映射
- 两个字母就两个for循环,三个字符我就三个for循环,以此类推,然后发现代码根本写不出来
- 输入1 * #按键等等异常情况
class Solution {
//初始对应所有的数字,为了直接对应2-9,新增了两个无效的字符串""
String[] MAPPING = { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" };
public List<String> letterCombinations(String digits) {
List<String> res = new ArrayList<>();
if (digits == null || digits.length() == 0) {
return res;
}
//每次迭代获取一个字符串,所以会涉及大量的字符串拼接,所以这里选择更为高效的 StringBuilder
StringBuffer temp = new StringBuffer();
//迭代处理
dfs(res, temp, digits, 0);
return res;
}
void dfs(List<String> res, StringBuffer temp, String digits, int num) {
if (temp.length() == digits.length()) {//确定终止条件
res.add(temp.toString());
return;
}
//比如digits如果为"23",num 为0,则str表示2对应的 abc
String str = MAPPING[digits.charAt(num) - '0'];//str 表示当前num对应的字符串
//digits[0]='2' ,之后转化为数字2,str取MAPPING[2]='adc'
for (int i = 0; i < str.length(); i++) {
temp.append(str.charAt(i));
//'adc'中取一个,之后num+1 ,在‘bcd’中再取一个
dfs(res, temp, digits, num + 1);//递归,处理下一层
temp.deleteCharAt(temp.length() - 1);//剔除末尾的,继续尝试
}
}
}
39. 组合总和
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1
输出: []
提示:
1 <= candidates.length <= 30
2 <= candidates[i] <= 40
candidates
的所有元素 互不相同1 <= target <= 40
代码:
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res=new ArrayList<>();
Deque<Integer> path =new ArrayDeque<>();
// Arrays.sort(candidates); // 先进行排序
dfs(res,path,candidates,target,0,0);
return res;
}
public void dfs(List<List<Integer>> res,Deque<Integer> path,int[] candidates,int target,int index,int sum){
if(sum==target){// 找到了数字和为 target 的组合
res.add(new ArrayList<>(path));
return;
}
if(sum>target){return;}//剪枝优化
for (int i=index;i<candidates.length;i++){
path.add(candidates[i]);
//一定一定要注意 下面这里dfs传入的参数是i而非index
//之所以传入的不是i+1是因为数字可以重复,
dfs(res,path,candidates,target,i,sum+candidates[i]);//注意这里的sum,后面没有sum-什么什么
path.removeLast();// 回溯,移除路径 path 最后一个元素
}
}
}
注意回溯的过程 ,允许数字的重复怎么办:
- 上图中绿线的效果是由代码段:
for (int i=index;i<candidates.length;i++){
中的i++
引起的 - 上图中蓝线的效果,之所以上面253 下面还是253,是因为
dfs(res,path,candidates,target,i,sum+candidates[i]);
里的i没有像之前一样加1 - 这里的代码段:
for (int i=index;i<candidates.length;i++){
中的i++
控制了横向的和 竖向的,图中的绿色和蓝色的标识,影响树枝 和 树层的去重
注意sum的处理
注意这里面的处理:
for (int i=index;i<candidates.length;i++){
path.add(candidates[i]);
sum+=candidates[i];
dfs(res,path,candidates,i,sum,target);
sum-=candidates[i];
path.removeLast();
}
可以换成:
for (int i=index;i<candidates.length;i++){
path.add(candidates[i]);
dfs(res,path,candidates,i,sum+candidates[i],target);//注意这里的sum,后面没有sum-什么什么
path.removeLast();
}
注意下面两种剪枝优化的效果对比:
40. 组合总和 II
给定一个候选人编号的集合 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用 一次 。
**注意:**解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]
提示:
1 <= candidates.length <= 100
1 <= candidates[i] <= 50
1 <= target <= 30
代码:
错误代码 (同时把树层和树枝都去重了):
漏掉了类似1,1,6这种情况
正确代码:
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res=new ArrayList<>();
Deque<Integer> path =new ArrayDeque<>();
Arrays.sort(candidates); // 先进行排序 这个是必要的步骤,为下面的>target 剪枝做基础
boolean []used=new boolean[candidates.length];
// Arrays.fill(used, false);
dfs(res,path,candidates,target,used,0,0);
return res;
}
public void dfs(List<List<Integer>> res, Deque<Integer> path, int[] candidates, int target,boolean []used, int index, int sum){
if(sum==target){// 找到了数字和为 target 的组合
res.add(new ArrayList<>(path));
return;
}
for (int i=index;i<candidates.length;i++){//注意
if(i!=0 && candidates[i]==candidates[i-1] && used[i-1]==false){
continue;
}
if(sum+candidates[i]>target){break;}//剪枝优化
path.add(candidates[i]);
used[i]=true;
//一定一定要注意 下面这里dfs传入的参数是i而非index
//之所以传入的不是i+1是因为数字可以重复,
dfs(res,path,candidates,target,used,i+1,sum+candidates[i]);//注意这里的sum,后面没有sum-什么什么
used[i]=false;
path.removeLast();// 回溯,移除路径 path 最后一个元素
}
}
}
boolean
和 Boolean
的区别!
-
Boolean[] used = new Boolean[candidates.length]; //数组中的每个元素都会被初始化为 `null`。 boolean[] used = new boolean[candidates.length]; //数组中的每个元素都会被初始化为 `false`。
131. 分割回文串
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是
回文串
。返回 s
所有可能的分割方案。
示例 1:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
示例 2:
输入:s = "a"
输出:[["a"]]
提示:
1 <= s.length <= 16
s
仅由小写英文字母组成
代码
class Solution {
List<List<String>> res = new ArrayList<>();
public List<List<String>> partition(String s) {
if (s == null || s.length() == 0)
return null;
List<String> path = new ArrayList<>();
dfs(path, s, 0);
return res;
}
public void dfs(List<String> path, String s, int startindex) {
if (startindex == s.length()) {// 已经划分到最后一个字母
res.add(new ArrayList<>(path));
return;
}
for (int i = startindex; i < s.length(); i++) {
String sub = s.substring(startindex, i + 1);//前闭后开
if (isHuiwen(sub)) {//注意这里先判断回文 回文是后续的基础
path.add(sub);
dfs(path, s, i + 1);
path.remove(path.size() - 1);
}
}
}
public boolean isHuiwen(String sub) {
int len = sub.length();
for (int i = 0; i < len / 2; i++) {
if (sub.charAt(i) != sub.charAt(len - 1 - i)) {
return false;
}
}
return true;
}
}
93. 复原 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 地址。
给定一个只包含数字的字符串 s
,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s
中插入 '.'
来形成。你 不能 重新排序或删除 s
中的任何数字。你可以按 任何 顺序返回答案。
示例 1:
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]
示例 2:
输入:s = "0000"
输出:["0.0.0.0"]
示例 3:
输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
提示:
1 <= s.length <= 20
s
仅由数字组成
代码
class Solution {
List<String> res = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
StringBuilder sb =new StringBuilder(s);
backTracking(sb,0,0);
return res;
}
private void backTracking(StringBuilder s, int startIndex, int dotCount){
if (dotCount == 3) {
if(isValid(s,startIndex,s.length()-1)){
res.add(s.toString());
return;
}
}
for (int i=startIndex;i<s.length();i++){
if(isValid(s,startIndex,i)){
s.insert(i+1,".");
backTracking(s,i+2,dotCount+1);//注意这里 因为加了逗号 所以需要在原来加1的基础上再加1
s.deleteCharAt(i+1);
}
}
}
//[start, end] 都是闭区间
private boolean isValid(StringBuilder s, int start, int end){
if(start>end)return false;
if(s.charAt(start) == '0' && start != end){
return false;
}
int num=0;
for(int i=start;i<=end;i++){
num=num*10+(s.charAt(i)-'0');
if(num>255)return false;
}
return true;
}
}
78. 子集
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的
子集(幂集)
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
nums
中的所有元素 互不相同
代码
如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
https://programmercarl.com/0078.%E5%AD%90%E9%9B%86.html#%E6%80%9D%E8%B7%AF
class Solution {
List<List<Integer>> res=new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
List<Integer> path=new ArrayList<>();
backTracking(nums,path,0);
return res;
}
public void backTracking(int[] nums,List<Integer> path,int startindex){
res.add(new ArrayList<>(path));//注意这里不能直接写path
for(int i=startindex;i< nums.length;i++){
path.add(nums[i]);
backTracking(nums,path,i+1);
path.remove(path.size()-1);
}
}
}
90. 子集 II
给你一个整数数组 nums
,其中可能包含重复元素,请你返回该数组所有可能的
子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
提示:
1 <= nums.length <= 10
上-10 <= nums[i] <= 10
代码1:使用used去重
class Solution {
List<List<Integer>> res=new ArrayList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<Integer> path=new ArrayList<>();
Arrays.sort(nums); // 先进行排序 这是必要的步骤,使相同的数字排列在一起
boolean []used=new boolean[nums.length];
backTracking(nums,path,used,0);
return res;
}
public void backTracking(int[] nums,List<Integer> path,boolean []used,int startindex){
res.add(new ArrayList<>(path));
for(int i=startindex;i< nums.length;i++){
if(i!=0 && nums[i]==nums[i-1] && used[i-1]==false){//注意这里仅仅是对树层的去重***
continue;
}
path.add(nums[i]);
used[i]=true;//注意不要遗忘了这里
backTracking(nums,path,used,i+1);//注意这里的i+1 不要写成index,注意当初始集合里面的数字要求不能重复使用的时候,这里必须是i+1; 如果初始集合里面的数字可以重复使用,这里是i;
used[i]=false;
path.remove(path.size()-1);
}
}
}
代码2:使用hashset去重
class Solution {
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<Integer> path = new ArrayList<>();
Arrays.sort(nums); // 先进行排序
backTracking(nums, path, 0);
return res;
}
public void backTracking(int[] nums, List<Integer> path, int startindex) {
res.add(new ArrayList<>(path));
HashSet<Integer> hs = new HashSet<>();
for (int i = startindex; i < nums.length; i++) {
if (hs.contains(nums[i])) {
continue;
}
hs.add(nums[i]);
path.add(nums[i]);
backTracking(nums, path, i + 1);
path.remove(path.size() - 1);
}
}
}
对于已经习惯写回溯的同学,看到递归函数上面的hs.add(nums[i]);
,下面却没有对应的pop之类的操作,应该很不习惯吧
这也是需要注意的点,hs.add(nums[i]);;
是记录本层元素是否重复使用,新的一层hs都会重新定义(清空)(每次进入递归的时候hs会被重新定义),所以要知道hs只负责本层!
491. 非递减子序列
给你一个整数数组 nums
,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例 1:
输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
示例 2:
输入:nums = [4,4,3,2,1]
输出:[[4,4]]
思路
而本题求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。
所以不能使用之前的去重逻辑! 使用used没有用
代码
class Solution {
List<List<Integer>> res=new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
List<Integer> path=new ArrayList<>();
backTracking(nums,path,0);
return res;
}
private void backTracking(int[] nums,List<Integer> path,int startindex){
if(path.size()>=2){
res.add(new ArrayList<>(path));
}
HashSet<Integer> hs=new HashSet<>();
for(int i=startindex;i<nums.length;i++){
//nums[i]<path.get(path.size()-1) 新的元素如果比path里面最后一个元素小 (控制上图中的蓝色圈)
//hs.contains(nums[i]) hs控制的是每层的去重(上图中的绿色标识)
if(!path.isEmpty() && nums[i]<path.get(path.size()-1) || hs.contains(nums[i])){//前面两句先运算
continue;
}
hs.add(nums[i]);
path.add(nums[i]);
backTracking(nums,path,i+1);
//注意这里不需要pop之类的操作
path.remove(path.size()-1);
}
}
}
对于已经习惯写回溯的同学,看到递归函数上面的hs.add(nums[i]);
,下面却没有对应的pop之类的操作,应该很不习惯吧
这也是需要注意的点,hs.add(nums[i]);;
是记录本层元素是否重复使用,新的一层hs都会重新定义(清空)(每次进入递归的时候hs会被重新定义),所以要知道hs只负责本层!
46. 全排列 注意Used数组的使用
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
提示:
1 <= nums.length <= 6
-10 <= nums[i] <= 10
nums
中的所有整数 互不相同
代码
class Solution {
List<List<Integer>> res=new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
List<Integer> path=new ArrayList<>();
boolean []used =new boolean[nums.length];//注意这里used的使用是必要的
backTracking(nums,path,used);
return res;
}
private void backTracking(int[] nums,List<Integer> path,boolean []used){
if(path.size()==nums.length){
res.add(new ArrayList<>(path));
}
for(int i=0;i<nums.length;i++){
if(used[i]==true)continue;
path.add(nums[i]);
used[i]=true;
backTracking(nums,path,used);
used[i]=false;
path.remove(path.size()-1);
}
}
}
我们需要引入一个布尔类型的数组 used
来标记每个元素是否已经在当前排列中被使用。在递归调用时,只选择那些未被使用的元素。
错误代码问题分析
这段代码出现 StackOverflowError
错误,主要原因在于递归调用没有合理控制元素的使用,导致无限递归。具体来说,在生成排列的过程中,没有对已经使用过的元素进行标记和排除,每次递归时都会尝试将所有元素添加到排列中,使得递归无法正常终止。
- 缺乏元素使用标记:**在
backTracking
方法的for
循环里,每次递归调用都会尝试将nums
数组中的所有元素添加到path
中,**这就造成同一个元素可能被多次使用,从而引发无限递归。 - 未正确终止递归:虽然代码里有递归终止条件
if(path.size() == nums.length)
,但由于元素使用的重复问题,这个条件很难被有效触发,进而导致栈溢出错误。
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
思路分析:
这道题目和46.全排列 (opens new window)的区别在与给定一个可包含重复数字的序列,要返回所有不重复的全排列。
去重法1:used(一般都需要先排序)
还要强调的是使用used数组去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了。
去重法2:hashset(更通用)
代码:
class Solution {
List<List<Integer>> res=new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
List<Integer> path=new ArrayList<>();
boolean []used =new boolean[nums.length];//注意这里used的使用是必要的
backTracking(nums,path,used);
return res;
}
private void backTracking(int[] nums,List<Integer> path,boolean []used){
if(path.size()==nums.length){
res.add(new ArrayList<>(path));
}
HashSet<Integer> hs=new HashSet<>();
for(int i=0;i<nums.length;i++){
if(used[i]==true || hs.contains(nums[i]))continue;
hs.add(nums[i]);
path.add(nums[i]);
used[i]=true;
backTracking(nums,path,used);
used[i]=false;
path.remove(path.size()-1);
}
}
}
class Solution {
int res = 0;
Map<Integer, Integer> path = new HashMap<>();
public int beautifulSubsets(int[] nums, int k) {
backTracking(nums, 0, k);
return res;
}
public void backTracking(int[] nums, int startindex, int k) {
if (!path.isEmpty()) {// 重要
res++;
}
for (int i = startindex; i < nums.length; i++) {
// 如果(新元素值 +k 或者-k) 不在path 中,那么加入
if (path.getOrDefault(nums[i] - k, 0) == 0 && path.getOrDefault(nums[i] + k, 0) == 0) {
path.merge(nums[i], 1, Integer::sum);// 元素
backTracking(nums, i + 1, k);
path.merge(nums[i], -1, Integer::sum);
}
}
}
}
其他
22. 括号生成
数字 n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
class Solution {
List<String> res=new ArrayList<>();
public List<String> generateParenthesis(int n) {
String path=""; //从根结点到叶子结点的路径字符串
dfs(path,n,n);
return res;
}
private void dfs(String path,int left,int right){//左括号还可以使用的个数
if(left==0 && right==0){
res.add(path);
}else if(left>right){
return;
}
if(left>0){
dfs(path+'(',left-1,right);
}
if(right>0){
dfs(path+')',left,right-1);
}
}
}
79. 单词搜索
给定一个 m x n
二维字符网格 board
和一个字符串单词 word
。如果 word
存在于网格中,返回 true
;否则,返回 false
。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例 1:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
示例 2:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出:true
code
https://leetcode.cn/problems/word-search/solutions/2361646/79-dan-ci-sou-suo-hui-su-qing-xi-tu-jie-5yui2/?envType=study-plan-v2&envId=top-100-liked
https://leetcode.cn/problems/word-search/solutions/2927294/liang-ge-you-hua-rang-dai-ma-ji-bai-jie-g3mmm/?envType=study-plan-v2&envId=top-100-liked
class Solution {
int dirs[][] = new int[][]{{0, 1}, {-1, 0}, {0, -1}, {1, 0}};
public boolean exist(char[][] board, String word) {
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (dfs(board, word, i, j, 0)) {
return true;
}
}
}
return false;
}
boolean dfs(char[][] board, String word, int i, int j, int k) {
if (board[i][j] != word.charAt(k)) {
return false;
}
if (k == word.length()-1) {
return true;
}
board[i][j] = 0;//走过路设置为0 下次就不走了 因为 board[i][j]=0 != word.charAt(k)
for (int[] d : dirs) {
int x = i + d[0];
int y = j + d[1]; // 相邻格子
if (0 <= x && x < board.length && 0 <= y && y < board[x].length && dfs(board, word, x, y, k + 1)) {
return true; // 搜到了!
}
}
board[i][j] = word.charAt(k);//恢复原来的样子
return false;
}
}