【算法18天:Day18】第六章二叉树 LeetCode 从中序与后序遍历序列构造二叉树(106)

170 阅读5分钟

题目四:

image.png

解法一:(递归)(根据示例输入画图比划)

解题思路:以后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来在切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。

  • 第一步:如果数组大小为零的话,说明是空节点了。
  • 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
  • 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
  • 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
  • 第五步:切割后序数组,切成后序左数组和后序右数组
  • 第六步:递归处理左区间和右区间

如何切割,以及边界值找不好很容易乱套。

此时应该注意确定切割的标准,是左闭右开,还有左开右闭,还是左闭右闭,这个就是不变量,要在递归中保持这个不变量。

在切割的过程中会产生四个区间,把握不好不变量的话,一会左闭右开,一会左闭右闭,必然乱套!

首先要切割中序数组,为什么先切割中序数组呢?

切割点在后序数组的最后一个元素,就是用这个元素来切割中序数组的,所以必要先切割中序数组。

中序数组相对比较好切,找到切割点(后序数组的最后一个元素)在中序数组的位置,然后切割。

接下来就要切割后序数组了。

首先后序数组的最后一个元素指定不能要了,这是切割点 也是 当前二叉树中间节点的元素,已经用了。

后序数组的切割点怎么找?

后序数组没有明确的切割元素来进行左右切割,不像中序数组有明确的切割点,切割点左右分开就可以了。

此时有一个很重的点,就是中序数组大小一定是和后序数组的大小相同的(这是必然)。

中序数组我们都切成了左中序数组和右中序数组了,那么后序数组就可以按照左中序数组的大小来切割,切成左后序数组和右后序数组。

此时,中序数组切成了左中序数组和右中序数组,后序数组切割成左后序数组和右后序数组。接下来可以递归了。

完整代码:

var buildTree = function(inorder, postorder) {
    if (!inorder.length) return null;
    const rootVal = postorder.pop(); // 从后序遍历的数组中获取中间节点的值, 即数组最后一个值
    let rootIndex = inorder.indexOf(rootVal); // 获取中间节点在中序遍历中的下标
    const root = new TreeNode(rootVal); // 创建中间节点
    root.left = buildTree(inorder.slice(0, rootIndex), postorder.slice(0, rootIndex)); // 创建左节点
    root.right = buildTree(inorder.slice(rootIndex + 1), postorder.slice(rootIndex)); // 创建右节点
    return root;
};

前序和中序可以唯一确定一棵二叉树。

后序和中序可以唯一确定一棵二叉树。

那么前序和后序可不可以唯一确定一棵二叉树呢?

前序和后序不能唯一确定一棵二叉树! ,因为没有中序遍历无法确定左右部分,也就是无法分割。

举一个例子:

前序和中序可以唯一确定一棵二叉树。

后序和中序可以唯一确定一棵二叉树。

那么前序和后序可不可以唯一确定一棵二叉树呢?

前序和后序不能唯一确定一棵二叉树! ,因为没有中序遍历无法确定左右部分,也就是无法分割。

举一个例子:

image.png

tree1 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。

tree2 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。

那么tree1 和 tree2 的前序和后序完全相同,这是一棵树么,很明显是两棵树!

所以前序和后序不能唯一确定一棵二叉树!

解法二:参考他人

image.png

有了左右子树的 postOrder 和 inorder 之后,就能继续递归构建左、右子树,一直递归下去,直到:无法形成 postOrder 和 inorder 数组,就构建不出子树了,即来到树的底部了,返回 null 节点。

递归函数可以选择接受数组本身,也可以接收指针,我采用后者,根据指针 iStart 到指针 iEnd 的 inorder 数组,和从 pStart 到 pEnd 的 postorder 数组,构建当前子树,避免每次递归都要切割字符串。

定位root在inorder数组中的位置 我用了一个 map 去提前存下所有节点值在 inorder 数组中的索引,这样就不用每次都花 O(n) 的时间去定位 root 的位置。(不用类似 indexOf 这样的库函数),用空间换取时间。

const buildTree = (inorder, postorder) => {
  const map = {};
  for (let i = 0; i < inorder.length; i++) { // 将节点值在inorder数组中的位置提前存入map
    map[inorder[i]] = i;
  }
  // 根据iStart到iEnd的inorder数组,和从pStart到pEnd的postorder数组构建当前子树
  const helper = (iStart, iEnd, pStart, pEnd) => { 
    if (pStart > pEnd || iStart > iEnd) { // 指针交错了,返回null节点
      return null;
    }
    const rootVal = postorder[pEnd]; // 获取当前要构建的根节点的值
    const mid = map[rootVal];        // 获取到它在inorder数组中的位置
    const leftNodeNum = mid - iStart; // 获取左子树的节点个数

    const root = new TreeNode(rootVal); // 创建节点
    root.left = helper(iStart, mid - 1, pStart, pStart + leftNodeNum - 1); // 用递归构建左子树
    root.right = helper(mid + 1, iEnd, pStart + leftNodeNum, pEnd - 1); // 用递归构建右子树

    return root; // 返回当前构建好的子树
  };

  return helper(0, inorder.length - 1, 0, postorder.length - 1); // 递归的入口
};