二叉树算法二
有了二叉树算法一的学习,对整个框架模板有了了解,下面做的是需要多刷题。
※116、填充每个节点的下一个右侧节点指针(中等)
注意题目中说了,初始状态下,所有next指针都是NULL,所以就不用担心根指针如何连接的问题。同样时拆分成左右指针来处理,唯一要注意的是,5和6节点的连接。
class Solution {
public Node connect(Node root) {
if (root == null) {
return root;
}
traverse(root.left, root.right);
// 初始状态下,所有 next 指针都被设置为 NULL。
return root;
}
void traverse(Node root1, Node root2) {
// 完美二叉树,没有左边,则一定没有右边
if (root1 == null) return;
root1.next = root2;
// 连接左边节点的左右孩子
traverse(root1.left, root1.right);
// 连接右边节点的左右孩子
traverse(root2.left, root2.right);
// 连接左边节点的右孩子节点 与 左边节点的左孩子节点(5,6)
traverse(root1.right, root2.left);
}
}
※114、二叉树展开为链表(中等)
class Solution {
public void flatten(TreeNode root) {
if (root == null) {
return;
}
flatten(root.left);
flatten(root.right);
// 后序位置,左右子树都被拉平了
// 把拉平的左子树拿到root的right
TreeNode left = root.left;
TreeNode right = root.right;
root.right = left;
root.left = null;
// 剩下的right还要继续接着root的right后面
TreeNode p = root;
while (p.right != null) {
p = p.right;
}
p.right = right;
}
}
上面这个是大佬的解释,确实,递归就是很神奇,不能深入看它,但它底层原理又得清楚,有时候写递归,特别是复杂题目,根本不可能搞懂真正的底层逻辑,只能以简单的例子来写。
二叉树的构造问题
※654、最大二叉树(中等)
二叉树的题目往往会让人很害怕,因为我们脑袋想不通递归的整个流程,终会害怕递归会写错,但实际上递归的代码是非常简单的,也不需要人真正去想清楚递归的原理,只要根着递归函数的意义来就行了,根据题目意思可以写出下面的伪代码:
TreeNode constructMaximumBinaryTree([3,2,1,6,0,5]) {
// 找到数组中的最大值
TreeNode root = new TreeNode(6);
// 递归调用构造左右子树
root.left = constructMaximumBinaryTree([3,2,1]);
root.right = constructMaximumBinaryTree([0,5]);
return root;
}
再详细点就可以写出下面的代码:
TreeNode constructMaximumBinaryTree(int[] nums) {
if (nums is empty) return null;
// 找到数组中的最大值
int maxVal = Integer.MIN_VALUE;
int index = 0;
// 找最大值下标
for (int i = 0; i < nums.length; i++) {
if (nums[i] > maxVal) {
maxVal = nums[i];
index = i;
}
}
TreeNode root = new TreeNode(maxVal);
// 递归调用构造左右子树
root.left = constructMaximumBinaryTree(nums[0..index-1]);
root.right = constructMaximumBinaryTree(nums[index+1..nums.length-1]);
return root;
}
我们定义一个构造二叉树的递归函数,那就相信这个函数,传给它什么,它就构造出什么。传给它1、3、2,找到最大值是3,放中间,左边放1,右边放2,1、2也都是最大值两边两部分各自的最大值。
class Solution {
public TreeNode constructMaximumBinaryTree(int[] nums) {
return build(nums, 0, nums.length - 1);
}
public TreeNode build(int[] nums, int left, int right) {
if (left > right) return null;
// 找最大值下标
int index = -1;
int max = Integer.MIN_VALUE;
for (int i = left; i <= right; i++) {
if (nums[i] > max) {
max = nums[i];
index = i;
}
}
// 以最大值构建根节点
TreeNode root = new TreeNode(max);
// 再往左右递归构建子节点,左右子节点也可以看成是独立的一棵树,也需要从根节点构造起
root.left = build(nums, left, index - 1);
root.right = build(nums, index + 1, right);
return root;
}
}
※105、从前序与中序遍历序列构造二叉树(中等)
跟前一道题一样,我们需要确定根节点,然后把数组分成两部分,一部分是根节点左边,一部分是根节点右边,分别用来构造左右子节点。对于前序遍历根节点很好找,就是第一个元素,但是它并不好划分出左右子节点,这个时候就要看中序遍历,因为题目中每个节点的值都是唯一的,我们可以靠之前前序遍历获得的根节点的值,在中序遍历中找到根节点的下标,那它的左边和右边就是左右子节点。同样的利用中序遍历,我们就能确定前序遍历数组的左右部分了。前序遍历数组和中序遍历数组两者是互相需要的,中序需要前序找根节点,前序需要中序找左右子节点。
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
return build(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
}
TreeNode build(int[] preorder, int preLeft, int preRight, int[] inorder, int inLeft, int inRight) {
if (preLeft > preRight) return null;
// 根据前序序列找到根节点
int rootVal = preorder[preLeft];
// 记录中序序列中根节点的下标
int index = -1;
for (int i = inLeft; i <= inRight; i++) {
if (inorder[i] == rootVal) {
index = i;
}
}
// 左子节点的个数
int leftSize = index - inLeft;
TreeNode root = new TreeNode(rootVal);
// 关键是限制出两个数组的左右子节点的范围
root.left = build(preorder, preLeft + 1, preLeft + leftSize, inorder, inLeft, index - 1);
root.right = build(preorder, preLeft + leftSize + 1, preRight, inorder, index + 1, inRight);
return root;
}
}
※106、从中序与后序遍历序列构造二叉树(中等)
和上面两个题一样,先找到根节点位置,然后去划分左右子节点。对于后序遍历数组,最后一个元素肯定是根节点,题目保证每一个节点值不同,那就再去中序遍历中找根节点下标,把数组分成两部分即可。
对于后序遍历数组来说,最后一个位置是根节点,前面存放顺序是:左右根。
class Solution {
public TreeNode buildTree(int[] inorder, int[] postorder) {
return build(inorder, 0, inorder.length - 1, postorder, 0, postorder.length - 1);
}
TreeNode build(int[] inorder, int inLeft, int inRight, int[] postorder, int postLeft, int postRight) {
if (inLeft > inRight) return null;
// 根节点值
int rootVal = postorder[postRight];
int index = -1;
// 找中序遍历中根节点的下标
for (int i = inLeft; i <= inRight; i++) {
if (inorder[i] == rootVal) {
index = i;
break;
}
}
// 左子节点的数目
int leftSize = index - inLeft;
TreeNode root = new TreeNode(rootVal);
// 注意后序遍历数组:左右根,最后一个位置是根节点
root.left = build(inorder, inLeft, index - 1, postorder, postLeft, postLeft + leftSize - 1);
root.right = build(inorder, index + 1, inRight, postorder, postLeft + leftSize, postRight - 1);
return root;
}
}
※889、根据前序和后序遍历构造二叉树(中等)
通过前序中序,或者后序中序遍历结果可以确定一棵原始二叉树,但是通过前序后序遍历结果无法确定原始二叉树。
例如下面这两幅图,他们的前序和后序遍历结果都是一样的
不过话说回来,用后序遍历和前序遍历结果还原二叉树,解法逻辑上和前两道题差别不大,也是通过控制左右子树的索引来构建:
1、首先把前序遍历结果的第一个元素或者后序遍历结果的最后一个元素确定为根节点的值。
2、然后把前序遍历结果的第二个元素作为左子树的根节点的值。
3、在后序遍历结果中寻找左子树根节点的值,从而确定了左子树的索引边界,进而确定右子树的索引边界,递归构造左右子树即可。
class Solution {
public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
return build(preorder, 0, preorder.length - 1, postorder, 0, postorder.length - 1);
}
TreeNode build(int[] preorder, int preLeft, int preRight, int[] postorder, int postLeft, int postRight) {
if (preLeft > preRight) return null;
// 如果相等,只有一个节点,直接返回这个节点即可
if (preLeft == preRight) return new TreeNode(preorder[preLeft]);
// 根节点的值
int rootVal = preorder[preLeft];
// 找到左节点的根节点的值
int leftFirst = preorder[preLeft + 1];
// 找到左节点的根节点在后序遍历数组中的位置
int index = -1;
for (int i = postLeft; i <= postRight; i++) {
if (postorder[i] == leftFirst) {
index = i;
break;
}
}
// 左节点的数目
int leftSize = index - postLeft + 1;
TreeNode root = new TreeNode(rootVal);
// 通过左节点的数目限制出左节点、右节点的位置
root.left = build(preorder, preLeft + 1, preLeft + leftSize, postorder, postLeft, postLeft + leftSize - 1);
root.right = build(preorder, preLeft + leftSize + 1, preRight, postorder, postLeft + leftSize, postRight - 1);
return root;
}
}
※297、二叉树的序列化与反序列化(困难)
题目意思是想设计一种方法把二叉树序列化,基于序列化的结果,再设计一个反序列化函数,将二叉树构造出来。序列化的过程,其实是通过各种遍历方式存储树中各个节点的过程,例如:前、中后序遍历,层序遍历当然也可以,题目中并没有做要求,自己设计。
先来看看前序遍历,我们用-1记录为null的节点,那么对于给定的树,可以写出下列代码:
class Solution {
List<Integer> ans = new LinkedList<>();
public List<Integer> preorderTraversal(TreeNode root) {
preOrder(root);
return ans;
}
void preOrder(TreeNode root) {
if (root == null) {
ans.add(-1);
return ;
}
// 前序位置加入ans
ans.add(root.val);
preOrder(root.left);
preOrder(root.right);
}
}
例如下面这个图:
会输出:
大佬画的这个图会更直观一点:
根据前序遍历的过程,可以先确定序列化函数:
public class Codec {
// 指定符号含义
String SEP = ",";
String NULL = "#";
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
StringBuffer sb = new StringBuffer();
serialize(root, sb);
return sb.toString();
}
// 协助进行序列化
void serialize(TreeNode root, StringBuffer sb) {
if (root == null) {
sb.append(NULL).append(SEP);
}
// 前序位置
sb.append(root.val).append(SEP);
serialize(root.left, sb);
serialize(root.right, sb);
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
}
}
通过序列化产生的字符串,然后再用split函数分割。
String data = "1,2,#,4,#,#,3,#,#,";
String[] nodes = data.split(",");
这样,nodes 列表就是二叉树的前序遍历结果,问题转化为:如何通过二叉树的前序遍历结果还原一棵二叉树?(也就是如何实现反序列化函数)
PS:一般语境下,单单前序遍历结果是不能还原二叉树结构的,因为缺少空指针的信息,至少要得到前、中、后序遍历中的两种才能还原二叉树。
但是这里的 node 列表包含空指针的信息,所以只使用 node 列表就可以还原二叉树。
也就是说,我们能够反序列产生二叉树的结构,是因为我们在序列化时记录了空节点的位置!
以题目输入为例:[1,2,3,null,null,4,5],序列化后结果为:1,2,#,#,3,4,#,#,5,#,#,(最后有逗号,可以作为split的对象),我们就可以利用这串包含空节点的前序遍历顺序建树。
前序遍历实现序列化、反序列化
public class Codec {
// 指定符号含义
String SEP = ",";
String NULL = "#";
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
StringBuffer sb = new StringBuffer();
serialize(root, sb);
return sb.toString();
}
// 协助进行序列化
void serialize(TreeNode root, StringBuffer sb) {
if (root == null) {
sb.append(NULL).append(SEP);
return;
}
// 前序位置
sb.append(root.val).append(SEP);
serialize(root.left, sb);
serialize(root.right, sb);
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
LinkedList<String> str = new LinkedList<>();
for (String t : data.split(SEP)) {
str.add(t);
}
return deserialize(str);
}
// 协助进行反序列化(也就是建树)
TreeNode deserialize(LinkedList<String> nodes) {
if (nodes.isEmpty()) return null;
// 列表第一个元素就是根节点
String rootVal = nodes.removeFirst();
// 遍历到NULL字符,那就说明到头了
if (rootVal.equals(NULL)) return null;
TreeNode root = new TreeNode(Integer.parseInt(rootVal));
root.left = deserialize(nodes);
root.right = deserialize(nodes);
return root;
}
}
中序遍历实现序列化、反序列化
中序遍历可以实现序列化,但是无法实现反序列化,因为根节点位置夹在左右节点中间,不能够找到具体根节点的所在位置,所以无法实现。序列化可以实现,也就是正常的进行中序遍历即可。
后序遍历实现序列化、反序列化
后序遍历可以实现序列化,也可以实现发序列化,因为它的根节点位置一定在末尾,所以就可以确定下来。
public class Codec {
// 定义字符含义
String SEP = ",";
String NULL = "#";
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
StringBuffer sb = new StringBuffer();
serialize(root, sb);
return sb.toString();
}
// 协助实现后序遍历序列化
void serialize(TreeNode root, StringBuffer sb) {
if (root == null) {
sb.append(NULL).append(SEP);
return;
}
// 正常求后序遍历顺序
serialize(root.left, sb);
serialize(root.right, sb);
// 后序位置
sb.append(root.val).append(SEP);
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
LinkedList<String> nodes = new LinkedList<>();
for (String str : data.split(",")) {
nodes.add(str);
}
return deserialize(nodes);
}
// 协助实现后序遍历反序列化
TreeNode deserialize(LinkedList<String> nodes) {
if (nodes == null) return null;
// 根节点在末尾
String last = nodes.removeLast();
// 是空节点
if (last.equals(NULL)) {
return null;
}
TreeNode root = new TreeNode(Integer.parseInt(last));
// 注意后序遍历的顺序:左右根,我们是倒着取数,所以根节点后应该插入右节点
root.right = deserialize(nodes);
root.left = deserialize(nodes);
return root;
}
}
层序遍历实现序列化、反序列化
想一想层序遍历,通过队列实现,第一个节点就是根节点,每一层遍历完了再往下遍历,所以也是可以实现序列化和反序列化的,先来看看层序遍历的序列化,也就是记录层序遍历的路径,只不过要同时记录空节点。
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ans = new LinkedList<>();
if (root == null) {return ans;}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int sz = queue.size();
List<Integer> tmp = new LinkedList<>();
for (int i = 0; i < sz; i++) {
TreeNode t = queue.poll();
tmp.add(t.val);
if (t.left != null) queue.offer(t.left);
if (t.right != null) queue.offer(t.right);
}
ans.add(new LinkedList(tmp));
}
return ans;
}
}
下面来看看具体的反序列化,如何用层序遍历实现。
注意,我们在序列化时存储了空节点,所以对于层序遍历来说,第一个节点一定是根节点,第二个节点是左子节点,第三个节点是右子节点(不论子节点是否存在,我们都用字符记录了),如果左子节点不为空,我们就再把左子节点入队,然后继续看右子节点,右子节点不为空就再把右子节点入队,仔细思考这个过程,其实和实现层序遍历的过程是一样的。
public class Codec {
String SEP = ",";
String NULL = "#";
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
if (root == null) return null;
// 使用层序遍历实现
StringBuffer sb = new StringBuffer();
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int sz = queue.size();
for (int i = 0; i < sz; i++) {
TreeNode tmp = queue.poll();
if (tmp == null) {
sb.append(NULL).append(SEP);
continue;
} else sb.append(tmp.val).append(SEP);
// 这里不要写tmp.left / tmp.right == null,这样不会记录下null空节点!
queue.offer(tmp.left);
queue.offer(tmp.right);
}
}
return sb.toString();
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if (data == null) return null;
String[] nodes = data.split(",");
// 第一个节点一定是根节点
TreeNode root = new TreeNode(Integer.parseInt(nodes[0]));
// 同样用迭代队列方式实现
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
for (int i = 1; i < nodes.length; ) {
TreeNode parent = queue.poll();
// 仅接着的两个节点一定是左、右子节点
String left = nodes[i++];
String right = nodes[i++];
// 考虑左子节点
if (left.equals(NULL)) {
parent.left = null;
} else {
parent.left = new TreeNode(Integer.parseInt(left));
queue.offer(parent.left);
}
// 考虑右子节点
if (right.equals(NULL)) {
parent.right = null;
} else {
parent.right = new TreeNode(Integer.parseInt(right));
queue.offer(parent.right);
}
}
return root;
}
}
※652、寻找重复的子树(中等)
更好地理解前序、中序和后序的位置作用,根据题目意思,可以选择具体在哪个位置求解。
有了上面序列化、反序列化题目的支撑,这道题其实就显而易见了,对于某个根节点,我们想要知道自己当前树的结构长成什么样,就必须先知道左子树、右子树长什么样,然后再加上根节点不就行了,这显然就需要在后序位置进行求解,因为只有在后序位置
,才能知道左右子结点长什么样。 用什么办法呢?序列化,对,就是序列化,可以用前、中、后、层序去实现。但上面说了要在后序位置求解,那直接用后序遍历实现序列化不就更方便吗?
好了,现在也知道自己长什么样了,要去看看别人长什么样,怎么办?用HashMap,每次把每个节点作为根节点的序列化结果存入,对应的出现次数++,我们只统计相同的,同时答案不能重复。
class Solution {
List<TreeNode> ans = new LinkedList<>();
public List<TreeNode> findDuplicateSubtrees(TreeNode root) {
traverse(root);
return ans;
}
// 辅助寻找答案
// 存储子树序列出现次数
HashMap<String, Integer> map = new HashMap<>();
String traverse(TreeNode root) {
// 按照之前的序列化方法来
if (root == null) return "#";
String left = traverse(root.left);
String right = traverse(root.right);
// 在后序位置求解答案
String serial = left + "," + right + "," + root.val;
int cnt = map.getOrDefault(serial, 0);
// 我们是站在根节点去看,在后序位置已经知道了根节点的左右子节点的位置关系
if (cnt == 1) {
ans.add(root);
}
map.put(serial, cnt + 1);
return serial;
}
}