文章目录
- 回溯算法模板
- 77. 组合 - 中等 - 10.20
- 216. 组合总和 III - 中等 - 10.20
- 17. 电话号码的字母组合 - 中等 - 10.21
- 39. 组合总和 - 中等 - 10.22
- 剑指 Offer 12. 矩阵中的路径
- 剑指 Offer 13. 机器人的运动范围
- 剑指 Offer 34. 二叉树中和为某一值的路径
- 剑指 Offer 36. 二叉搜索树与双向链表
- 剑指 Offer 54. 二叉搜索树的第k大节点
- *剑指 Offer 55 - I. 二叉树的深度
- 剑指 Offer 55 - II. 平衡二叉树
- 剑指 Offer 64. 求1+2+…+n
- 剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
- 剑指 Offer 68 - II. 二叉树的最近公共祖先
- 剑指 Offer 37. 序列化二叉树
- 剑指 Offer 38. 字符串的排列
回溯算法模板
77. 组合 - 中等 - 10.20
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
class Solution {
//保存结果
List<List<Integer>> result = new ArrayList<>();
//路径
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
combineHelper(n,k,1);
return result;
}
/**
* 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex
* @param startIndex 用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。
*/
private void combineHelper(int n, int k, int startIndex){
//终止条件 路径长度等于k
if(path.size() == k){
result.add(new ArrayList<>(path)); //添加为一个结果
return;
}
//遍历查找
//剪枝前:n,击败57%
//剪枝后:n - (k-path.size()) + 1,击败99%
for(int i = startIndex; i <= n - (k-path.size()) + 1; i++){
path.add(i); //路径添加
combineHelper(n,k,i+1); //回溯调用,从下一个节点开始 i+1
path.removeLast(); //撤销已处理的节点
}
}
}
216. 组合总和 III - 中等 - 10.20
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
- 所有数字都是正整数。
- 解集不能包含重复的组合。
处理过程就是 path收集每次选取的元素,相当于树型结构里的边,sum来统计path里元素的总和。处理过程 和 回溯过程是一一对应的,处理有加,回溯就要有减!
剪枝操作
class Solution {
//保存结果
List<List<Integer>> result = new ArrayList<>();
//路径
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
combineHelper(k,n,1,0);
return result;
}
private void combineHelper(int k, int n, int startIndex, int sum){
//剪枝
if(sum > n){
return;
}
//终止条件 路径长度等于k
if(path.size() == k){
if(sum == n) result.add(new ArrayList<>(path)); //添加为一个结果
return;
}
//遍历查找
//剪枝前:9
//剪枝后:9 - (k-path.size()) + 1
for(int i = startIndex; i <= 9 - (k - path.size()) + 1; i++){
path.add(i); //路径添加
sum += i;
combineHelper(k,n,i+1,sum); //回溯调用,从下一个节点开始 i+1
path.removeLast(); //撤销已处理的节点
sum -= i;
}
}
}
17. 电话号码的字母组合 - 中等 - 10.21
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
class Solution {
//数字到字符串的映射
String[] str = {"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
//结果集
List<String> res = new ArrayList<>();
//路径 操作字符
StringBuilder sb = new StringBuilder();
public List<String> letterCombinations(String digits) {
//特殊处理
if(digits == null || digits.length() == 0) return res;
//调用回溯函数
backtrack(digits,0);
return res;
}
//回溯函数
void backtrack(String digits,int index){
//退出条件 如果 输入字符的长度 等于 操作字符的长度 就把操作字符添加进去,然后返回
if(digits.length() == sb.length()){
res.add(sb.toString());
return;
}
//获取当前数字对应的字符串
String val = str[digits.charAt(index) - '2'];
//遍历
for(char v: val.toCharArray()){
sb.append(v); //拼接字符
backtrack(digits,index+1); //递归调用,index+1是 使用下一个数字对应的字符串
//操作完成后删除刚才用过的字母
sb.deleteCharAt(sb.length()-1);
}
}
}
39. 组合总和 - 中等 - 10.22
给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。
candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是唯一的。
对于给定的输入,保证和为 target 的唯一组合数少于 150 个。
方法一:减法(更快)
// 方法一:减法
class Solution {
List<List<Integer>> res;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
//存放结果
res = new ArrayList<>();
//排序
Arrays.sort(candidates);
//回溯
backtrack(candidates,target,new ArrayList<>(),0);
return res;
}
//回溯
private void backtrack(int[] candidates,int remains,List<Integer> path,int start){
//如果做减法后剩下0,则把这条路径添加进来
if(remains == 0){
res.add(new ArrayList<>(path));
return;
}
//遍历
for(int i=start; i<candidates.length; i++){
//如果剩余值小于0,就返回
if(remains-candidates[i] < 0) return;
//剪枝 即去掉相同数的路径 如[2,2,3,7] 去掉第二个2。
if(i > 0 && candidates[i] == candidates[i-1]) continue;
//添加元素到路径
path.add(candidates[i]);
//回溯
backtrack(candidates, remains-candidates[i], path, i);
//找到了一个解或者 remains < 0 了,将当前数字移除,然后继续尝试
path.remove(path.size()-1);
}
}
}
方法二:加法
//方法二:加法
class Solution {
List<List<Integer>> res;
List<Integer> path;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
//存放结果
res = new ArrayList<>();
//存放路径
path = new ArrayList<>();
//排序
Arrays.sort(candidates);
//回溯
backtrack(candidates,target,0,path,0);
return res;
}
//回溯
private void backtrack(int[] candidates, int target, int sum, List<Integer> path, int start){
//如果做加法后等于target,则把这条路径添加进来
if(sum == target){
res.add(new ArrayList<>(path));
return;
}
//遍历
for(int i=start; i<candidates.length; i++){
//如果累加和大于target,就返回
if(sum > target) return;
//剪枝 即去掉相同数的路径 如[2,2,3,7] 去掉第二个2。
if(i > 0 && candidates[i] == candidates[i-1]) continue;
//添加元素到路径
path.add(candidates[i]);
//累加
sum += candidates[i];
//回溯
backtrack(candidates, target, sum, path, i);
//找到了一个解或者 remains < 0 了,将当前数字移除,然后继续尝试
path.remove(path.size()-1);
//上面加了,这里就要减回去
sum -= candidates[i];
}
}
}
剑指 Offer 12. 矩阵中的路径
解析:
class Solution {
public boolean exist(char[][] board, String word) {
//将word转换成字串数组
char[] words = word.toCharArray();
//遍历图
for(int i=0; i<board.length; i++){
for(int j=0; j<board[0].length; j++){
//如果找到了,就返回true,否则继续找
if(dfs(board,words,i,j,0)) return true;
}
}
//遍历结束还没找到返回false
return false;
}
//i,j是元素位置下标,k是传入字符串当前索引
private boolean dfs(char[][] board, char[] words, int i, int j, int k){
//判断传入参数的可行性 i 与图行数row比较,j与图列数col比较
//如果board[i][j] == word[k],则表明当前找到了对应的数,就继续执行(标记找过,继续dfs 下上右左)
if(i>=board.length || i<0 || j>=board[0].length || j<0 || board[i][j] != words[k]) return false;
// 表示找完了,每个字符都找到了
// 一开始k=0,而word.length肯定不是0,所以没找到,就执行dfs继续找。
if(k == words.length-1) return true;
// 访问过的标记空字符串,“ ”是空格 '\0'是空字符串,不一样的!
// 比如当前为A,没有标记找过,且A是word中对应元素,则此时应该找A下一个元素,假设是B,在dfs(B)的时候还是-
// ->要搜索B左边的元素(假设A在B左边),所以就是ABA(凭空多出一个A,A用了2次,不可以),如果标记为空字符串->
// 就不会有这样的问题,因为他们值不相等AB != ABA。
board[i][j] = '\0';
//顺序是 下上右左, 上面找到了对应索引的值所以k+1
boolean res = dfs(board,words,i+1,j,k+1) || dfs(board,words,i-1,j,k+1) ||
dfs(board,words,i,j+1,k+1) || dfs(board,words,i,j-1,k+1);
// 还原找过的元素,因为之后可能还会访问到(不同路径)
board[i][j] = words[k];
// 返回结果,如果false,则if(dfs(board, words, i, j, 0)) return true;不会执行,就会继续找
return res;
}
}
剑指 Offer 13. 机器人的运动范围
解析:DFS
class Solution {
public int movingCount(int m, int n, int k) {
boolean[][] visited = new boolean[m][n]; //辅助数组
return dfs(m,n,k,0,0,visited);
}
public int dfs(int m, int n, int k, int i, int j, boolean[][] visited){
//下标越界 或 行坐标和列坐标的数位之和大于k 或 标志位为false
if(i>=m || j>=n || k<getNumSum(i)+getNumSum(j) || visited[i][j]){
return 0;
}
//可以到达的格子 设置为true
visited[i][j] = true;
//结果 = 1 + 向下可达的格子数 + 向右可达的格子数
return 1 + dfs(m,n,k,i+1,j,visited) + dfs(m,n,k,i,j+1,visited);
}
//数的各个位数之和
private int getNumSum(int a){
int sum = a % 10;
int tmp = a / 10;
while(tmp > 0){
sum += tmp % 10;
tmp /= 10;
}
return sum;
}
}
剑指 Offer 34. 二叉树中和为某一值的路径
给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。
解析:回溯
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
LinkedList<List<Integer>> res = new LinkedList<>(); //结果
LinkedList<Integer> path = new LinkedList<>(); //路径
public List<List<Integer>> pathSum(TreeNode root, int target) {
recall(root,target);
return res;
}
public void recall(TreeNode root, int target){
if(root == null) return;
path.add(root.val); //单条路径添加值
target -= root.val; //做减法
//如果做减法后结果为0 并且 当前为叶子节点,就添加路径
if(target == 0 && root.left == null && root.right == null){
res.add(new LinkedList<>(path)); //添加路径
}
//回溯调用,查找左右子树
recall(root.left, target);
recall(root.right, target);
//撤销已处理的节点
path.removeLast();
}
}
剑指 Offer 36. 二叉搜索树与双向链表
剑指 Offer 36. 二叉搜索树与双向链表
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
为了让您更好地理解问题,以下面的二叉搜索树为例:
解析:
/*
// Definition for a Node.
class Node {
public int val;
public Node left;
public Node right;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val,Node _left,Node _right) {
val = _val;
left = _left;
right = _right;
}
};
*/
class Solution {
Node pre, head; //pre为前一个节点,head为头节点
public Node treeToDoublyList(Node root) {
if(root == null) return null;
dfs(root);
//经过dfs处理之后,pre就有指向了,然后进行头尾相连
head.left = pre;
pre.right = head;
//返回头节点
return head;
}
public void dfs(Node cur){
//递归结束条件
if(cur == null) return;
//左
dfs(cur.left);
//如果pre为空,就说明是第一个节点,头结点,然后用head保存头结点,用于之后的返回
if(pre == null) head = cur;
//如果pre不为空,那就说明是中间的节点。并且pre保存的是上一个节点,让上一个节点的右指针指向当前节点
else pre.right = cur;
//再让当前节点的左指针指向父节点,也就连成了双向链表
cur.left = pre;
//保存当前节点,用于下层递归创建
pre = cur;
//右
dfs(cur.right);
}
}
剑指 Offer 54. 二叉搜索树的第k大节点
解法一:先使用中序遍历存储元素,然后返回倒数第k大的节点值。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int kthLargest(TreeNode root, int k) {
if(root == null) return 0;
List<Integer> res = new ArrayList<>();
inOrder(root,res);
return res.get(res.size()-k); //获取第k大的节点值
}
//中序遍历
public void inOrder(TreeNode root, List<Integer> res){
if(root == null) return;
inOrder(root.left,res);
res.add(root.val);
inOrder(root.right,res);
}
}
优化后:遍历右根左,到倒数第k大值时就直接返回。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
private int res = 0, n = 0;
public int kthLargest(TreeNode root, int k) {
this.n = k;
inOrder(root);
return res;
}
public void inOrder(TreeNode root){
//注意这里是先遍历右子树
if(root.right != null && n > 0) inOrder(root.right);
n--; //递减
if(n == 0) { //找到倒数第k大的值
res = root.val;
return;
}
if(root.left != null && n > 0) inOrder(root.left);
}
}
*剑指 Offer 55 - I. 二叉树的深度
解析:层序遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) return 0;
Queue<TreeNode> que = new LinkedList<>();
que.offer(root);
int n = 0; //树的深度
while(!que.isEmpty()){
n++;
int len = que.size();
while(len > 0){
TreeNode node = que.poll();
if(node.left != null) que.offer(node.left);
if(node.right != null) que.offer(node.right);
len--;
}
}
return n;
}
}
解法二:递归
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) return 0;
return Math.max(maxDepth(root.left),maxDepth(root.right)) + 1;
}
}
剑指 Offer 55 - II. 平衡二叉树
解析:递归计算出左右子树的高度,然后判断是否相差大于1。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isBalanced(TreeNode root) {
return dfs(root) >= 0;
}
public int dfs(TreeNode root){
if(root == null) return 0;
int leftHeight = dfs(root.left); //左子树高度
int rightHeight = dfs(root.right); //右子树高度
if(leftHeight == -1 || rightHeight == -1 || Math.abs(leftHeight - rightHeight) > 1){ //如果左右子树高度为-1,或者两者的差值大于1,就返回-1
return -1;
}else{
return Math.max(leftHeight, rightHeight) + 1; //树的深度
}
}
}
剑指 Offer 64. 求1+2+…+n
解析:递归,短路&&
class Solution {
public int sumNums(int n) {
//如果n小于等于0,就不会执行&&后面的语句
boolean flag = n > 0 && (n += sumNums(n - 1)) > 0;
return n;
}
}
剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
解析:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) return null;
if(p.val < root.val && q.val < root.val){ //往左搜索
return lowestCommonAncestor(root.left, p, q);
}
if(p.val > root.val && q.val > root.val){ //往右搜索
return lowestCommonAncestor(root.right, p, q);
}
return root; //如果出现p,q在一左一右,就直接返回当前的根节点
}
}
剑指 Offer 68 - II. 二叉树的最近公共祖先
解析:
三种情况
1、p q 一个在左子树 一个在右子树,那么当前节点即是最近公共祖先。
2、p q 都在左子树
3、p q 都在右子树
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) return null;
//如果当前节点等于p或q,返回当前节点
if(root == p || root == q) return root;
//在左子树找
TreeNode left = lowestCommonAncestor(root.left, p, q);
//在右子树找
TreeNode right = lowestCommonAncestor(root.right, p, q);
// p q 一个在左,一个在右
if(left != null && right != null) return root;
// p q 都在左子树
if(left != null) return left;
// p q 都在右子树
if(right != null) return right;
return null;
}
}
剑指 Offer 37. 序列化二叉树
解析:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
if(root == null) return "[]";
StringBuilder res = new StringBuilder("["); //左大括号
Queue<TreeNode> que = new LinkedList<>();
que.add(root);
while(!que.isEmpty()){
TreeNode node = que.poll();
if(node != null){ //如果节点不为空
res.append(node.val + ","); //添加给节点值
que.add(node.left); //添加左孩子
que.add(node.right); //添加右孩子
}else{
res.append("null,"); //节点为空,添加空值
}
}
res.deleteCharAt(res.length()-1); //删除逗号
res.append("]"); //右大括号
return res.toString();
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if(data.equals("[]")) return null;
String[] vals = data.substring(1, data.length()-1).split(","); //根据","分割字符串
TreeNode root = new TreeNode(Integer.parseInt(vals[0])); //创建根节点
Queue<TreeNode> que = new LinkedList<>();
que.add(root);
int i=1; //下标
while(!que.isEmpty()){
TreeNode node = que.poll();
if(!vals[i].equals("null")){
node.left = new TreeNode(Integer.parseInt(vals[i])); //构建左孩子
que.add(node.left); //添加左孩子
}
i++; //移动
if(!vals[i].equals("null")){
node.right = new TreeNode(Integer.parseInt(vals[i])); //构建右孩子
que.add(node.right); //添加右孩子
}
i++; //移动
}
return root;
}
}
// Your Codec object will be instantiated and called as such:
// Codec codec = new Codec();
// codec.deserialize(codec.serialize(root));
剑指 Offer 38. 字符串的排列
解析:回溯
class Solution {
List<String> res;
StringBuilder path;
boolean[] visited;
public String[] permutation(String s) {
//空值处理
if(s.equals("")) return new String[]{};
//初始化
this.res = new ArrayList<>();
this.path = new StringBuilder();
this.visited = new boolean[s.length()];
//字符串转为字符数组
char[] ch = s.toCharArray();
//排序,方便处理重复的字符
Arrays.sort(ch);
backtrack(ch,0);
return res.toArray(new String[0]);
}
public void backtrack(char[] ch, int depth){
if(depth == ch.length){
res.add(path.toString());
return;
}
//遍历字符数组
for(int i=0; i<ch.length; i++){
if(visited[i]) continue; //跳过
if(i > 0 && ch[i-1] == ch[i] && !visited[i-1]) continue; //重复字串的处理
path.append(ch[i]); //添加字符
visited[i] = true; //标记已访问
backtrack(ch, depth+1); //回溯,深度+1
visited[i] = false; //重置访问
path.deleteCharAt(path.length()-1); //删除刚添加的字符,重新尝试
}
}
}