22. 括号生成
class Solution { public List<String> generateParenthesis(int n) { List<String> result = new ArrayList(); if (n == 0) { return result; } // 必须要用字符串,每次拼接要产生新对象。不能用 StringBuffer StringBuilder 之类,栈里不能都操作一个对象 dfs(n, n, "", result); return result; } private void dfs(int leftN, int rightN, String currentNodeStr, List<String> result) { // 左右括号都用完 if (leftN == 0 && rightN == 0) { result.add(currentNodeStr); return; } // 剩余的右括号不仅要 cover 掉剩余的左括号,还要 cover 掉现有没匹配上的左括号 // 所以剩余的右括号一定要大于剩余的左括号 if (rightN < leftN) { return; } // 左、右递归 if (leftN > 0) { // 必须要 leftN-1 ,不能 leftN-- dfs(leftN-1, rightN, currentNodeStr + "(", result); } if (rightN > 0) { // 必须要 rightN-1 ,不能 rightN-- dfs(leftN, rightN-1, currentNodeStr + ")", result); } } }
51. N 皇后
在类中定义一个全局的结果
树的最大层数是行,每个分支是列,每次递归行数加一,循环对每一列进行递归
递归函数的最开始,判断 如果行数已经到了最后一行(=n),添加到全局结果中,return
判断可以放置,就先在棋盘上放置好(设为 "Q"),再进行递归
注意如果 判断不可以放置,不用 return,而是回来之后恢复现场,重新设为 ".",也就是回溯恢复现场
判断能否放置:检查此次皇后放置位置的同一列的上半部分。以及左上斜对角和右上斜对角(下半部分不用检查),检查斜对角的时候注意是单层循环,ij行列同时加减,不是双层循环
关于回溯
先将第一个皇后放在第一行的第一列上,符合题目要求
开始放置第二个皇后。放在第二行的第一个与第一行的皇后为同一列,不符合题意,继续向后搜素,放在第二列上面与第一个皇后在同一斜线上,不符合题意,继续向后搜素,发现放在第三列符合题意
开始放置第三个皇后。放在第三行的任意位置都会出现冲突,此时需要回溯,将第二个皇后放置在第四列,此时符合题意,继续放置第三个皇后,发现第三个皇后放置在第三行的第二列符合题意
继续放置第四个皇后。放在第四行的任意位置都会出现冲突,此时需要回溯,第三个皇后向后移动,发现依然不符合题意,继续回溯,第二行的皇后无法再向后移动,继续回溯,将第一个皇后向后移动到第二列,符合题意
移动第二个皇后,发现放在第四列符合题意
移动第三个皇后,发现放在第一列符合题意
移动第四个皇后,发现放在第三列符合题意
回溯结束
class Solution { private static List<List<String>> res = new ArrayList(); public Solution() { // 不知道为啥,提交答案的时候似乎有之前用例的残余 res.clear(); } public List<List<String>> solveNQueens(int n) { dfs(n, getInitChessBoard(n), 0); return res; } private void dfs(int n, char[][] chessBoard, int newQueenRow) { if (newQueenRow == n) { res.add(char2DArray2StringList(chessBoard, n)); return; } // 对每个分支 dfs。每个分支是每一列。(列的变化通过在 chessBoard 的放置传递进去,而层数即行的变化需要作为显式参数) for (int newQueenCol=0;newQueenCol<n;++newQueenCol) { // 不能 return 返回,因为对那些不满足条件的,要回溯,即回去恢复现场 /* if (!canSet(n, chessBoard, newQueenRow, newQueenCol)) { return; } */ if (canSet(n, chessBoard, newQueenRow, newQueenCol)) { // 可以设置了再设置 chessBoard[newQueenRow][newQueenCol] = 'Q'; dfs(n, chessBoard, newQueenRow + 1); // 回溯就指的是这一步,不满足条件的不要 return退出搜索,而是到这里回来后恢复现场 chessBoard[newQueenRow][newQueenCol] = '.'; } } } private boolean canSet(int n, char[][] chessBoard, int newQueenRow, int newQueenCol) { // 不用检查同一行(每次 dfs 层数 +1 也就是说限制了一行只能放置一个) // 检查同一列上面 for (int i=0; i<newQueenRow; i++) { // 不是自己且为 Q if (chessBoard[i][newQueenCol] == 'Q') { return false; } } // 检查左上斜对角(不用检查下半截) // 注意不是双层循环,斜对角要横纵坐标同进同退,是单层循环 for (int i=newQueenRow-1,j=newQueenCol-1;i>=0 && j>=0; i--,j--) { if (chessBoard[i][j] == 'Q') { return false; } } // 检查右上斜对角(不用检查下半截) // 注意不是双层循环,斜对角要横纵坐标同进同退,是单层循环 for (int i=newQueenRow-1,j=newQueenCol+1;i>=0 && j<n; i--,j++) { if (chessBoard[i][j] == 'Q') { return false; } } return true; } private char[][] getInitChessBoard(int n) { char[][] initChessBoard = new char[n][n]; for (int i=0;i<n;i++) { for (int j=0;j<n;j++) { initChessBoard[i][j] = '.'; } } return initChessBoard; } private List<String> char2DArray2StringList(char[][] charArray, int n) { List<String> list = new ArrayList(); for (int i=0;i<n;i++) { // copy 一行的字符 到 字符串 s String s = String.copyValueOf(charArray[i], 0, n); list.add(s); } return list; } }
17. 电话号码的字母组合
递归树的层数是按钮的总个数,每次递归层数加一
递归树的分支是每个按钮上对应的那几个字符,因此每次递归对这几个字符分别递归
class Solution { public Solution() { resList.clear(); } private List<String> resList = new ArrayList(); private static Map<Character, List<Character>> buttonMap = new HashMap(); static { buttonMap.put('2', Arrays.asList('a', 'b', 'c')); buttonMap.put('3', Arrays.asList('d', 'e', 'f')); buttonMap.put('4', Arrays.asList('g', 'h', 'i')); buttonMap.put('5', Arrays.asList('j', 'k', 'l')); buttonMap.put('6', Arrays.asList('m', 'n', 'o')); buttonMap.put('7', Arrays.asList('p', 'q', 'r', 's')); buttonMap.put('8', Arrays.asList('t', 'u', 'v')); buttonMap.put('9', Arrays.asList('w', 'x', 'y', 'z')); } public List<String> letterCombinations(String digits) { if (digits == null || "".equals(digits)) { return resList; } dfs(digits, 0, ""); return resList; } private void dfs(String digits, int curButtonLayer, String curRes) { // 走到最后一层叶子节点,结果获得,递归结束 if (curButtonLayer == digits.length()) { resList.add(curRes); return; } //根据层数获得当前按钮 Character curButton = digits.charAt(curButtonLayer); //根据当前按钮获得当前按钮上的字符 List<Character> buttonChars = buttonMap.get(curButton); // 递归树的分支是每个按钮上对应的那几个字符,因此每次递归对这几个字符分别递归 for (int i=0;i<buttonChars.size();i++) { // 递归树的层数是按钮的总个数,每次递归层数加一 dfs(digits, curButtonLayer + 1, curRes + buttonChars.get(i)); } } }
46. 全排列
就是 dfs & 回溯,注意点:
- 由于每一个排列不能有重复字符,所以用一个 boolean[] used = new boolean[nums.length]; 来标记当前数字是否被添加到了结果中
- 如果层数达到 n,添加结果时,要添加深拷贝 resList.add(new LinkedList(curArray)); 而不能直接添加 curArray,要不然结果里就是curArray的复制,而curArray在后面的递归里还会变
递归 & 回溯过程:
for (int i=0;i<nums.length;i++) { // 全排列要求不能有重复的数字,所以用 uesd 来标志某数字是否被使用过 if (!used[i]) { curArray.addLast(nums[i]); used[i] = true; dfs(nums, curLayer+1, curArray, used); // dfs 完成后进行回溯 curArray.removeLast(); used[i] = false; } }
class Solution { private List<List<Integer>> resList = null; Solution() { resList = new ArrayList(); } public List<List<Integer>> permute(int[] nums) { // 初始化标记数组 boolean[] used = new boolean[nums.length]; for (int i=0;i<nums.length;i++) { used[i] = false; } dfs(nums, 0, new LinkedList<Integer>(), used); return resList; } private void dfs(int[] nums, int curLayer, LinkedList<Integer> curArray, boolean[] used) { // 层数是固定的,为nums的长度。所以以层数达到最后一层作为递归终止的条件 if (curLayer == nums.length) { // 这里添加时,要curArray的复制,要不然结果里就是curArray的复制,而curArray在后面的递归里还会变 resList.add(new LinkedList(curArray)); return; } for (int i=0;i<nums.length;i++) { // 全排列要求不能有重复的数字,所以用 uesd 来标志某数字是否被使用过 if (!used[i]) { curArray.addLast(nums[i]); used[i] = true; dfs(nums, curLayer+1, curArray, used); // dfs 完成后进行回溯 curArray.removeLast(); used[i] = false; } } } }
78. 子集
两种 dfs :
1.选或不选nums[层数]
总层数就是 nums 的长度
每个节点有两个分支:选nums[层数] 还是 不选nums[层数]
到最后一层所有的叶子节点就是结果,所以如果层数是最后一层,把结果加到结果集里
class Solution { private List<List<Integer>> res = null; Solution() { res = new ArrayList(); } public List<List<Integer>> subsets(int[] nums) { //初始层数是0 dfs(nums, new LinkedList(), 0); return res; } public void dfs(int[] nums, LinkedList<Integer> curRes, int curLayer) { // 叶子节点的值是最后的结果 if (curLayer == nums.length) { res.add(new ArrayList(curRes)); return; } // 在每个数字有两种选择: curRes.addLast(nums[curLayer]); // 1.有自己这个数字 dfs(nums, curRes, curLayer+1); // 回溯 curRes.removeLast(); // 2.没自己这个数字 dfs(nums, curRes, curLayer+1); } }
2.每个节点可选的数字只有 nums[起始坐标~末尾]:
0 1 2 3 4 5 6 ......
1 2 3 4 5 6 7 .......
2 3 4 5 6 7 8 ......
根节点的起始坐标初始是0,每个节点的起始坐标都是:
- 它的左边节点的起始坐标+1(每层第一个节点没有左边节点,不符合这条)
- 它的父节点的起始坐标+1
如下面这样
0 1 2 3 4 5 6 ......
1 2 3 4 5 6 7 .......
2 3 4 5 6 7 8 ......
然后最终 所有节点都是结果,每次dfs都要把当前放入结果
class Solution { private List<List<Integer>> res = null; Solution() { res = new ArrayList(); } public List<List<Integer>> subsets(int[] nums) { //初始起始坐标是0 dfs(nums, new LinkedList(), 0); return res; } public void dfs(int[] nums, LinkedList<Integer> curRes, int startIndex) { // 每个节点的值都是最终的结果 res.add(new ArrayList(curRes)); // 每个节点有 起始坐标~nums.length 这么些选择 for (int i=startIndex;i<nums.length;i++) { curRes.addLast(nums[i]); // 下一层的起始坐标是现在的起始数字再加1 dfs(nums, curRes, i+1); // 回溯 curRes.removeLast(); } } }
77. 组合
根节点的起始数字是1,每个节点的起始数字都是:
- 它的左边节点的起始数字+1(每层第一个节点没有左边节点,不符合这条)
- 它的父节点的起始数字+1
如下面这样
1 2 3 4 5 6 ......
2 3 4 5 6 7 .......
3 4 5 6 7 8 ......
类似于上面子集问题的解法2,但是子集问题是所有节点都是答案,而这个组合问题只有叶子节点是答案
class Solution { List<List<Integer>> res = null; Solution() { res = new ArrayList(); } public List<List<Integer>> combine(int n, int k) { // 起始层数是0,起始数字是1 dfs(n, k, new LinkedList(), 0, 1); return res; } public void dfs(int n, int k, LinkedList<Integer> curRes, int curLayer, int startIndex) { // 一共有 k 层,所有的叶子节点是答案 // 很类似于子集问题,但子集问题所有的节点都是答案 if (curLayer == k) { res.add(new ArrayList(curRes)); return; } // 每个节点的选择是从 startIndex...n for (int i=startIndex;i<=n;i++) { curRes.addLast(i); // 层数+1,startIndex 是 i+1 dfs(n,k,curRes,curLayer+1,i+1); curRes.removeLast(); } } }
39. 组合总和
类似于上面的组合,但是不是固定长度的,所以不只有叶子节点,而是所有的节点都有可能是结果
还有添加到结果的时候需要额外判断是否等于 target
以及如果已经大于 target ,要进行剪枝,都是正数,再往下加还是大于了
另外还有一点是组合中数字可以重复
- 虽然可以有重复的,但是 1,3,3 还是等价于 3,1,3 即 3,1 等价于 1,3。即还是要避免这种情况。
- 因为可以有重复的,所以下一层的 startIndex 是 i 而不是 i+1
class Solution { List<List<Integer>> res = null; Solution() { res = new ArrayList(); } public List<List<Integer>> combinationSum(int[] candidates, int target) { dfs(candidates, target, new LinkedList(), 0, 0); return res; } private void dfs(int[] candidates, int target, LinkedList<Integer> curRes, int startIndex, int curSum) { if (curSum == target) { res.add(new ArrayList(curRes)); return; } else if (curSum > target) { // 由于都是正数,已经超过了。再往下加还是超过,所以先剪枝 return; } for (int i=startIndex;i<candidates.length;i++) { curRes.addLast(candidates[i]); // 虽然可以有重复的,但是 1,3,3 还是等价于 3,1,3 即 3,1 等价于 1,3 // 因为可以有重复的, 所以下一层 startIndex 是 i 而不是 i+1 dfs(candidates, target, curRes, i, curSum+candidates[i]); curRes.removeLast(); } } }
79. 单词搜索
进行 dfs
- dfs 的起点是什么?
- 棋盘上所有元素值为 word.charAt(0) 的元素
- 除了board和dfs到的当前元素i,j,还需要什么变量?
- 由于同一个单元格里的字母不允许被重复使用,所以需要一个 boolean[][] visited 访问数组,来标志一次dfs中,这个元素有没有被访问过(初始false,访问过了标为true)
- 需要一个变量k来表示前面已经完成了word从0到k-1的字母,现在需要寻找的是word.charAt(k)的字母
- 怎样判断这一步的dfs不是有效的?
- 超过了board边界
- 已经被访问过
- 不等于当前期望的字符 board[i][j] != word.charAt(k)
- 怎么判断当前dfs已经找到棋盘上等于word的路径?
- 当 k==word.length 时
class Solution { Boolean res = false; Solution() { res = false; } public boolean exist(char[][] board, String word) { // 从那些和 word 第一个字符一样的元素开始 dfs for(int i=0;i<board.length;i++) { for(int j=0;j<board[0].length;j++) { if(word.charAt(0) == board[i][j]) { dfs(board, getInitVisited(board), word, i, j, 0); // 每次 dfs 完都判断一下,如果已经是true了,可以提前退出 if (res) { break; } } } } return res; } private void dfs(char[][] board, boolean[][] visited, String word, int i, int j, int k) { // 不是有效的,直接返回 if (!isValid(board, visited, word, i, j, k)) { return; } // 到最后一个字符了,与word的相等, 就说明获得了结果,可以返回了 if (k == word.length()-1) { res = true; return; } visited[i][j] = true; dfs(board, visited, word, i-1, j, k+1); dfs(board, visited, word, i+1, j, k+1); dfs(board, visited, word, i, j-1, k+1); dfs(board, visited, word, i, j+1, k+1); // 回溯 visited[i][j] = false; } private boolean isValid(char[][] board, boolean[][] visited, String word,int i, int j, int k) { // 1.超出边界 if (i<0 || i>=board.length) { return false; } else if (j<0 || j>=board[0].length) { return false; } // 2.已经参观过 else if (visited[i][j]) { return false; } // 3.不是期望的那个字符 else if (word.charAt(k) != board[i][j]) { return false; } return true; } private boolean[][] getInitVisited(char[][] board) { boolean[][] visited = new boolean[board.length][board[0].length]; for (int i=0;i<board.length;i++) { for (int j=0;j<board[0].length;j++) { visited[i][j] = false; } } return visited; } }
131. 分割回文串
有点像上面的组合
- 起始位置是上一层的结束位置 +1
- 判断是 回文串 之后,再往下 dfs
- 每一层是 起始位置~结束位置 遍历
- 当起始位置==s.length() 时添加结果,退出循环
由于每个节点都要判断是否是回文串,所以可以先对整个字符串求出 dp :
boolean dp[i][j] 表示字符串从 i 到 j 是否是回文串:
- if i==j dp[i][j]=true
- else if s.charAt(i) != s.charAt(j) dp[i][j]=false;
- else if j-i==1(如"aa") dp[i][j]=true
- else dp[i][j]=dp[i+1][j-1]
dp[i][j]=dp[i+1][j-1] 由于需要从左下角的元素推出现在的元素
所以遍历应该是从左下到右上——从下到上&从左到右
即从下到上: i 从 n-1 到 0
从左到右(又j>i):j 从 i 到 n
class Solution { List<List<String>> res = null; Solution() { res = new ArrayList(); } public List<List<String>> partition(String s) { boolean[][] dp = getDP(s); dfs(s, dp, new LinkedList(), 0); return res; } private void dfs(String s, boolean[][] dp, LinkedList<String> curRes, int startIndex) { // startIndex 越界,添加结果集 if (startIndex==s.length()) { res.add(new ArrayList(curRes)); } for (int j0=startIndex;j0<s.length();j0++) { //substring 方法i,j不包含j,而其它地方包括dp认为的i,j都是包括j的。 // 所以这里 substring 的时候要 j0+1 String substr = s.substring(startIndex, j0+1); // 是回文串才往下 // 和八皇后一样,这里不能在前面循环外面 return,而是符合条件才往下。因外外面没有 j0? if (dp[startIndex][j0]) { curRes.addLast(substr); // 如 aabcb, startIndex是0,j0现在等于1,那么已经分出来了是回文的aa // 那么后面就要继续分bcb,即startIndex是j+1,里面循环j+1到length分bcb dfs(s, dp, curRes, j0+1); curRes.removeLast(); } } } private boolean[][] getDP(String s) { int n=s.length(); boolean[][] dp = new boolean[n][n]; // dp[i][j] s的i到j是否是回文 // dp[i][j] = dp[i+1][j-1]; // 需要通过左下角的结果推出来,所以遍历顺序应该是从左下到右上 // i从下到上 for (int i=n-1;i>=0;i--) { // 由于是从i到j,所以j永远是大于i的,所以j从i开始往上增 // j从左到右 for (int j=i;j<n;j++) { if (i==j) { dp[i][j] = true; } else if (s.charAt(i) != s.charAt(j)) { dp[i][j] = false; } // 只有两个元素时,而且此时已经通过了上面的s.charAt(i) != s.charAt(j) // 举例,如 aa else if (j-i==1) { dp[i][j] = true; } else { // 需要通过左下角的结果推到出来 // 所以 dp 遍历的顺序应该是从左下到右上 dp[i][j] = dp[i+1][j-1]; } } } return dp; } }
class Solution { List<String> res = null; public Solution() { res = new ArrayList(); } public List<String> restoreIpAddresses(String s) { dfs(s, new LinkedList(), 0); return res; } private void dfs(String s, LinkedList<String> curRes, int startIndex) { // 起始下标超了(说明已经全部划分完了),并且当前结果中已有4个分段 if (startIndex == s.length() && curRes.size() == 4) { // 拼接结果 StringBuilder sb = new StringBuilder(); for (String ss : curRes) { sb.append(ss).append("."); } res.add(sb.substring(0, sb.length()-1).toString()); return; } // 前面结束了符合结果的加进去了,这些结束了却还没符合结果的,直接返回 if (curRes.size() >= 4 || startIndex == s.length()) { return; } // end=startIndex 开始 for (int end=startIndex;end<s.length();end++) { // subString 的第二个参数不包括结束字符,所以 end 要+1 String subs = s.substring(startIndex, end+1); // 如果当前划分满足 0~255 if (isIpSeg(subs)) { curRes.addLast(subs); // 进行递归,下次开始下标是 endIndex dfs(s, curRes, end+1); // 回溯,恢复现场 curRes.removeLast(); } } } private boolean isIpSeg(String s) { // 长度不符合,不用转换成数字,直接就先返回false if (s.length()<1 || s.length() > 3) { return false; } // 以0开头的非 0,也不符合,如 02, 023 if (s.length() > 1 && s.charAt(0) == '0') { return false; } // 长度==3,但是开头数字大于2,还是先返回false if (s.length()==3 && s.charAt(0) - '0' > 2) { return false; } Integer a = Integer.valueOf(s); if (a>=0 && a<=255) { return true; } return false; } }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器