目录
二叉树的基本知识可以查看上一篇博客【数据结构】二叉树 — 图文并茂,带你领略它的魅力!!!
二叉树所有题都是围绕着遍历进行的。任何一个二叉树,递归的时候一定是先递归到最左边的。
二叉树相关oj题
1. 检查两颗树是否相同
给你两棵二叉树的根节点p和q编写一个函数来检验这两棵树是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。100. 相同的树 - 力扣(LeetCode)
比较相同出现的情况:结构上 和 节点值上(val) 两个因素
- 一个为空、一个不为空,不相同
- 两个都为空,相同
- 两个都不为空 不一定,需要判断val是否相同
子问题思路:先判断根节点的可能出现的上面的情况,然后递归判读左树 和 右树的可能出现的情况。
public boolean isSameTree(TreeNode p, TreeNode q) {
//只要存在结构上的不同直接返回false
if(p == null && q!= null || p!=null && q == null) {
return false;
}
//上述代码代码走完之后 要么是两个都为空,要么是两个都不为空
if(p == null && q == null) {
return true;
}
//代码走到这里 两个都不为空,判断值是否一样
if(p.val != q.val) {
return false;
}
//p != null && q!=null && p.val == q.val;
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
- 时间复杂度:O(min(m,n)) 访问到的节点数不会超过较小的二叉树的节点数。(较小的二叉树为空就不判断了)
- 空间复杂度:O(min(m,n)) 递归调用的层数不会超过较小的二叉树的最大高度,最坏情况下,二叉树的高度等于节点数。
2. 另一棵树的子树
给你两棵二叉树 root 和 subRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false 。root 也可以看做它自身的一棵子树。
问题分析:三种情况
- 是不是相同的树
- 是不是root的左子树
- 是不是root的右子树
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if(root == null || subRoot == null) {
return false;
}
//1.是不是和根节点相同
if(isSameTree(root, subRoot)) {
return true;
}
//判断是不是root的左子树
if(isSubtree(root.left, subRoot)) {
return true;
}
//判断是不是root的右子树
if(isSubtree(root.right, subRoot)) {
return true;
}
//返回
return false;
}
//检查两个树是否相同
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p == null && q != null || p != null && q == null) {
return false;
}
//走到这里,p和q 要么都为空 ,要么都不为空
if(p == null && q == null) {
return true;
}
//走到这里,p和q 一定都不为空了,比较他们的val值
if(p.val != q.val) {
return false;
}
//走到这里,p和q都不为空 且val值一样
return isSameTree(p.left,q.left) && isSameTree(p.right,q.right);
}
这里注意,如果不判断空root是否为null,下面这种情况就会出现 空指针异常了
- root树有个S节点,subRoot右T个节点
- 时间复杂度为:O(S*T) root 中每个节点都要跟subRoot中的节点就进行比较。判断两颗树是否相同的方法O(N)。
- 空间复杂度为:O(max(S,T))
3. 翻转二叉树
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。226. 翻转二叉树 - 力扣(LeetCode)
子问题思路:让左树与右树翻转,再让左树的子节点翻转,然后右树的子节点反转。
public TreeNode invertTree(TreeNode root) {
//空树不翻转直接返回
if(root == null) {
return root;
}
//这行代码是走到叶子节点,不用交换空的节点了
if(root.left == null && root.right == null) {
return root;
}
//交换
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
invertTree(root.left);
invertTree(root.right);
return root;
}
4. 判断一颗二叉树是否是平衡二叉树
给定一个二叉树,判断它是否是高度平衡的二叉树。110. 平衡二叉树 - 力扣(LeetCode)
平衡二叉树:一个二叉树每个节点的左右两个子树的高度差的绝对值不超过 1
子问题思路:判断根节点的左子树与右子树高度差是否平衡,然后再判断左子树节点高度差是否平衡,右节点高度差是否平衡。
//最坏情况下 每个节点 都要求高度
public boolean isBalanced(TreeNode root) {
if(root == null) {
return true;
}
//先求当前root,左树和右树的高度
int leftHeight = maxDepth(root.left);
int rightHeight = maxDepth(root.right);
return Math.abs(leftHeight - rightHeight) <= 1
&& isBalanced(root.left) && isBalanced(root.right);
}
//计算二叉树最大深度
public int maxDepth(TreeNode root) {
//判断该二叉树是否为空树,同时也是递归结束条件
if (root == null) {
return 0;
}
int leftHeight = maxDepth(root.left);
int rightHeight = maxDepth(root.right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
- 时间复杂度为:O(N^2) 有n个节点每个节点都要求高度(计算高度差),求每个节点求高度的时间复杂度为O(n)。每个节点都要遍历求树的高度,每个节点每次都要调用maxDepth方法。
- 上述代码有重复高度计算的情况。字节面试:能不能实现时间复杂度O(n)的代码。
- 求高度的过程当中 就判断是否平衡,如果某个子树不满足平衡,就返回负数。即—边求高度,一边判端平衡的问题。
//最坏情况下 每个节点 都要求高度
public boolean isBalanced(TreeNode root) {
if(root == null) {
return true;
}
return maxDepth(root) >= 0;
}
public int maxDepth(TreeNode root) {
//判断该二叉树是否为空树,同时也是递归结束条件
if (root == null) {
return 0;
}
int leftHeight = maxDepth(root.left);
if(leftHeight < 0) {
return -1;
}
int rightHeight = maxDepth(root.right);
if (leftHeight >= 0 && rightHeight >= 0
&& Math.abs(leftHeight - rightHeight) <= 1) {
//在这种情况下 才会返回 真是的高度
return Math.max(leftHeight, rightHeight) + 1;
} else {
return -1;
}
}
5. 对称二叉树
给你一个二叉树的根节点 root
, 检查它是否轴对称。101. 对称二叉树 - 力扣(LeetCode)
- 判断整颗树是不是轴对称的,就是判断这颗树的左子树 和 这棵树的右子树是不是对称。
- 如何判断 左子树与右子树对称, 即左子树的左树和右子树的右树与左子树的右树和右子树的左树是否对称。
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return isSymmetricChild(root.left, root.right);
}
private boolean isSymmetricChild(TreeNode leftTree, TreeNode rightTree) {
if((leftTree == null && rightTree != null) || (leftTree != null && rightTree == null)) {
return false;
}
if(leftTree == null && rightTree == null) {
return true;
}
// root.left != null && root.right != null 判断值是否相同
if(leftTree.val != rightTree.val) {
return false;
}
return isSymmetricChild(leftTree.left, rightTree.right)
&& isSymmetricChild(leftTree.right, rightTree.left);
}
6. 二叉树的构建及遍历
编一个程序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储)。 例如如下的先序遍历字符串: ABC##DE#G##F### 其中“#”表示的是空格,空格字符代表空树。建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。二叉树遍历_牛客题霸_牛客网
问题分析:这里已经指定了空树的位置,所以根据前序遍历序列可以画出二叉树。
- 给的前序遍历的字符串,我们也用前序遍历的方式进行解决。
- 思路:遍历这个字符串根据前序遍历的方式创建二叉树,然后中序遍历打印出即可。
class TreeNode {
public char val; //数值域
public TreeNode left;//左子树引用
public TreeNode right;//右子树引用
public TreeNode(char val) {
this.val = val;
}
}
public class Main {
public static int i = 0;
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 = createTree(str);
inOrder(root);
}
}
//创建二叉树并返回根节点
public static TreeNode createTree(String str) {
//1.遍历字符串str
/*for(int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
}*/
TreeNode root = null;
if(str.charAt(i) != '#') {
//2.前序遍历创建二叉树
root = new TreeNode(str.charAt(i));
i++;
root.left = createTree(str);
root.right = createTree(str);
} else {
//跳过'#'
i++;
}
//3.放回根节点
return root;
}
//中序遍历
public static void inOrder(TreeNode root) {
if(root == null) {
return;
}
inOrder(root.left);
System.out.print(root.val + " ");
inOrder(root.right);
}
}
- 遍历字符串第一时间想到的是 for循环,但是这里需要递归,每次递时字符串总是会从头开始遍历。把 遍历字符串 i 下标变量放到创建树这个方法的外面。
- 定义为当前类的成员变量 i ,跟着递归的次数增加,每次递归就是想要的字符了。因createTree方法为static修饰的,i 定义的为static 变量。
- 被static修饰的成员变量只有一份它是和很多对象共享的。但是本题在OJ上只有一个测试用例,所以没有报错。总之慎用。
- 因为 i 定义的为static 变量,关于多个测试用例调用该方法 i 置为0的问题,可以在每次对象创建使用该方法前把 i 置为 0
- 还有一种方法,就是当你在读取字符串的时候就把字符串放到队列当中,不需要 i 的值了也可以解决(下面题目中会讲到类似的方法)
- 注意体会,在递的过程中节点并没有绑定起来,而是在归的过程中才绑定了节点(建立联系)。
- 一定是先遍历到最左边树的最底层, 然后返回,对节点进行组织。
- 前序遍历,中序遍历,后序遍历都是一条路线,只是访问根节点的时机不一样。
- 这里的 i 难道不会越界吗? 不会。因为给的是先序字符串一定是合法的,符合前序遍历规则的。
- 即递归的次数是一定的,回溯的次数也是一定的,递归与回溯的次数一定是一样的。
7. 二叉树的层序遍历
层序遍历:从上到下,从左到右,依次遍历。
- 从上到下:前序,中序,后序本质上都是从上到下。
- 从左到右:创建一个队列实现。
第一种方法:无返回值要求
思路:用非递归的方式(也可以用递归的方式写)。创建一个队列,放二叉树的根节点,当队列不为空时出队列,记录并打印这个节点,然后放左子树,右子树。过程一定是先入左后入右。
public void levelOrder2(TreeNode root) {
if (root == null) {
return;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()) {
TreeNode cur = queue.poll();
System.out.print(cur.val + " ");
if(cur.left != null) {
queue.offer(cur.left);
}
if (cur.right != null) {
queue.offer(cur.right);
}
}
}
第二种方法:有返回值要求。102. 二叉树的层序遍历 - 力扣(LeetCode)
- 问题分析:需要确定每一层有哪些以及多少个节点
- 思路:记录队列中元素个数,来确定每层元素的个数 ,将每层的元素都放到一个数组中,然后再将每个数组的第一个元素都放到一个数组中。
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ret = new ArrayList<>();
if (root == null) {
return ret;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()) {
int size = queue.size();
List<Integer> tmp = new ArrayList<>();
//记录队列中元素个数,来确定每层的元素个数
while (size != 0) {
TreeNode cur = queue.poll();
//System.out.print(cur.val + " ");
tmp.add(cur.val);
size--;
if(cur.left != null) {
queue.offer(cur.left);
}
if (cur.right != null) {
queue.offer(cur.right);
}
}
ret.add(tmp);
}
return ret;
}
拓展:阿里的面试题:输出一颗二叉树的左视图(从左边看),即数组每层的第一个节点。
右视图,即数组每层的最后一个节点。
8. 判断一棵树是不是完全二叉树
利用层序遍历的思路:
// 判断一棵树是不是完全二叉树
boolean isCompleteTree(TreeNode root) {
if (root == null) {
return true;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode cur = queue.poll();
if (cur != null ) {
queue.offer(cur.left);
queue.offer(cur.right);
} else {
break;//结束这个循环
}
}
//代码走到这里队列一定不为空,因为null同样入队了
//需要判断队列当中 是否有非空元素
while (!queue.isEmpty()) {
//一个元素一个元素 出队列判断 是不是空
if (queue.poll() != null) {
return false;
}
}
return true;
}
9. 二叉树的最近公共祖先
给定一个二叉树,找到该树中两个指定节点的最近公共祖先。
最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”其中p和q 均存在。236. 二叉树的最近公共祖先 - 力扣(LeetCode)
第一种方法:
问题分析:
p 和 q 在根的左右两边
都在根的左边 或者 右边
其中一个节点是公共祖先
还有一种情况,根节点root 是p或者q其中一个,如果是其中一个则公共节点就是它,跟上面的 其中一个节点是公共节点情况一样
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null) {
return null;
}
if (root == p || root == q) {
return root;
}
TreeNode leftTree = lowestCommonAncestor(root.left, p,q);
TreeNode rightTree = lowestCommonAncestor(root.right, p,q);
if (leftTree != null && rightTree != null) {
return root;
} else if (leftTree != null) {
return leftTree;
} else {
return rightTree;
}
}
第二种方法: 利用栈来做
- 二叉树目前来说不能这样做,因为没有父节点地址;可以使用数据结构,用两个栈。
- 谁多谁先走差值个,s2先弹出1个,然后s1与s2 一起一个一个弹,比较是否相同,相同就是公共祖先。
- 如何从根节点到指定节点,找到路径上的所有节点,放到栈里面
- 回溯的时候弹出去,怎么知道当前这个节点要弹出去?
10. 根据前序与中序遍历序列构造二叉树
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。 105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)题目给有
- preorder 保证 为二叉树的前序遍历序列
- inorder 保证 为二叉树的中序遍历序列
所以不用判断二叉树为空、以及遍历的序列是否合法的情况等
问题分析:
- 根据前序遍历,找到根
- 在中序遍历当中,找到根的位置 ri
以前序遍历的方式递归,递归在回溯的时候才把二叉树给串起来。
11. 根据中序与后序遍历序列构造二叉树
给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)
- 根据后序遍历序列找根节点
- 跟上面的代码差不多一样,只不过是从后往前遍历中序遍历序列,然后创建根,创建右树,再创建左树
12. 二叉树创建字符串
给你二叉树的根节点 root ,请你采用前序遍历的方式,将二叉树转化为一个由括号和整数组成的字符串,返回构造出的字符串。
空节点使用一对空括号对 "()" 表示,转化后需要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空括号对。606. 根据二叉树创建字符串 - 力扣(LeetCode)
13. 二叉树前序非递归遍历实现
给你二叉树的根节点 root
,返回它节点值的 前序 遍历。144. 二叉树的前序遍历 - 力扣(LeetCode)
问题分析:非递归root是不会变的,我们需要创建cur代替root,利用栈(先进后出)模拟递归来解决问题。
public void preOrderNor(TreeNode root) {
if (root == null) {
return;
}
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
stack.push(cur);
System.out.print(cur.val + " ");
cur = cur.left;//cur往左走
}
TreeNode top = stack.pop();
cur = top.right;
}
}
注意这里的while(cur != null || !stack.isRmpty()) 条件是反向推出来的。
14. 二叉树中序非递归遍历实现
- 一直往左走遇到空停止,左树走完了弹出栈顶元素并且打印。
- 遍历弹出节点的右子树
public void inOrderNor(TreeNode root) {
if (root == null) {
return;
}
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.pop();
System.out.print(top.val + " ");
cur = top.right;
}
}
15. 二叉树后序非递归遍历实现
public void postOrderNor(TreeNode root) {
if (root == null) {
return;
}
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
TreeNode prev = 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 == prev) {
System.out.print(top.val + " ");
stack.pop();
prev = top;
} else {
cur = top.right;
}
}
}
- 注意:if(top.right == null || top.right == prev) 代表两种情况
- 1、右边为空了 2、右边已经被打印完了
- prev = top 是记录下最新被打印的那个节点。
好啦Y(^o^)Y,本节内容到此就结束了。下一篇内容一定会火速更新!!!
后续还会持续更新数据结构与算法方面的内容,还请大家多多关注本up,第一时间获取新鲜的知识。
如果觉得文章不错,别忘了一键三连哟!