[Watermelon Brother Said Algorithm] Construct binary tree from preorder and inorder traversal sequence

Get into the habit of writing together! This is the 9th day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

Hello everyone, I am the front-end watermelon brother. Today we will talk about a somewhat difficult binary tree algorithm problem: constructing a binary tree from a preorder and inorder traversal sequence.

Given two integer arrays preorder and inorder, where preorder is a preorder traversal of a binary tree and inorder is an inorder traversal of the same tree, construct a binary tree and return its root node.

Example 1:

输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
复制代码

Example 2:

输入: preorder = [-1], inorder = [-1]
输出: [-1]
复制代码

LeetCode topic address:

leetcode-cn.com/problems/co…

ideas

The core of this problem is to make good use of the preorder traversal and inorder traversal characteristics of the binary tree.

Let's look at this binary tree in our example.

Its preorder traversal is:[3,9,20,15,7]

The in-order traversal is:[9,3,15,20,7]

The characteristic of preorder traversal is to visit the root node first, and then visit the left and right nodes. So in the preorder traversal array, the first element is the root node of the entire tree .

Preorder traversal removes the remaining nodes after the first element. In fact, it is possible to find an index position and divide these nodes. After the division, the left side is the left node set, and the right side is the right node set.

Let's look at in-order traversal, what are the characteristics of in-order traversal. In-order traversal traversal first visits the left node, then the root node, and finally the right node.

Earlier, we knew what the root node was through preorder traversal, and then we found the root node position in inorder traversal.

At this time, the left side of the root node position is all nodes of the left subtree of the root node (because of 左->根->右the ), and we can also calculate the number of left subtrees at this time.

After getting the number of left subtrees, we can go back to the preorder traversal to calculate the subarray of the left subtree.

Here we get the preorder traversal array and the inorder traversal array of the left subtree.

Hey, isn't this a nesting doll? Next, we pass these two arrays into the recursive function, and the recursion is formed.

The same is true for the right subtree, so I won’t go into details here.

Code

Let me show you my code implementation.

function buildTree(preorder, inorder{
  if (preorder.length === 0return null;
  const first = preorder[0];
  const root = new TreeNode(first);
  // 根节点在中序遍历中的位置
  const idx = inorder.indexOf(first);

  root.left = buildTree(
    preorder.slice(1, idx + 1),
    inorder.slice(0, idx)
  );
  root.right = buildTree(
    preorder.slice(idx + 1),
    inorder.slice(idx + 1)
  );
  return root;
};

复制代码

每次我们找到中序遍历中根节点的位置 idx,找到数组的切割位置。分别对 preorder 和 inorder 进行切割,找到左子树和右子树各自的前序遍历和中序遍历数组,然后接着递归。递归结束条件为数组为空。

这种实现的优点是可读性好,不容易写错。

但从效率上,它可以更好,有两个地方可以改进:

  • 每次都要拷贝旧数组生成一个新数组,其实这里我们可以通过维护两对数组开头和结束索引来避免拷贝

  • 每次都要遍历 inorder 数组,来找出根节点的位置,效率较低。这点可以用哈希表缓存值到索引的映射

我并不喜欢这种极致的优化导致的可读性下降。不过我还是得和你们说说优化思路的。

用了这两个方案后,我就要用一个新的递归函数了,因为参数变了。在这里,你可以给递归函数_buildTree 或 MyBuildTree 或者 f(函数的意思)、r(递归的意思)。

这里的命名我都不满意,我还是想用 buildTree。要是 JavaScript 也支持 Java 的那种真正的多态写法就好。Java Script 你这个冒牌 Java。

function buildTree(preorder, inorder{
  const map = {};
  for (let i = 0; i < inorder.length; i++) {
    map[inorder[i]] = i;
  }
  return _buildTree(
      preorder, inorder, map,
      0, preorder.length,
      0, inorder.length
  );
};

function _buildTree(preorder, inorder, map, pL, pR, iL, iR{
  if (pL >= pR) return null;
  const first = preorder[pL];
  const root = new TreeNode(first);
  const idx = map[first];
  const leftSize = idx - iL;

  root.left = _buildTree(
    preorder, inorder, map,
    pL + 1, pL + 1 + leftSize,
    iL, iL + leftSize
  );
  root.right = _buildTree(
    preorder, inorder, map,
    pL + leftSize + 1, pR,
    idx + 1, iR
  );
  return root;
};

复制代码

这种实现的递归函数参数非常多,眼花缭乱,而且计算索引时也非常容易写错,但相比第一种实现确实运行效率更高。

结尾

代码是写给人看的,不是写给机器看的,只是顺便计算机可以执行而已。

在可读性和性能上,我们需要根据场景进行权衡。

如果是业务逻辑代码,对性能没有极致的要求,请写给人看的代码,可读性优先。

如果是底层的注重性能的非业务代码,比如像是 C++ 的 STL 库,那就写出极致性能的代码,可读性可以适当妥协。但这要求你花费更多时间去编写代码,且需要有足够的测试用例来保证正确性。

如果你去面试做算法题,不要强求自己一次写出完美的最佳实现。写出第一版后,再在原来的基础上一点点优化。面试官想要考察你的代码优化能力和思考。

我是前端西瓜哥,欢迎关注我。

本文首发于我的公众号:前端西瓜哥

Guess you like

Origin juejin.im/post/7085672379837317151