记录 1 道算法题
从前序与中序遍历序列构造二叉树
首先,前序遍历是根左右,中序遍历是左根右。就是前序遍历可以整体看为[根][左][右],中序遍历整体看为[左][根][右]。
a
b e
c d
我们知道前序遍历一定是根节点开头的,例如:a, b, c, d, e,a是根节点,后面的bc可能是左子节点也可能不是。这时候就要与中序遍历进行匹配了。
中序遍历是 c, b, d, a, e
这时候,我们知道某一个根节点,根据中序遍历的规则,根节点的左边就是它的左子节点。这时候我们可以看到 a 节点 左边有 3个节点。说明 节点a 他的左子树及其孩子有 3 个。说明前序遍历里面, 节点a 后面的 b,c,d都是在 节点a 的左边。按照 [根][左][右] 的左右来分,则 节点a 的右边的节点就是中序遍历里面的 a 的 下标 +1, 即 e 的位置,下标为 4。
补充一个细节,就是节点个数是相等的,所以前序遍历和中序遍历的结果存放数组的长度是相等的。
a b c d e
[x [ 左 ] [右]]
c b d a e
[[ 左 ] x [右]]
然后观察一下根节点改变之后有什么变化,在上面的左区间里面,找到新的根节点。当 b 被看作根节点的时候。
a b c d e
[x [左][右]]
c b d a e
[[左]x [右]]
当继续在左区间里面找新的根节点的时候,就是 节点c 。我们会发现 节点c 在中序遍历里面已经没有左子树了。就说明当 中序遍历的根节点的 left 为空的时候,左边的树已经恢复了。就是 [左] [根] [右] 的左已经到尽头了,要找中和右了。
那右边的是怎样的呢。在区间中,出去根节点和左边,剩下的就是右子树及其节点。从上面可以看到,当区间缩小到只剩下 1个节点 的时候,right 就确定了。当然也有可能是没有右节点。这是递归到了最里面要终止开始逐层返回的时候。
看到这种区间一直缩小的,就很适合用递归。用下标来标记区间。
为了避免每次都要在中序遍历里面找根节点的下标,有一个优化就是先遍历一次中序遍历数组,然后记录下每一个 val的下标,到时候之间取。
function buildTree(preorder, inorder) {
const map = {}
// 记录下标
for (let i = 0; i < inorder.length; i++) {
map[inorder[i]] = i
}
const create = (pre, ino, preStart, preEnd, inoStart, inoEnd) => {
// 终止条件,就是区间没有的时候,就是开始下标和结尾下标错开
if (inoStart > inoEnd) {
return null
}
// 找到根 和 中序遍历中 根的下标
const rootVal = pre[preStart]
const rootIdx = map[rootVal]
const root = new TreeNode(rootVal)
// 中序遍历左子树的个数
// 中序遍历的 [左] 区间肯定等于 0,[根下标]
// 前序遍历对应的区间则是 [根下标], count + 根下标
// 区间内容数量是一样的。
const count = rootIdx - inoStart
// 递归开始
// create 指定一个新的根节点
// 个数 = 结尾下标 - 开始下标 - 1 根据这个公式计算区间的结尾
// [左] [根]
root.left = create(pre, ino, preStart + 1, preStart + count,inoStart, rootIdx - 1)
root.right = create(pre, ino, preStart + count + 1, preEnd, rootIdx + 1, inoEnd)
// 返回最开头的根节点
return create(preorder, inorder, 0, preorder.length - 1, 0, inorder.length - 1)
}
}