大家好,我是挨打的阿木木,爱好算法的前端摸鱼老。最近会频繁给大家分享我刷算法题过程中的思路和心得。如果你也是想提高逼格的摸鱼老,欢迎关注我,一起学习。
题目
105. 从前序与中序遍历序列构造二叉树
给定一棵树的前序遍历 preorder 与中序遍历 inorder。请构造二叉树并返回其根节点。
示例 1:
Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
Output: [3,9,20,null,null,15,7]
示例 2:
Input: preorder = [-1], inorder = [-1]
Output: [-1]
提示:
1 <= preorder.length <= 3000inorder.length == preorder.length-3000 <= preorder[i], inorder[i] <= 3000preorder和inorder均无重复元素inorder均出现在preorderpreorder保证为二叉树的前序遍历序列inorder保证为二叉树的中序遍历序列
思路
-
做这道题目之前一定要先搞懂什么是前序遍历,什么是中序遍历,如果还不知道的小伙伴们可以从我这篇文章看起,[路飞]_一文彻底搞懂二叉树的前序、中序、后序遍历;总结一下就是:
(1)前序遍历: 先访问根结点,然后遍历左子树,最后遍历右子树。
(2)中序遍历: 先遍历左子树,然后访问根结点,最后遍历右子树。
-
那么我们的根节点已经找到了,就是前序遍历
preorder中的第一个元素,然后再去中序遍历inorder中找到根节点所在的位置index; -
这样子从前序遍历
preorder索引1开始,到index中的元素就是左子树,index后的元素就是右子树。而且索引1就是根节点的左子节点,索引index + 1就是根节点的右子节点, 这样子就可以递归求解了; -
但是每一轮我们找索引可能会很麻烦,由于题目中已经保证了不存在重复元素,那么我们可以建立起一个
map,去维护值和索引之间的映射关系。
暴力实现
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} preorder
* @param {number[]} inorder
* @return {TreeNode}
*/
function buildTree(preorder, inorder) {
// 前序遍历的第一个节点是根节点
const node = new TreeNode(preorder[0]);
// 找到根节点在中序遍历中的位置
let index = inorder.findIndex(v => v === preorder[0]);
// 如果中序遍历左边有元素,说明有左子节点,递归寻找
if (index > 0) {
node.left = buildTree(preorder.slice(1, index + 1), inorder.slice(0, index));
}
// 如果中序遍历右边有元素,说明有右子节点,递归寻找
if (index < preorder.length - 1) {
node.right = buildTree(preorder.slice(index + 1), inorder.slice(index + 1));
}
return node;
}
优化
每次我们都需要去遍历找根节点在中序遍历中的index, 有点不方便,我们可以一开始用map保存起来中序遍历的节点的关系,但是用了这个方式每次就不能切割数组了,只能缓存原数组的开始坐标位置和结束坐标位置,用官方的图就是这样子的一个公式。
优化代码
var buildTree = function(preorder, inorder) {
let map = new Map(inorder.map((item, index) => [item, index]));
return getChildNode(preorder, map, 0, preorder.length - 1, 0, inorder.length - 1);
};
function getChildNode(preorder, map, pLeft, pRight, iLeft) {
// 前序遍历边界溢出即结束
if (pLeft > pRight) {
return null;
}
// 前序遍历第一个节点就是根节点
const node = new TreeNode(preorder[pLeft]);
// 找到根节点在中序遍历中的位置
let index = map.get(preorder[pLeft]);
// 把原本的切割数组递归改成用指针替代
node.left = getChildNode(preorder, map, pLeft + 1, index - iLeft + pLeft, iLeft);
node.right = getChildNode(preorder, map, index - iLeft + pLeft + 1, pRight, index + 1);
return node;
}
结果
栈解法
思路
- 用栈
stack保存当前所有可能包含右子节点的节点,每次新增左子节点时候入栈,然后去匹配在中序遍历中,如果下一个值不是栈中的上一个节点,说明有右节点; - 每次遇到节点,判断它是否中序遍历的第一个节点,如果不是,说明还有左子节点,入栈;
- 如果是的话,取出当前节点,然后判断当前节点有没有右子节点,有的话建立起当前节点和右子节点的关系,然后把右子节点入栈做判断。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} preorder
* @param {number[]} inorder
* @return {TreeNode}
*/
var buildTree = function(preorder, inorder) {
// 存放前序遍历中未用到的项
let stack = [];
// 存放中序遍历的索引
let index = 0;
// 加个伪节点,不用每次都判断是否起点, 起个不可能出现的数字
let root = new TreeNode(NaN);
stack.push(root);
// 前序遍历跑一遍
while (preorder.length) {
// 判断是右节点还是左节点
if (stack[stack.length - 1].val === inorder[index]) {
let prev = stack.pop();
index++;
// 如果中序遍历到了根节点,那么我们需要判断有没有右节点
// 即stack中的下个节点如果不是中序遍历的下个节点,说明中间有右节点
if (stack[stack.length - 1].val !== inorder[index]) {
const nextNode = new TreeNode(preorder.shift());
prev.right = nextNode;
stack.push(nextNode);
}
} else {
// 如果是左节点,新建节点,构造关系,入栈即可
// 新建节点
const nextNode = new TreeNode(preorder.shift());
// 构造关系
stack[stack.length - 1].left = nextNode;
// 入栈
stack.push(nextNode);
}
}
return root.left;
};
看懂了的小伙伴可以点个关注、咱们下道题目见。如无意外以后文章都会以这种形式,有好的建议欢迎评论区留言。