剑指offer(三):搜索与回溯算法
题目一:矩阵中的路径
解题思路:
本问题是典型的矩阵搜索问题,可使用 深度优先搜索(DFS)+ 剪枝 解决。
- 深度优先搜索: 可以理解为暴力法遍历矩阵中所有字符串可能性。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。
- 剪枝: 在搜索中,遇到这条路不可能和目标字符串匹配成功 的情况(例如:此矩阵元素和目标字符不同、此元素已被访问),则应立即返回,称之为可行性剪枝
DFS 解析:
class Solution {
public boolean exist(char[][] board, String word) {
char[] words = word.toCharArray();
for(int i = 0; i < board.length; i++) {
for(int j = 0; j < board[0].length; j++) {
if(dfs(board, words, i, j, 0)) return true;
}
}
return false;
}
boolean dfs(char[][] board, char[] word, int i, int j, int k) {
if(i >= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != word[k]) return false;
if(k == word.length - 1) return true;
board[i][j] = '\0';
boolean res = dfs(board, word, i + 1, j, k + 1) || dfs(board, word, i - 1, j, k + 1) ||
dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i , j - 1, k + 1);
board[i][j] = word[k];
return res;
}
}
dfs怎么写:
- 遍历每个起点
- 进了dfs首先判断合不合理,不合理就是false
- 判断是否到了终点,是就返回true
- 没有到终点,继续深入dfs, dfs的时候按照给定的方向,一般有两种pattern
(一是向四方向扩散,而是向八方向扩散)深入回来以后,如果是true直接返回上一层true,否则继续扩散 - 扩散完还没解,那就返回false,让上一层向其他方向扩散,开始新的dfs
题目二:机器人的运动范围
深度优先搜索:
class Solution {
public int movingCount(int m, int n, int k) {
boolean[][] visited = new boolean[m][n];
int res = 0;
Queue<int[]> queue= new LinkedList<int[]>();
queue.add(new int[] { 0, 0, 0, 0 });
while(queue.size() > 0) {
int[] x = queue.poll();
int i = x[0], j = x[1], si = x[2], sj = x[3];
if(i >= m || j >= n || k < si + sj || visited[i][j]) continue;
visited[i][j] = true;
res ++;
queue.add(new int[] { i + 1, j, (i + 1) % 10 != 0 ? si + 1 : si - 8, sj });
queue.add(new int[] { i, j + 1, si, (j + 1) % 10 != 0 ? sj + 1 : sj - 8 });
}
return res;
}
}
广度优先搜索:
-
BFS/DFS : 两者目标都是遍历整个矩阵,不同点在于搜索顺序不同。DFS 是朝一个方向走到底,再回退,以此类推;BFS 则是按照“平推”的方式向前搜索。
-
BFS 实现: 通常利用队列实现广度优先遍历。
算法解析:
- 初始化: 将机器人初始点 (0, 0) 加入队列 queue ;
- 迭代终止条件: queue 为空。代表已遍历完所有可达解。
- 迭代工作:
- 单元格出队: 将队首单元格的 索引、数位和 弹出,作为当前搜索单元格。
- 判断是否跳过: 若 ① 行列索引越界 或 ② 数位和超出目标值 k 或 ③ 当前元素已访问过 时,执行 continue
- 标记当前单元格 :将单元格索引 (i, j) 存入 Set visited 中,代表此单元格 已被访问过 。
- 单元格入队: 将当前元素的 下方、右方 单元格的 索引、数位和 加入 queue 。
- 返回值: Set visited 的长度 len(visited) ,即可达解的数量
class Solution {
public int movingCount(int m, int n, int k) {
boolean[][] visited = new boolean[m][n];
int res = 0;
//使用队列来辅助实现
Queue<int[]> queue= new LinkedList<int[]>();
queue.add(new int[] { 0, 0, 0, 0 });
while(queue.size() > 0) {
int[] x = queue.poll();
int i = x[0], j = x[1], si = x[2], sj = x[3];
if(i >= m || j >= n || k < si + sj || visited[i][j]) continue;
visited[i][j] = true;
res ++;
queue.add(new int[] { i + 1, j, (i + 1) % 10 != 0 ? si + 1 : si - 8, sj });
queue.add(new int[] { i, j + 1, si, (j + 1) % 10 != 0 ? sj + 1 : sj - 8 });
}
return res;
}
}
重要思想:
数位之和计算:
(x + 1) % 10 != 0 ? s_x + 1 : s_x - 8;
题目三:树的子结构
public class p26 {
public boolean isSubStructure(TreeNode A, TreeNode B) {
return (A!=null&&B!=null)&&(recur(A,B)||isSubStructure(A.left,B)||isSubStructure(A.right,B));
}
// 1、该函数功能是实现对两个树结构进行比较,确定B的结构是否在A中;
boolean recur(TreeNode A, TreeNode B){
// 2、寻找递归的结束条件
//当节点B为空时。说明已经访问到了B的所有子结构,此时为true
if (B==null)return true;
//当节点A为空时,说明已经访问到了末尾,此时还在比较,说明已经失败返回flase
//当节点A和B的值不相同
if (A==null||A.val == B.val)return false;
//3、找出函数的等价关系式
return recur(A.left, B.left)&&recur(A.right, B.right);
}
}
题目四:二叉树的镜像
//方法一:借用递归来进行操作
public TreeNode mirrorTree(TreeNode root) {
//1、终止条件,当节点为空时,则返回null
if (root==null)return null;
//2、递推工作,就是设计中间值交换两个节点
TreeNode tem;
// 答案更加精简一些
// TreeNode tmp = root.left;
// root.left = mirrorTree(root.right);
// root.right = mirrorTree(tmp);
if (root.left!=null||root.right!=null){
tem = root.left;
root.left = root.right;
root.right = tem;
if (root.left!=null)mirrorTree(root.left);
if (root.right!=null)mirrorTree(root.right);
}
//3、返回当前节点root;
return root;
//方法二:使用辅助栈来进行操作
class Solution {
public TreeNode mirrorTree(TreeNode root) {
if(root == null) return null;
Stack<TreeNode> stack = new Stack<>() {{ add(root); }};
while(!stack.isEmpty()) {
TreeNode node = stack.pop();
if(node.left != null) stack.add(node.left);
if(node.right != null) stack.add(node.right);
TreeNode tmp = node.left;
node.left = node.right;
node.right = tmp;
}
return root;
}
}
题目五:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
return root==null||recur(root.left,root.right);
}
//1、函数功能,实现对数两边是否对称进行判别
boolean recur(TreeNode A, TreeNode B){
//2、找出递归结束的条件
//2.1、终止条件是最终两边同时为null
if (A==null&&B==null)return true;
//2.2、出现不同时为null,或者当前比较的两个值不相同
if (A==null||B==null||A.val!=B.val)return false;
//3、找出函数的等价关系式
return recur(A.left,B.right)&&recur(A.right,B.left);
}
}
题目六:从上到下打印二叉树
先序遍历:同一层的节点按照从左到右的顺序打印
//借助队列来进行遍历
public int[] levelOrder(TreeNode root) {
if (root == null)return null;
ArrayList<Integer> list = new ArrayList<>();
Queue<TreeNode> que = new LinkedList<>();
que.add(root);
while (!que.isEmpty()){
TreeNode tem = new TreeNode(que.poll().val);
list.add(tem.val);
if (tem.left!=null)que.add(tem.left);
if (tem.right!=null)que.add(tem.right);
}
int[] res = new int[list.size()];
int i = 0;
for (Integer integer : list) {
res[i++] = integer;
}
return res;
}
层序遍历:同一层的节点按照从左到右的顺序打印
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
Queue<TreeNode> que = new LinkedList<>();
List<List<Integer>> list = new ArrayList<>();
if(root!=null)que.add(root);
while (!que.isEmpty()){
List<Integer> sublist = new ArrayList<>();
//这里只能使用这种形式,如果还是原来的i++,这个时候i在不断增大,size在不断的减小,其实这样会导致出现值的错误
for (int i = que.size();i>0; i--) {
TreeNode tem = que.poll();
sublist.add(tem.val);
if (tem.left!=null)que.add(tem.left);
if (tem.right!=null)que.add(tem.right);
}
list.add(sublist);
}
return list;
}
}
层序遍历的注意点:
- 怎么解决这个当前层打印,使用循环,循环次数为当前层中元素的个数,但是要注意此时应使用i–的方式。因为这个列表是在不断变小
for (int i = que.size();i>0; i--)
之字遍历:这一层的节点按照从左到右的顺序打印,下一层方向相反
//方法一:使用双栈来进行操作
class Solution {
public static List<List<Integer>> levelOrder(TreeNode root) {
Stack<TreeNode> que = new Stack<>();
List<List<Integer>> list = new ArrayList<>();
if (root!=null)que.add(root);
//设置奇偶标志位
int j = 0;
while (!que.empty()){
//辅助栈
Stack<TreeNode> que1 = new Stack<>();
j++;
List<Integer> sublist = new ArrayList<>();
for (int i =que.size();i>0; i--) {
TreeNode tem = que.pop();
sublist.add(tem.val);
//奇数偶数层不同遍历方式
if (j%2==0){
if (tem.right!=null)que1.push(tem.right);
if (tem.left!=null)que1.push(tem.left);
}else{
if (tem.left!=null)que1.push(tem.left);
if (tem.right!=null)que1.push(tem.right);
}
}
//更新栈
que = que1;
list.add(sublist);
}
return list;
}
}
//方法二:层序遍历 + 双端队列
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
List<List<Integer>> res = new ArrayList<>();
if(root != null) queue.add(root);
while(!queue.isEmpty()) {
LinkedList<Integer> tmp = new LinkedList<>();
//在这里实现赋值,然后不断减小
for(int i = queue.size(); i > 0; i--) {
TreeNode node = queue.poll();
if(res.size() % 2 == 0) tmp.addLast(node.val);
else tmp.addFirst(node.val);
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
res.add(tmp);
}
return res;
}
}
//方法三:层序遍历 + 双端队列(奇偶层逻辑分离)
//方法四:层序遍历 + 倒序
题目七:二叉树中和为某一值的路径
//先序遍历+路径记录
class Solution {
LinkedList<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int sum) {
recur(root, sum);
return res;
}
//1、实现路径法分析和记录,递推参数为root和目标值
void recur(TreeNode root, int tar) {
//2、终止条件,当前进入的是空节点
if(root == null) return;
//3、递归工作
//3.1、将当前节点数加入路径path
path.add(root.val);
//3.2、目标值更新
tar -= root.val;
//3.3、路径记录,达到条件将此路径加入res
if(tar == 0 && root.left == null && root.right == null)
res.add(new LinkedList(path));//避免直接添加 path 对象,而是拷贝了一个 path 对象并加入到 res 。
//3.4、遍历/左右子节点
recur(root.left, tar);
recur(root.right, tar);
//3.5、向上回溯前,需要将当前节点从路径中删除
path.removeLast();
}
}
题目八:二叉搜索树与双向链表
二叉搜索树 /二叉查找树
二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
class Solution {
Node pre, head;
public Node treeToDoublyList(Node root) {
if(root == null) return null;
dfs(root);
head.left = pre;
pre.right = head;
return head;
}
//中序遍历
void dfs(Node cur) {
//递归的终止条件
if(cur == null) return;
//遍历左子树
dfs(cur.left);
//对于头节点是没有前向节点的
if(pre != null) pre.right = cur;
//更改节点的引用指向
else head = cur;
cur.left = pre;
pre = cur;
//遍历右节点
dfs(cur.right);
}
}
题目九:序列化二叉树
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 cur = que.poll();
if (cur!=null){
res.append(cur.val+",");
que.add(cur.left);
que.add(cur.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 =="[]")return null;
String[] vallist = data.substring(1,data.length()-1).split(",");
TreeNode root = new TreeNode(Integer.parseInt(vallist[0]));
Queue<TreeNode> que = new LinkedList<>();
que.add(root);
int i = 1;
while (!que.isEmpty()){
TreeNode cur = que.poll();
if (!vallist[i].equals("null")){
cur.left = new TreeNode(Integer.parseInt(vallist[i]));
que.add(cur.left);
}
i++;
if (!vallist[i].equals("null")){
cur.right = new TreeNode(Integer.parseInt(vallist[i]));
que.add(cur.right);
}
i++;
}
return root;
}
}
tips:
- 当对字符串进行修改的时候,需要使用 StringBuffer 和 StringBuilder 类,和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。
- 包装类**(Integer、Long、Byte、Double、Float、Short)**都是抽象类 Number 的子类。
- Math 的方法都被定义为 static 形式,通过 Math 类可以在主函数中直接调用。
题目十:字符串的排列
class Solution {
List<String> res = new LinkedList<>();
char[] c;
public String[] permutation(String s) {
c = s.toCharArray();
dfs(0);
return res.toArray(new String[res.size()]);
}
//1、函数功能是固定某一位元素,与其他位置的进行交换
void dfs(int x) {
//2、终止条件是x遍历到最后一个元素
if(x == c.length - 1) {
res.add(String.valueOf(c)); // 添加排列方案
return;
}
//3、递归工作
HashSet<Character> set = new HashSet<>();
for(int i = x; i < c.length; i++) {
if(set.contains(c[i])) continue; // 重复,因此剪枝
set.add(c[i]);
swap(i, x); // 交换,将 c[i] 固定在第 x 位
dfs(x + 1); // 开启固定第 x + 1 位字符
swap(i, x); // 恢复交换
}
}
void swap(int a, int b) {
char tmp = c[a];
c[a] = c[b];
c[b] = tmp;
}
}
tips:
- HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合。HashSet 允许有 null 值。HashSet 是无序的,即不会记录插入的顺序。
题目十一:二叉搜索树的第 k 大节点
//过于笨重的方法
ArrayList<Integer> list = new ArrayList<>();
public int kthLargest(TreeNode root, int k) {
dfs(root);
Collections.sort(list);
return list.get(list.size()-1+k);
}
void dfs(TreeNode node){
if (node==null)return;
if (node.left!=null)dfs(node.left);
list.add(node.val);
if (node.right!=null)dfs(node.right);
}
}
解题思路:
二叉搜索树的中序遍历为递增序列。根据此性质,易得二叉搜索树的 中序遍历倒序 为 递减序列 。求 “二叉搜索树第 k 大的节点” 可转化为求 “此树的中序遍历倒序的第 k个节点”
递增数列:
// 打印中序遍历
void dfs(TreeNode root) {
if(root == null) return;
dfs(root.left); // 左
System.out.println(root.val); // 根
dfs(root.right); // 右
}
递减数列:
// 打印中序遍历倒序
void dfs(TreeNode root) {
if(root == null) return;
dfs(root.right); // 右
System.out.println(root.val); // 根
dfs(root.left); // 左
}
//题解
class Solution {
int res,k;
public int kthLargest(TreeNode root, int k) {
//用来访问本类的成员方法
this.k = k;
dfs(root);
return res;
}
void dfs(TreeNode node){
if (node==null)return;
dfs(node.right);
if (k==0)return;
if (--k == 0)res = node.val;
dfs(node.left);
}
}
题目十二: 二叉树的深度
//方法一:通过层序遍历确定有多少层
class Solution {
public int maxDepth(TreeNode root) {
int depth = 0 ;
Queue<TreeNode> que = new LinkedList<>();
if (root!=null)que.add(root);
while (!que.isEmpty()){
depth++;
for (int i = que.size(); i > 0; i--) {
TreeNode cur = que.poll();
if (cur.left!=null)que.add(cur.left);
if (cur.right!=null)que.add(cur.right);
}
}
return depth;
}
}
//方法二:通过后序遍历来确定层数递归
//主要是突破点就是一个树的最大层等于左子树和右子树的最大值加1
//1、递归的功能,得到某个树的层数
public int maxDepth(TreeNode root) {
//2、递归的终止条件是子节点为空此时返回0;
if (root==null)return 0;
//3、递归条件类似于斐波那契数列,层数和左右层数有关
return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
}
题目十二:平衡二叉树
//方法一:后序遍历进行判断
public boolean isBalanced(TreeNode root) {
if (root==null)return true;
int ldepth = depth(root.left);
int rdepth = depth(root.right);
int def = ldepth - rdepth;
if (def>1||def<-1)return false;
return isBalanced(root.right)&&isBalanced(root.left);
}
int depth(TreeNode root){
if (root==null)return 0;
return Math.max(depth(root.left),depth(root.right))+1;
}
//方法二:后序遍历加枝剪
class Solution {
public boolean isBalanced(TreeNode root) {
return recur(root) != -1;
}
//1、递归功能是判断层数,当不为平衡数则返回-1;
private int recur(TreeNode root) {
//2、递归的终止条件——是空的则其深度为0,当左右子树不为平衡树时则其不为平衡树
if (root == null) return 0;
int left = recur(root.left);
if(left == -1) return -1;
int right = recur(root.right);
if(right == -1) return -1;
//3、递推返回值为层数,不为平衡树则返回-1;
return Math.abs(left - right) < 2 ? Math.max(left, right) + 1 : -1;
}
}
补充知识:
- 先序遍历:先访问根结点,然后再访问左子树,最后访问右子树
- 中序遍历:先访问左子树,中间访问根节点,最后访问右子树
- 后序遍历:先访问左子树,再访问右子树,最后访问根节点
Tip:
- 二叉搜索树:一般都是使用这个都是中序遍历
- 二叉树路径:判断是从某一个根节点到叶子节点满足某一路径使用先序遍历来进行操作
- 从底层到顶层,使用后序遍历更加方便
- 层序遍历可借助队列来操作
题目十三:求 1 + 2 + … + n
//方法一:平均计算
public int sumNums(int n) {
return (1 + n) * n / 2;
//方法二:迭代
public int sumNums(int n) {
int res = 0;
for(int i = 1; i <= n; i++)
res += i;
return res;
}
//方法三:
public int sumNums(int n) {
if(n == 1) return 1;
n += sumNums(n - 1);
return n;
}
//方法四:
class Solution {
public int sumNums(int n) {
boolean x = n > 1 && (n += sumNums(n - 1)) > 0;
return n;
}
}
题目十四:二叉搜索树的最近公共祖先
//方法一:迭代
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while(root != null) {
if(root.val < p.val && root.val < q.val) // p,q 都在 root 的右子树中
root = root.right; // 遍历至右子节点
else if(root.val > p.val && root.val > q.val) // p,q 都在 root 的左子树中
root = root.left; // 遍历至左子节点
else break;
}
return root;
}
}
//方法二:
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.right,p,q);
if(p.val<root.val&&q.val<root.val)return lowestCommonAncestor(root.left,p,q);
return root;
}
}
题目十五:二叉树的最近公共祖先
通过递归对二叉树进行先序遍历,当遇到节点 p或q时返回。从底至顶回溯,当节点 p,q 在节点 root的异侧时,节点 root 即为最近公共祖先,则向上返回 root。
//1、函数功能是判断二叉树的最近公共祖先
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//2、终止条件:
//2.1、当越过叶节点,则直接返回null
//2.2、当root等于p、q,则直接返回root;
if (root==null||root==p||root==q)return root;
TreeNode left = lowestCommonAncestor(root.left,p,q);
TreeNode right = lowestCommonAncestor(root.right,p,q);
//3、返回值
//3.1、当左右树的返回值都为空是,说明没有p、q
if (left==null||right==null)return null;
//3.2、左边为空,则说明右边有p、q则返回,右节点
if (left==null)return right;
//3.3、右边为空,则说明左边有p、q则返回,左节点
if (right==null)return left;
//3.4、左右都不为空,说明p、q分布在root的异侧
return root;
}