本节主要讨论有关构造二叉树的题目。
LeetCode-106.从中序与后序遍历序列构造二叉树
给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这棵 二叉树 。
就是用中序和后序遍历的结果反推二叉树的结构,那么我们需要分析一下一棵二叉树的后序遍历和前序遍历有什么特性:
- 后序遍历:最后一个结果是二叉树的根节点。
- 中序遍历:二叉树的左子树节点在根节点左边,右子树节点在根节点右边。
是不是就发现根据这个特性就可以一个节点一个节点的构造出二叉树了!
图解:
文字说明:
- 由后序结果可知二叉树的根节点为3
- 在中序结果中找到根节点3,其左侧为左子树的所有结点,右侧为右子树的所有结点:
可求出左右子树的中序遍历 - 可知左子树结点个数和右子树结点个数,又因为后序遍历为左右中:
可根据中序左子树的个数裁剪出左子树的后序遍历 - 至此求得了一个节点和该节点对应的左子树和右子树的后序和中序遍历
- 循环1-3,不断各个节点
递归
递归分析:
- 返回值和入参:返回当前构造出的某个节点,入参是中序和后序数组
- 终止条件:中序数组或后序数组为空 返回空节点
- 单层递归条件:
- 创建后序最后一个值的节点,代表当前序列对应的树根节点,并将其弹出数组
- 找到根节点在中序的索引,进而分割中序和后序数组
- 将其新的中序和后序作为新的参数递归构造当前节点的左子树和右子树
实现:
function buildTreeRecursive(inorder, postorder) {
if (!inorder.length || !postorder.length) return null;
//弹出后序最后一个值
const curRootVal = postorder.pop();
//创建该值表示的节点
const curRoot = new TreeNode(curRootVal);
//找到该值在中序的位置
const curRootIndex = inorder.indexOf(curRootVal);
//左子树前序和后序
const leftTreeInorder = inorder.slice(0, curRootIndex);
const leftTreePostorder = postorder.slice(0, curRootIndex);
//右子树前序和后序
const rightTreeInorder = inorder.slice(curRootIndex + 1);
const rightTreePostorder = postorder.slice(curRootIndex);
curRoot.left = buildTree(leftTreeInorder, leftTreePostorder);
curRoot.right = buildTree(rightTreeInorder, rightTreePostorder);
return curRoot;
}
var buildTree = function (inorder, postorder) {
return buildTreeRecursive(inorder, postorder);
};
注意点:在求左右子树后序数组时需要注意我们在之前pop了后序数组的最后一位,此时中序得到的索引在后序而言前移了一位,因此rightTreePostorder = postorder.slice(curRootIndex);
LeetCode-105.从前序与中序遍历序列构造二叉树
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
本题和上题一样,自己尝试分析一下; 代码实现如下:
function buildTreeRecursive(preorder, inorder) {
if (!preorder.length || !inorder.length) return null;
//获取前序第一个节点作为根
const rootVal = preorder.shift();
//创建根节点
const root = new TreeNode(rootVal);
//获取根节点在中序的索引
const rootIndex = inorder.indexOf(rootVal);
//左子树前序和中序
const leftTreePreorder = preorder.slice(0, rootIndex);
const leftTreeInorder = inorder.slice(0, rootIndex);
//右子树前序和中序
const rightTreePreorder = preorder.slice(rootIndex);
const rightTreeInorder = inorder.slice(rootIndex + 1);
root.left = buildTreeRecursive(leftTreePreorder, leftTreeInorder);
root.right = buildTreeRecursive(rightTreePreorder, rightTreeInorder);
return root;
}
var buildTree = function (preorder, inorder) {
return buildTreeRecursive(preorder, inorder);
};
LeetCode-654.最大二叉树
给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建:
- 创建一个根节点,其值为
nums中的最大值。 - 递归地在最大值 左边 的 子数组前缀上 构建左子树。
- 递归地在最大值 右边 的 子数组后缀上 构建右子树。
返回 nums 构建的 最大二叉树 。
实际上与前两道题也是类似,递归的构建子树,只是规则变了。
不过多分析,分析交给你。
代码如下:
function getMaxVal(arr) {
let max = -Infinity;
let index = -1;
for (let i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
index = i;
}
}
return [max, index];
}
function buildTreeRecursive(nums) {
if (!nums.length) return null;
//获取最大值
const [max, index] = getMaxVal(nums);
//创建最大值节点
const root = new TreeNode(max);
//获取左右子树数组
const leftTreeArray = nums.slice(0, index);
const rightTreeArray = nums.slice(index + 1);
root.left = buildTreeRecursive(leftTreeArray);
root.right = buildTreeRecursive(rightTreeArray);
return root;
}
var constructMaximumBinaryTree = function (nums) {
return buildTreeRecursive(nums);
};
LeetCode-617.合并二叉树
给你两棵二叉树: root1 和 root2 。
想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。 返回合并后的二叉树。
如图所示,实际上就是两棵树同时递归,然后创建对应的节点,正确的连接起来就可以了。此外,本题还可以用迭代来完成,利用队列,成对的加入对应节点,就像之前做过的判断数相同一样,不过使用迭代时,你就不能创建一棵新树了,因为你无法在创建一个节点后将其左子树连接到该节点上,因为你还没创建左子树。
递归
递归分析:
- 返回值和入参:返回当前两棵树的对应节点所构造新子树,入参为左树节点,右树节点
- 终止条件:两节点都为空时,返回空节点
- 单层递归逻辑:
- 创建合并后的节点
- 一个存在一个不存在,新节点值等于存在的
- 两个都存在,新节点值等于和
- 递归创建左右子树
- 创建合并后的节点
实现:
function buildTreeRecursive(leftTreeNode, rightTreeNode) {
//----终止条件
if (!leftTreeNode && !rightTreeNode) return null;
//单层递归逻辑
//创建合并后的节点
let root = new TreeNode((leftTreeNode ? leftTreeNode.val : 0) + (rightTreeNode ? rightTreeNode.val : 0));
//指定节点的左右子树
root.left = buildTreeRecursive(leftTreeNode ? leftTreeNode.left : null, rightTreeNode ? rightTreeNode.left : null);
root.right = buildTreeRecursive(leftTreeNode ? leftTreeNode.right : null, rightTreeNode ? rightTreeNode.right : null);
return root;
}
var mergeTrees = function (root1, root2) {
return buildTreeRecursive(root1, root2)
};
迭代
迭代的方法使用队列辅助,由于不能指定新节点的左右孩子节点,因此方案是将第二棵树的值合并到第一棵树上;
但是这里会遇到一个问题,如何判断这种情况:!nodeA && nodeB 即第一棵树的当前节点为空,第二颗不为空,此时如果创建一个新节点,但是没有父节点连接他,就会出现问题,所以需要知道父节点,但是这种遍历方式又不支持,因此我们只能每次判断当前节点的左右子节点。
实现:保证循环不变量:每轮循环nodeA和nodeB都是存在的;
var mergeTrees = function (root1, root2) {
const quene = [];
if (!root1 && !root2) return root1;
if (root1 && !root2) return root1;
if (!root1 && root2) return root2;
quene.push(root1);
quene.push(root2);
//保证每次入队的节点都存在
while (quene.length) {
const nodeA = quene.shift();
const nodeB = quene.shift();
//先合并
nodeA.val += nodeB.val;
//新节点入队
//nodeA和nodeB都有左右子节点,成对入队
if (nodeA.left && nodeB.left) {
quene.push(nodeA.left);
quene.push(nodeB.left);
}
if (nodeA.right && nodeB.right) {
quene.push(nodeA.right);
quene.push(nodeB.right);
}
//nodeA没有,nodeB有,则将nodeB的子树迁移到nodeA下
if (!nodeA.left && nodeB.left) {
nodeA.left = nodeB.left;
}
if (!nodeA.right && nodeB.right) {
nodeA.right = nodeB.right;
}
//对于nodeA 和 nodeB 都没有子节点 以及 只有nodeA有子节点的情况->不用入队处理
}
return root1;
};
总结
对于构造二叉树类的题目:
- 使用递归的方法通常比较简单
- 递归构造类似于先序遍历,先根据条件构造一个子树的根节点,然后在通过递归指定其左右子树