树之习题分析下
一、二叉树的序列化与反序列化
(一)、题目需求
序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
示例:
你可以将以下二叉树:
1
/ \
2 3
/ \
4 5
序列化为 “[1,2,3,null,null,4,5]”
提示: 这与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。
说明: 不要使用类的成员 / 全局 / 静态变量来存储状态,你的序列化和反序列化算法应该是无状态的。
(二)、解法
1、树结点类
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
@Override
public String toString() {
return "TreeNode{" +
"val=" + val +
", left=" + left +
", right=" + right +
'}';
}
}
2、序列化解法
public String serialize(TreeNode root) {
if (root == null) {
return "{}";
}
List<TreeNode> list = new ArrayList<>();
// list: {1,2,3,null,null,4,5,null,null,null,null}
list.add(root);
for (int i = 0; i < list.size(); i++) {
TreeNode node = list.get(i);
if (null == node) {
continue;
}
list.add(node.left);
list.add(node.right);
}
// list:{1,2,3,null,null,4,5}
while (list.get(list.size() - 1) == null) {
list.remove(list.size() - 1);
}
// sb:{1,2,3,#,#,4,5}
StringBuilder sb = new StringBuilder("{");
sb.append(list.get(0).val);
for (int i = 1; i < list.size(); i++) {
if (list.get(i) == null) {
sb.append(",#");
} else {
sb.append(",").append(list.get(i).val);
}
}
sb.append("}");
return sb.toString();
}
3、反序列化解法
public TreeNode deserialize(String data) {
if (data == null || "{}".equals(data)) {
return null;
}
String[] datas = data.substring(1, data.length() - 1).split(",");
boolean isLeft = true;
int index = 0;
List<TreeNode> queue = new ArrayList<>();
TreeNode node = new TreeNode(Integer.parseInt(datas[0]));
queue.add(node);
for (int i = 1; i < datas.length; i++) {
if (!("#").equals(datas[i])) {
TreeNode node1 = new TreeNode(Integer.parseInt(datas[i]));
if (isLeft) {
queue.get(index).left = node1;
} else {
queue.get(index).right = node1;
}
queue.add(node1);
}
if (!isLeft) {
index++;
}
isLeft = !isLeft;
}
return queue.get(0);
}
(三)、代码分析
1、序列化解法分析
1、首先通过层次遍历,将该树的所有结点存入List集合中。此时由于叶子节点的左右子结点皆为空,因此该List集合的末尾处存在多余的元素。
List<TreeNode> list = new ArrayList<>();
// list: {1,2,3,null,null,4,5,null,null,null,null}
list.add(root);
for (int i = 0; i < list.size(); i++) {
TreeNode node = list.get(i);
if (null == node) {
continue;
}
list.add(node.left);
list.add(node.right);
}
2、通过一个简单的while循环进行多余元素的去除。
// list:{1,2,3,null,null,4,5}
while (list.get(list.size() - 1) == null) {
list.remove(list.size() - 1);
}
3、此时List集合中所存在的元素皆为我们所需要的元素,根据题意不限制序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。因此此处通过将树的结构序列化的规则如下:
- 以”{}“内的元素表示树的结点
- 空元素以”#“表示
- 非空元素以其值表示
- 各元素之间以“,”间隔
StringBuilder sb = new StringBuilder("{");
sb.append(list.get(0).val);
for (int i = 1; i < list.size(); i++) {
if (list.get(i) == null) {
sb.append(",#");
} else {
sb.append(",").append(list.get(i).val);
}
}
sb.append("}");
2、反序列化解法分析
1、首先进行判断该字符串表示的树是否为空
if (data == null || "{}".equals(data)) {
return null;
}
2、若该字符串表示的树非空,则将该字符串通过”,“进行分割,得到各元素的值。并初始化各变量:
- isLeft:判断当前节点是否为左子节点
- index:当前节点的父节点
- queue:辅助构造树结构
- node:根节点
String[] datas = data.substring(1, data.length() - 1).split(",");
boolean isLeft = true;
int index = 0;
List<TreeNode> queue = new ArrayList<>();
TreeNode node = new TreeNode(Integer.parseInt(datas[0]));
queue.add(node);
3、遍历前文,通过分割","得出的字符串数组,构造树结构
由于,树的的字符串数组的元素是按照层次遍历的顺序存入的。因此当前元素对应节点的父节点对应的元素下标为:当前元素下标/2。因此,当从下标为1的元素开始遍历,每两个节点便为一个节点的子节点。而该节点的位置便通过index来表示。而这一对子节点中,左子节点顺序靠前,因此isLeft标志先为true表示该节点为左子节点,后需变为false表示该节点为右子节点,如此反复进行。
(1)、情况一:当前元素非空:
若当前元素非空,则初始化以当前元素为值的树节点。并判断当前节点是否为父节点的左子节点,若为左子节点,则通过index找到queue中保存的父节点,并将其设为其左子节点。若为右子节点,则设为其右子节点。同时将其加入队列中。
(2)、情况二:当前元素为空:则无需进行任何操作
(3)、共同操作:每当遍历完一个节点后,若左子节点为false,则表示index对应的节点其子节点全部遍历完成,此时需赋值下一节点的子节点,因此index++。同时每遍历一个节点,左子节点的标识皆需进行变化。
for (int i = 1; i < datas.length; i++) {
if (!("#").equals(datas[i])) {
TreeNode node1 = new TreeNode(Integer.parseInt(datas[i]));
if (isLeft) {
queue.get(index).left = node1;
} else {
queue.get(index).right = node1;
}
queue.add(node1);
}
if (!isLeft) {
index++;
}
isLeft = !isLeft;
}
二、二叉树的右视角
(一)、题目需求
给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
示例:
输入: [1,2,3,null,5,null,4]
输出: [1, 3, 4]
解释:
1 <---
/ \
2 3 <---
\ \
5 4 <---
(二)、解法
public List<Integer> rightSideView(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
LinkedList<TreeNode> queue = new LinkedList<>();
queue.offer(root);
boolean findRight = false;
while (!queue.isEmpty()) {
int size = queue.size();
findRight = false;
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (!findRight) {
result.add(node.val);
findRight = true;
}
if (node.right != null) {
queue.offer(node.right);
}
if (node.left != null) {
queue.offer(node.left);
}
}
}
return result;
}
(三)、代码分析
根据题意,可想到该题需要使用树的层次遍历,并且每次先从该层的右端开始遍历。
1、通过queue辅助进行树的层次遍历
while (!queue.isEmpty()) {
// ... 省略中间代码
}
2、通过当前queue中的节点数量,确定该层的遍历次数。并初始化findRight变量,寻找该层的最右端节点
int size = queue.size();
findRight = false;
3、根据先前获取的queue的节点数量,开始遍历。由于当前队列中的队头节点为上一层的最右端节点。因此首先将其加入ressult返回结果集中,之后再逐个节点判断其右子节点与左子节点是否为空,若不为空,则加入queue中,用于下一次遍历寻找最右端元素。
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (!findRight) {
result.add(node.val);
findRight = true;
}
if (node.right != null) {
queue.offer(node.right);
}
if (node.left != null) {
queue.offer(node.left);
}
}
三、二叉树的最近公共祖先
(一)、题目需求
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
(二)、解法
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || p == root || q == root) {
return root;
}
// 递归寻找左右
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if (left != null && right != null) {
return root;
} else if (left != null) {
return left;
} else {
return right;
}
}
(三)、代码分析
1、首先确定结点p与结点q的所有情况
(1)、情况一:p与q都在根节点的左子树中
(2)、情况二:p与q都在根节点的右子树中
(3)、情况三:p与q分别在根节点的左右两边
2、确定返回条件
若root为空,则说明遍历到了最低端,返回null。若p结点或q节点为root节点,则说明p节点或q节点被寻找到,返回p节点或q节点。
if (root == null || p == root || q == root) {
return root;
}
3、递归寻找左右子树
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
4、确定返回结果
(1)、情况一:该节点的左递归查询结果与右递归查询结果皆非空,则说明该节点为p节点与q节点的最近公共祖先,返回该节点。
(2)、情况二:该节点的左递归查询结果非空,右查询结果为空,则说明该节点的左查询结果为p节点与q节点的最近公共祖先,返回左查询结果。
(3)、情况三:该节点的右递归查询结果非空,左查询结果为空,则说明该节点的右查询结果为p节点与q节点的最近公共祖先,返回右查询结果。
if (left != null && right != null) {
return root;
} else if (left != null) {
return left;
} else {
return right;
}
四、平衡二叉树
(一)、题目需求
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:true
示例 2:
输入:root = [1,2,2,3,3,null,null,4,4]
输出:false
示例 3:
输入:root = []
输出:true
提示:
- 树中的节点数在范围
[0, 5000]
内 -104 <= Node.val <= 104
(二)、解法
public static boolean isBalanced(TreeNode root) {
if (root == null) {
return true;
}
return maxTree(root) != -1;
}
private static int maxTree(TreeNode node) {
if (node == null) {
return 0;
}
int left = maxTree(node.left);
int right = maxTree(node.right);
if (left == -1 || right == -1 || Math.abs(left - right) > 1) {
return -1;
} else {
return Math.max(left, right) + 1;
}
}
(三)、代码分析
1、首先判断是否递归至为根节点
if (node == null) {
return 0;
}
2、左右子树递归,得出左右子树的高度
int left = maxTree(node.left);
int right = maxTree(node.right);
3、判断当前节点的左右子树是否失衡
(1)、情况一:其左子树或右子树存在其一为失衡的,则以该节点为根节点的树也失衡。
(2)、情况二:若该子树的左右子树的高度差大于1,则以该节点为根节点的树失去平衡。
(3)、情况三:以该节点为根节点的树并未失衡,则该树的高度为其左右子树的高度的最大值加上它本身,即:Math.max(left, right) + 1
if (left == -1 || right == -1 || Math.abs(left - right) > 1) {
return -1;
} else {
return Math.max(left, right) + 1;
}