一、题目内容 中等

给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。

示例1:

4. 从中序与后序遍历序列构造二叉树(106) - 图1 输入:inorder = [9,3,15,20,7], postorder = [9,15,7,20,3] 输出:[3,9,20,null,null,15,7]

示例2:

输入:inorder = [-1], postorder = [-1] 输出:[-1]

提示:

  • 1 <= inorder.length <= 3000
  • postorder.length == inorder.length
  • -3000 <= inorder[i], postorder[i] <= 3000
  • inorder 和 postorder 都由 不同 的值组成
  • postorder 中每一个值都在 inorder 中
  • inorder 保证是树的中序遍历
  • postorder 保证是树的后序遍历

二、解题思路

首先回忆一下如何根据两个顺序构造一个唯一的二叉树,相信理论知识大家应该都清楚,
就是以 后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来在切后序数组。
一层一层切下去,每次后序数组最后一个元素就是节点元素。

image.png

说到一层一层切割,就应该想到了递归。
来看一下一共分几步:

  • 第一步:如果数组大小为零的话,说明是空节点了。
  • 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
  • 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
  • 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
  • 第五步:切割后序数组,切成后序左数组和后序右数组
  • 第六步:递归处理左区间和右区间
  1. var buildTree = function (inorder, postorder) {
  2. // 第一步
  3. if (!postorder.length) return null
  4. // 第二步:后序遍历数组最后一个元素,就是当前的中间节点
  5. const nodeVal = postorder[postorder.length - 1]
  6. const node = new TreeNode(nodeVal)
  7. // 当该节点是叶子节点,直接返回
  8. if (postorder.length === 1) return root
  9. // 第三步:找切割点
  10. const mid = inorder.indexOf(nodeVal)
  11. // 第四步:切割中序数组,得到 中序左数组和中序右数组
  12. // 第五步:切割后序数组,得到 后序左数组和后序右数组
  13. // 第六步
  14. root.left = buildTree(中序左数组,后序左数组)
  15. root.right = buildTree(中序右数组,后序右数组)
  16. return root
  17. };

难点大家应该发现了,就是如何切割,以及边界值找不好很容易乱套。
此时应该注意确定切割的标准,是左闭右开,还有左开又闭,还是左闭又闭,这个就是不变量,要在递归中保持这个不变量。
在切割的过程中会产生四个区间,把握不好不变量的话,一会左闭右开,一会左闭又闭,必然乱套!

首先要切割中序数组,为什么先切割中序数组呢?
切割点在后序数组的最后一个元素,就是用这个元素来切割中序数组的,所以必要先切割中序数组。
中序数组相对比较好切,找到切割点(后序数组的最后一个元素)在中序数组的位置,然后切割,如下代码中坚持左闭右开的原则:

  1. // 找到切割点
  2. const mid = inorder.indexOf(nodeVal)
  3. // 左闭右开区间:[0, mid)
  4. const leftInorder = inorder.slice(0, mid)
  5. // [mid + 1, end)
  6. const rightInorder = inorder.slice(mid + 1)

后序数组的切割点怎么找?
后序数组没有明确的切割元素来进行左右切割,不像中序数组有明确的切割点,切割点左右分开就可以了。
此时有一个很重要的点,就是中序数组大小一定是和后序数组的大小相同的(这是必然)。
中序数组我们都切成了左中序数组和右中序数组了,那么后序数组就可以按照左中序数组的大小来切割,切成左后序数组和右后序数组。

  1. // postorder 舍弃末尾元素,因为这个元素就是中间节点,已经用过了
  2. postorder.pop()
  3. // 左闭右开,使用切割后的中序数组的大小,作为切割点。[0, leftInorder.length]
  4. const leftPostorder = postorder.slice(0, leftInorder.length)
  5. // [leftInorder.length, end]。这里的 leftInorder.length 不加 1,因为不像中序数组需要去掉中间切割点
  6. const rightPostorder = postorder.slice(leftInorder.length)

三、具体代码

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val, left, right) {
  4. * this.val = (val===undefined ? 0 : val)
  5. * this.left = (left===undefined ? null : left)
  6. * this.right = (right===undefined ? null : right)
  7. * }
  8. */
  9. /**
  10. * @param {number[]} inorder
  11. * @param {number[]} postorder
  12. * @return {TreeNode}
  13. */
  14. var buildTree = function (inorder, postorder) {
  15. if (!inorder.length) return null
  16. const nodeVal = postorder[postorder.length - 1]
  17. const mid = inorder.indexOf(nodeVal)
  18. const root = new TreeNode(nodeVal)
  19. const leftInorder = inorder.slice(0, mid)
  20. const rightInorder = inorder.slice(mid + 1)
  21. postorder.pop()
  22. const leftPostorder = postorder.slice(0, leftInorder.length)
  23. const rightPostorder = postorder.slice(leftInorder.length)
  24. root.left = buildTree(leftInorder, leftPostorder)
  25. root.right = buildTree(rightInorder, rightPostorder)
  26. return root
  27. };

上面的代码,每次递归,我们都需要新建数组,这样十分消耗内存。
我们使用数组下标的方式,缩小数组区间,来优化性能。

  1. const createTree = (inorder, inorderStart, inorderEnd, postorder, postStart, postEnd) => {
  2. if (postStart === postEnd) return null
  3. const nodeVal = postorder[postEnd - 1]
  4. const root = new TreeNode(nodeVal)
  5. const mid = inorder.indexOf(nodeVal)
  6. if (postEnd - postStart === 1) return root
  7. // 分割中序数组
  8. // 左中序区间,左闭右开 [inorderStart, mid)
  9. const leftInorderStart = inorderStart
  10. const leftInorderEnd = mid
  11. // 右中序区间,左闭右开[mid + 1, rightInorderEnd)
  12. const rightInorderStart = mid + 1
  13. const rightInorderEnd = inorderEnd
  14. // 切割后序数组
  15. // 左后序区间,左闭右开 [postStart, postStart + mid - inorderStart)
  16. const leftPostStart = postStart
  17. const leftPostEnd = postStart + mid - inorderStart
  18. // 右后序区间,左闭右开 [postStart + mid - inorderStart, postEnd - 1)
  19. const rightPostStart = postStart + mid - inorderStart
  20. const rightPostEnd = postEnd - 1 // 减一,目的把用了的后序元素去除
  21. root.left = createTree(inorder, leftInorderStart, leftInorderEnd, postorder, leftPostStart, leftPostEnd)
  22. root.right = createTree(inorder, rightInorderStart, rightInorderEnd, postorder, rightPostStart, rightPostEnd)
  23. return root
  24. }
  25. /**
  26. * @param {number[]} inorder
  27. * @param {number[]} postorder
  28. * @return {TreeNode}
  29. */
  30. var buildTree = function (inorder, postorder) {
  31. if (!inorder.length) return null
  32. return createTree(inorder, 0, inorder.length, postorder, 0, postorder.length)
  33. };

四、其他解法

五、从前序与后序遍历序列构造二叉树(105)

  1. var buildTree = function (preorder, inorder) {
  2. if (!preorder.length) return null
  3. const nodeVal = preorder.shift()
  4. const root = new TreeNode(nodeVal)
  5. const mid = inorder.indexOf(nodeVal)
  6. const leftInorder = inorder.slice(0, mid)
  7. const rightInorder = inorder.slice(mid + 1)
  8. const leftPreorder = preorder.slice(0, leftInorder.length)
  9. const rightPreorder = preorder.slice(leftInorder.length)
  10. root.left = buildTree(leftPreorder, leftInorder)
  11. root.right = buildTree(rightPreorder, rightInorder)
  12. return root
  13. };