文章目录
- 一、 检查两棵二叉树是否相同
- 二、 另一棵二叉树的子树
- 三、 二叉树的构建及遍历
- 四、序列化二叉树和反序列化二叉树(难)
- 五、二叉树创建字符串
- 六、 二叉树前序非递归遍历实现
- 七、 二叉树中序非递归遍历实现
- 八、 二叉树后序非递归遍历实现
- 九、二叉搜索树中找到两个结点的最近公共祖先
- 十、二叉树中找到两个结点的最近公共祖先
- 总结
提示:本人是正在努力进步的小菜鸟,不喜勿喷~,如有大佬发现某题有更妙的解法和思路欢迎提出讨论~
一、 检查两棵二叉树是否相同
OJ链接
📌📌📌题目描述:
给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的
返回值: boolean
📌📌📌解题思路:
两棵树同时递归, 每遍历到一个结点就判断
1️⃣当 p, q, 一个为空一个不为空时, 返回 false
2️⃣当 p, q, 的val值不同时, 返回 false
3️⃣当 p, q, 同时遍历到空结点时, 说明 p, q 的所有祖先点相同, 返回 true
⚠️⚠️⚠️注意:
不能 p, q, 同时不为空且值相同时就返回 true, 因为有可能这个结点暂时相同, 但子树的结点还不能相同, 一定是当 p, q, 同时遍历到空结点时, 才说明这一路的结点没有返回 false, 那么此时 p, q 的所有祖先点相同, 才能返回true
public boolean isSameTree(TreeNode p, TreeNode q) {
// p, q, 有其中一个不为空
if ( (p == null && q != null ) || (p != null && q == null) ) {
return false;
}
// val不相同的情况
if (p.val != q.val) {
return false;
}
// p, q, 都为空的情况
if (p == null && q == null) {
return true;
}
// 左右子树都要判断
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
二、 另一棵二叉树的子树
OJ链接
📌📌📌题目描述:
给你两棵二叉树 root 和 subRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false
二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树
返回值: boolean
📌📌📌解题思路:
上一题就是 判断两棵二叉树是否相同, 判断一棵树是否是另一颗的子树, 只需要遍历以 root 为根
的树的每一个结点
, 和以 subRoot 为根
的树, 只要是相同的树则说明 root 这棵树中有 subRoot 这棵树, 所以上一题的代码可以直接拿来用
题目描述中说 tree 也可以看做它自身的一棵子树 , 说明: 如果给定的 root 和 subRoot 本身就是相同的树, 也符合题目要求, 所以首先要对给定的 root 和 subRoot 判断是否为同一棵树
然后利用子问题思想: 让 root 的左右子树递归判断是否存在一个结点和 subRoot 是相同的树
⚠️⚠️⚠️注意:
虽然题目给定两个树的结点范围是[1,2000], 说明两棵树都不为空, 但是不能省略代码中对 root 和 sunRoot 的判空, 因为:
1️⃣在上一题分析过, 当 p, q, 同时遍历到空结点时, 说明 p, q 的所有祖先点相同, 返回 true, 所以 root 和subRoot 都需要遍历到空结点
2️⃣还有一种情况是 root 这棵树的所有结点都遍历完了也不满足和 subRoot 是同一棵树, 所以要返回 false
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
// 当root遍历到null还没找到和subRoot相同的树
if(root == null) {
return false;
}
if(isSameTree(root,subRoot)) {
return true;
}
// 遍历,左右子树有一边满足条件即可
return isSubtree(root.left,subRoot) || isSubtree(root.right,subRoot);
}
private boolean isSameTree(TreeNode root, TreeNode subRoot) {
if(root == null && subRoot == null) {
return true;
}
if((root != null && subRoot == null) || (root == null && subRoot != null)) {
return false;
}
if(root.val != subRoot.val) {
return false;
}
return isSameTree(root.left,subRoot.left) && isSameTree(root.right,subRoot.right);
}
三、 二叉树的构建及遍历
OJ链接
📌📌📌题目描述:
编一个程序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储), 例如如下的先序遍历字符串: ABC##DE#G##F### 其中 “#” 表示的是空格,空格字符代表空树。建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。
返回值: void
📌📌📌解题思路:
题目分成两部分: 利用字符串构造二叉树+ 中序遍历
中序遍历很简单, 主要是如何利用字符串构造二叉树, 根据题目描述, 给定的字符串是前序遍历, 所以我们利用字符串构建二叉树时也应该使用前序遍历的顺序
需要一个变量 i 表示下标, 当 i 为 # 时表示 null, null 能 new 出结点吗? 当然不能, i++ 即可, 当 i 不为 # 时, new 出一个结点, 然后别忘了也要 i++
以根节点为例: 根节点不为空: new了一个结点, i++ , 然后让根结点往左递归, 链接下一个结点
(每一个结点都是这样的方式), 直到 i 访问到 # , 栈帧空间一路返回, 链接每一个结点的右子树
最终返回这棵树的根节点, 以便进行中序遍历
⚠️⚠️⚠️注意:
题目设置了多组输入的情况, 所以要利用 Scanner in = new Scanner(System.in)
处理多组输入
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 注意 hasNext 和 hasNextLine 的区别
while (in.hasNextLine()) { // 注意 while 处理多个 case
String str = in.nextLine();
TreeNode root = creatTree(str);
inOrder(root);
}
}
public static int i = 0;
private static TreeNode creatTree(String str) {
TreeNode root = null;
if(str.charAt(i) != '#') {
root = new TreeNode(str.charAt(i));
i++;
root.left = creatTree(str);
root.right = creatTree(str);
}else {
// 当 i 遍历到 # 时
i++;
}
return root;
}
private static void inOrder(TreeNode root){
if(root == null) {
return;
}
inOrder(root.left);
System.out.print(root.val + " ");
inOrder(root.right);
}
四、序列化二叉树和反序列化二叉树(难)
OJ链接
📌📌📌题目描述:
请实现两个函数,分别用来序列化和反序列化二叉树,不对序列化之后的字符串进行约束,但要求能够根据序列化之后的字符串重新构造出一棵与原二叉树相同的树
二叉树的序列化(Serialize)是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树等遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过 某种符号表示空节点(#)
// 简单来说: 序列化: 二叉树 => 字符串
二叉树的反序列化(Deserialize)是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树
// 简单来说: 反序列化: 字符串 => 二叉树
返回值: 序列化返回String 和 反序列化TreeNode
📌📌📌解题思路:
反序列化: 以前序遍历的方式实现对二叉树的反序列化和上一题思路基本一致
序列化:
返回值要求是String, 系统会检查你的字符串格式, 需要有 {} 括起来每一个结点的val值, 并且有逗号分隔
首先append一个'{'
, 同样使用前序遍历的方式, 只需对每个结点判断:
如果结点不为空, 直接append该结点val值和逗号
如果结点为空, append 井号和逗号
无论哪种情况, 当最后一次append之后会多余一个逗号, 要利用deleteCharAt()
方法删除这个逗号, 最后再appen一个'}'
⚠️⚠️⚠️注意:
反序列化方法的核心是要找到字符串中的有效字符, 也就是数字字符, 但题目说明每一个结点的val值范围: [0,150]
, 所以有效数字字符可能不止一位,
如果 i 下标访问到有效(数字)字符, 需要找到下一个逗号之前的 所有数字字符, 得到的是一个字符串, 要把这个字符串转化成 int 类型, 作为val值new出结点
所以在反序列化中注意以下几个方法的使用
charAt()
: 参数是 int 类型, 表示下标. 返回值是 char 类型
用于在字符串中访问任意下标(从 0 开始)
的值, 小心越界 !!!
subsrting()
: 参数是两个 int 类型, 表示开始和结束下标. 返回值是 String 类型
用于截取字符串, 注意范围是左开右闭
Integer.parseInt()
: 参数是 String 类型的字符串, 返回值是 int 类型
用于把纯数字字符串转化成数字, 参数中不能有非数字字符,否则会报NumberFormatException
异常
要经常考虑使用subsrting()
方法的情况, 使用该方法需要确定开始下标和结束下标, 所以 把寻找结束位置下标封装成一个方法: findEndIndex()
// 序列化: 二叉树 => 字符串
public static String serialize(TreeNode root) {
// 先new一个StringBuffer对象, 返回值是String, 最后要toString
StringBuffer sb = new StringBuffer("{");
// 判空
if (root == null) {
sb.append('}');
return sb.toString();
}
// 利用前序遍历 遍历结点, append每一个结点val值+逗号
preOrder(root, sb);
sb.deleteCharAt(sb.length() - 1);
// 删除最后一个逗号,再append一个}
sb.append('}');
return sb.toString();
}
private static void preOrder(TreeNode root, StringBuffer str) {
if (root == null) {
str.append('#').append(',');
return;
}
str.append(root.val).append(',');
preOrder(root.left, str);
preOrder(root.right, str);
}
// 反序列化: 字符串 => 二叉树
public static int i = 1; // 成员属性, 用来遍历字符串
public static TreeNode Deserialize(String str) {
TreeNode root = null;
int len = str.length();
// 如果字符串为空:"{}" => 1下标的字符是'}', 返回null
if (str.charAt(i) == '}') {
return root;
}
// 如果i下标的字符不是'#'
if (str.charAt(i) != '#') {
int startIndex = i; // 开始下标
int endIndex = findEndIndex(str, len); // 结束下标
// substring : [start, end) 截取str字符串中的数字字符串
String ret = str.substring(startIndex, endIndex);
// 转化成整形数字, new出结点
root = new TreeNode(Integer.parseInt(ret));
i++;
root.left = Deserialize(str);
root.right = Deserialize(str);
} else {
// 如果i下标的字符是'#', i往后走到下一个有效字符
i += 2;
}
return root;
}
// 寻找end下标
private static int findEndIndex(String str, int length) {
while (i < length && str.charAt(i) != ',') {
if (str.charAt(i) != '}') {
i++;
} else {
break;
}
}
return i;
}
// 反序列化
public static TreeNode Deserialize(String str) {
TreeNode root = null;
int i = 1;
int len = str.length();
if (str.charAt(i) == '}') {
return root;
} else {
// new出字符串转化为数字的 结点
String val = getStringVal(str,len);
root = new TreeNode(Integer.parseInt(val));
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (i < len - 1) {
TreeNode top = queue.poll();
if(top != null) {
String val = getStringVal(str, len);
if (val.charAt(0) == '#') {
top.left = null;
} else {
top.left = new TreeNode(Integer.parseInt(val));
queue.offer(top.left);
}
i++;
if(val.charAt(0) == '#') {
top.right = null;
} else {
top.right = new TreeNode(Integer.parseInt(val));
queue.offer(top.right);
}
i++;
}
}
return root;
}
五、二叉树创建字符串
OJ链接
📌📌📌题目描述:
给你二叉树的根节点 root ,请你采用前序遍历的方式,将二叉树转化为一个由括号和整数组成的字符串,返回构造出的字符串。
空节点使用一对空括号对 “()” 表示,转化后需要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空括号对。
返回值: String
📌📌📌解题思路:
根据前序遍历的顺序, 结合上图两个示例, 写出相应操作代码即可
⚠️⚠️⚠️注意:
树中节点的数目范围是 [1, 104]
, 保证树不为空, 所以可以省去对 root 的判空
也不比担心 root 会遍历到空结点, 因为每次递归前都会对 root 的 left 和 right 进行判断
public String tree2str(TreeNode root) {
StringBuilder sb = new StringBuilder();
convertedString(sb, root);
return sb.toString();
}
private void convertedString(StringBuilder sb, TreeNode root){
sb.append(root.val);
if(root.left != null) {
sb.append("(");
convertedString(sb, root.left);
sb.append(")");
}else {
// 左子树为空
if(root.right != null){
// 如果有右子树
sb.append("()");
}else {
// 如果没有右子树
return;
}
}
if(root.right != null) {
sb.append("(");
convertedString(sb, root.right);
sb.append(")");
}else {
return;
}
}
六、 二叉树前序非递归遍历实现
OJ链接
📌📌📌题目描述:
给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
返回值: List<Integer>
📌📌📌解题思路:
递归的执行时的函数栈帧就是递出去再回退, 如果要模拟递归, 一般情况下都是使用栈, 前序遍历(根, 左, 右)
二叉树的非递归实现就是, 没有递归就只能用利用while循环
在循环中遍历, 对于每个结点来说, 先把它访问了, root 不能存放其他结点的地址, 需要一个跑腿的变量 cur ,令 cur = root.left
, 然后如果 cur 不为空就让 cur 入栈并访问, 再令 cur 往左走 当 cur 为空时退出循环
说明没有左子树了, 此时栈顶数据就是当前空结点的父节点(根) , 所以此时出栈栈顶元素, 就可以访问到根的右孩子结点
这样只是访问了第一个"右" , 那么还有很多个"右"如何范围呢, 当第一个"右"访问之后, 只说明当前的子树
已经前序遍历完了, 栈还不为空
, 还需要继续访问靠近根的那棵树的"右", 和递归类似, 还是上述的过程, 所以还需要加一个外循环, 栈为空时
才说明最后一个"右"也访问完了
⚠️⚠️⚠️注意:
内循环的条件就是cur != null , 因为 cur 总是往左走, 要把"左"全部访问完才能访问"右", 当内循环退出时, 就令 cur = 出栈的结点的 right ,那么此时:
1️⃣cur == null, 继续让栈顶数据出栈, cur = 出栈的结点的 right , 访问其他(可能存在)的"右" , 直到栈为空为止
2️⃣cur != null, 继续入栈, 执行内循环, 所以外循环还有一个条件是 cur != null
上述两判断条件的逻辑关系是 || 而不是&&, 原因:
如果是一棵树没有左子树, 只有右子树的
情况, 当 cur 遍历到根的左, 为空, 出栈根节点, 去访问根的右, 此时右不为空但栈为空
, 只满足一个条件, 此时需要让cur执行内循环
综上, 外循环判断条件是cur != null || !stack.isEmpty()
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> ret = new ArrayList<>();
if(root == null) {
return ret;
}
// new一个顺序栈来模拟递归
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
// 注意外循环的判断条件
while (cur != null || !stack.isEmpty()) {
while(cur != null) {
stack.push(cur);
ret.add(cur.val);
cur = cur.left;
}
cur = stack.pop();
cur = cur.right;
}
return ret;
}
七、 二叉树中序非递归遍历实现
OJ链接
📌📌📌题目描述:
给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。
返回值: List<Integer>
📌📌📌解题思路:
和前序遍历大致相同, 只需要修改访问根节点的时机即可
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> ret = new ArrayList<>();
if(root == null) {
return ret;
}
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
while(cur != null) {
stack.push(cur);
cur = cur.left;
}
cur = stack.pop();
ret.add(cur.val);
cur = cur.right;
}
return ret;
}
八、 二叉树后序非递归遍历实现
OJ链接
📌📌📌题目描述:
给你一棵二叉树的根节点 root ,返回其节点值的 后序遍历
返回值: List<Integer>
📌📌📌解题思路:
前两题都是访问右结点在根节点之前, 访问过根节点之后就不需要了它了, 所以可以直接 pop, 但后序遍历需要先通过根节点访问到"右" , 返回去访问"根"
, 所以当 cur 遍历到空时, 先 peek 一下栈顶数据
(如果 pop 就把根丢了, 回不去了) , 此时有两种情况:
1, 栈顶数据的右 == null , 那就不管了, 直接访问 peek 的数据, 访问完就 pop
2, 栈顶数据的右 != null , 让 cur = 栈顶数据的右, 执行内循环, 让右入栈
⚠️⚠️⚠️注意:
先看一下根据刚才分析实现的代码
while (cur != null || !stack.isEmpty()) {
while(cur!=null) {
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.peek();
if(top.right == null) {
ret.add(top.val);
}else {
cur = top.right;
}
}
还有一个隐藏的问题
如图所示(前面步骤省略):
所以完整代码如下:
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> ret = new ArrayList<>();
if(root == null) {
return ret;
}
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
TreeNode tmp = null;
while (cur != null || !stack.isEmpty()) {
while(cur!=null) {
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.peek();
if(top.right == null || top.right == tmp) {
ret.add(top.val);
tmp = stack.pop();
}else {
cur = top.right;
}
}
return ret;
}
九、二叉搜索树中找到两个结点的最近公共祖先
OJ链接
📌📌📌题目描述:
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
1️⃣对于该题的最近的公共祖先定义:对于有根树T的两个节点p、q,最近公共祖先LCA(T,p,q)表示一个节点x,满足x是p和q的祖先且x的深度尽可能大。在这里,一个节点也可以是它自己的祖先
2️⃣二叉搜索树是若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值 // 简单来说, 二叉搜索树的中序遍历序列满足有序
3️⃣所有节点的值都是唯一的。
4️⃣p、q 为不同节点且均存在于给定的二叉搜索树中。
数据范围:
3<=节点总数<=10000
0<=节点值<=10000
返回值: int
📌📌📌解题思路:
理解了二叉搜索树的特殊性质(第二条)
, 这题就很简单了, 递归遍历二叉树的每一个结点 , 分析共有以下几种情况:
1️⃣此时的结点的 val 是 p 或 q , 返回此结点
2️⃣是否 p , q 各自在此节点的两边, 返回此节点
3️⃣p,q 都在此结点的左边或右边, 在哪边就去哪边找, 不能返回此节点!!
⚠️⚠️⚠️注意:
上述第三种情况, 不能直接返回当前结点, 因为题目描述中说明了一个节点也可以是它自己的祖先, 以上图为例, 如果要找 4, 5 的最近公共祖先, 应该是 4, 而不是 1 或 7
public int lowestCommonAncestor (TreeNode root, int p, int q) {
return lowestCommonAncestorChild(root, p, q).val;
}
private TreeNode lowestCommonAncestorChild(TreeNode root, int p, int q) {
if (root.val == p || root.val == q) {
return root;
} else if ((root.val > p && root.val < q) || (root.val < p && root.val > q)) {
return root;
} else if (root.val > p && root.val > q) {
TreeNode ret = lowestCommonAncestorChild(root.left, p, q);
return ret;
} else {
TreeNode ret = lowestCommonAncestorChild(root.right, q, p);
return ret;
}
}
十、二叉树中找到两个结点的最近公共祖先
OJ链接
📌📌📌题目描述:
给定一棵二叉树(保证非空)以及这棵树上的两个节点对应的val值 o1 和 o2,请找到 o1 和 o2 的最近公共祖先节点。
1<=节点总数<=10^5
0<=节点值
注:本题保证二叉树中每个节点的val值均不相同。
所以节点值为 5 和节点值为 1 的节点的最近公共祖先节点的节点值为 3,所以对应的输出为 3。节点本身可以视为自己的祖先
返回值: int
📌📌📌解题思路:
虽然都是子问题思想, 但和上题不同, 因为上题可以利用二叉搜索树的性质, 它的结点分布是有一定规律的, 而普通二叉树没有节点大小的分布规律, 但是可以利用前序遍历去寻找
如果左树找到一个结点的 val 和 o1 或 o2 相等, 则直接返回这个结点, 不再继续往下寻找, 找不到返回 null
从栈帧空间返回去右树找是否有一个结点的 val 和 o1 或 o2 相等, 找到直接返回这个结点, 不再继续往下寻找, 找不到返回 null
当左右都找完了判断:
1️⃣有一边为空, 一边不为空, 说明不为空的那一边找到了最近公共祖先, 哪边不为空返回哪边
2️⃣两边都为空, 两边都没找到, 返回 null
3️⃣两边都不为空, 说明当前结点左右都找到了, 返回当前结点
⚠️⚠️⚠️注意:
1️⃣总体思路就是: 对每一棵树判断: 如果左找到了就去找右边, 看右边有没有 , 但是判定找到
的条件一定是: 当前结点(不是当前结点的left或right)的 val 值 和 o1 或 o2 相等
, 和上一题同理, 以本题的图为例, 2, 4 的公共祖先是2, 而不是 5 或 3
2️⃣记得每次递归调用要接收返回值
public int lowestCommonAncestor (TreeNode root, int o1, int o2) {
return lowestCommonAncestorChild(root,o1,o2).val;
}
private TreeNode lowestCommonAncestorChild(TreeNode root, int o1, int o2) {
if(root == null) {
return null;
}
if(root.val == o1 || root.val == o2) {
return root;
}
TreeNode leftRet = lowestCommonAncestorChild(root.left,o1,o2);
TreeNode rightRet = lowestCommonAncestorChild(root.right,o1,o2);
if(leftRet != null && rightRet == null) {
return leftRet;
}else if(leftRet == null && rightRet != null){
return rightRet;
}else if(leftRet == null || rightRet == null) {
return null;
}else {
return root;
}
}
总结
以上是收录的十道关于二叉树的OJ题练习, 用作学习之余的整理分享, 仅供参考
如果本篇对你有帮助,请点赞收藏支持一下,小手一抖就是对作者莫大的鼓励啦😋😋😋~
上山总比下山辛苦
下篇文章见