二叉树19:构造二叉树两道题

Posted 纵横千里,捭阖四方

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二叉树19:构造二叉树两道题相关的知识,希望对你有一定的参考价值。

  1. 今天我们要解决两道构造二叉树的题;

106.从中序与后序遍历序列构造二叉树

105.从前序与中序遍历序列构造二叉树

先记住一个结论:根据前序和中序可以唯一构造一棵二叉树,根据中中序和后序也能唯一构造一个棵二叉树,但是前序和后序不行。下面分析

1.leetcode106.从中序与后序遍历序列构造二叉树

根据一棵树的中序遍历与后序遍历构造二叉树。

你可以假设树中没有􏰀复的元素。例如,给出

中序遍历 inorder = [9,3,15,20,7] 后序遍历 postorder = [9,15,7,20,3] 返回如下的二叉树:

1.1 构造过程

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

 解决此问题的关键在于要很熟悉树的各种遍历次序代表的什么,最好能够将图画出来。本题解带你先进行中序遍历和后续遍历二叉树,然后再根据遍历结果将二叉树进行还原。

首先,来一棵树

然后再看树的遍历结果:

根据中序和后序遍历结果还原二叉树
中序遍历和后续遍历的特性
首先来看题目给出的两个已知条件 中序遍历序列 和 后序遍历序列 根据这两种遍历的特性我们可以得出两个结论

  1. 在后序遍历序列中,最后一个元素为树的根节点
  2. 在中序遍历序列中,根节点的左边为左子树,根节点的右边为右子树

 

树的还原过程描述
根据中序遍历和后续遍历的特性我们进行树的还原过程分析

  1. 首先在后序遍历序列中找到根节点(最后一个元素)
  2. 根据根节点在中序遍历序列中找到根节点的位置
  3. 根据根节点的位置将中序遍历序列分为左子树和右子树
  4. 根据根节点的位置确定左子树和右子树在中序数组和后续数组中的左右边界位置
  5. 递归构造左子树和右子树
  6. 返回根节点结束

树的还原过程变量定义
需要定义几个变量帮助我们进行树的还原

  1. HashMap memo 需要一个哈希表来保存中序遍历序列中,元素和索引的位置关系.因为从后序序列中拿到根节点后,要在中序序列中查找对应的位置,从而将数组分为左子树和右子树
  2. int ri 根节点在中序遍历数组中的索引位置
  3. 中序遍历数组的两个位置标记 [is, ie],is 是起始位置,ie 是结束位置
  4. 后序遍历数组的两个位置标记 [ps, pe] ps 是起始位置,pe 是结束位置

位置关系的计算
在找到根节点位置以后,我们要确定下一轮中,左子树和右子树在中序数组和后续数组中的左右边界的位置。

  1. 左子树-中序数组 is = is, ie = ri - 1
  2. 左子树-后序数组 ps = ps, pe = ps + ri - is - 1 (pe计算过程解释,后续数组的起始位置加上左子树长度-1 就是后后序数组结束位置了,左子树的长度 = 根节点索引-左子树)
  3. 右子树-中序数组 is = ri + 1, ie = ie
  4. 右子树-后序数组 ps = ps + ri - is, pe - 1

听不明白没关系,看图就对了,计算图示如下:

树的还原过程:

 最后看代码:

class Solution {

    HashMap<Integer,Integer> memo = new HashMap<>();
    int[] post;

    public TreeNode buildTree(int[] inorder, int[] postorder) {
        for(int i = 0;i < inorder.length; i++) memo.put(inorder[i], i);
        post = postorder;
        TreeNode root = buildTree(0, inorder.length - 1, 0, post.length - 1);
        return root;
    }

    public TreeNode buildTree(int is, int ie, int ps, int pe) {
        if(ie < is || pe < ps) return null;

        int root = post[pe];
        int ri = memo.get(root);

        TreeNode node = new TreeNode(root);
        node.left = buildTree(is, ri - 1, ps, ps + ri - is - 1);
        node.right = buildTree(ri + 1, ie, ps + ri - is, pe - 1);
        return node;
    }
}

2.LeetCode105 从前序和中序遍历序列构造二叉树

在LeetCode网站看到一个非常详细的解析,也有点烧脑,我还没有完全吃透,先直接原文贴过来了。

https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by--22/

解法一、递归

先序遍历的顺序是根节点,左子树,右子树。中序遍历的顺序是左子树,根节点,右子树。

所以我们只需要根据先序遍历得到根节点,然后在中序遍历中找到根节点的位置,它的左边就是左子树的节点,右边就是右子树的节点。

生成左子树和右子树就可以递归的进行了。

比如上图的例子,我们来分析一下。

preorder = [3,9,20,15,7]
inorder = [9,3,15,20,7]
首先根据 preorder 找到根节点是 3
    
然后根据根节点将 inorder 分成左子树和右子树
左子树
inorder [9]

右子树
inorder [15,20,7]

把相应的前序遍历的数组也加进来
左子树
preorder[9] 
inorder [9]

右子树
preorder[20 15 7] 
inorder [15,20,7]

现在我们只需要构造左子树和右子树即可,成功把大问题化成了小问题
然后重复上边的步骤继续划分,直到 preorder 和 inorder 都为空,返回 null 即可

事实上,我们不需要真的把 preorder 和 inorder 切分了,只需要用分别用两个指针指向开头和结束位置即可。注意下边的两个指针指向的数组范围是包括左边界,不包括右边界。

对于下边的树的合成。

左子树

右子树:

public TreeNode buildTree(int[] preorder, int[] inorder) {
    return buildTreeHelper(preorder, 0, preorder.length, inorder, 0, inorder.length);
}

private TreeNode buildTreeHelper(int[] preorder, int p_start, int p_end, int[] inorder, int i_start, int i_end) {
    // preorder 为空,直接返回 null
    if (p_start == p_end) {
        return null;
    }
    int root_val = preorder[p_start];
    TreeNode root = new TreeNode(root_val);
    //在中序遍历中找到根节点的位置
    int i_root_index = 0;
    for (int i = i_start; i < i_end; i++) {
        if (root_val == inorder[i]) {
            i_root_index = i;
            break;
        }
    }
    int leftNum = i_root_index - i_start;
    //递归的构造左子树
    root.left = buildTreeHelper(preorder, p_start + 1, p_start + leftNum + 1, inorder, i_start, i_root_index);
    //递归的构造右子树
    root.right = buildTreeHelper(preorder, p_start + leftNum + 1, p_end, inorder, i_root_index + 1, i_end);
    return root;
}

上边的代码很好理解,但存在一个问题,在中序遍历中找到根节点的位置每次都得遍历中序遍历的数组去寻找,参考这里 ,我们可以用一个HashMap把中序遍历数组的每个元素的值和下标存起来,这样寻找根节点的位置就可以直接得到了。

public TreeNode buildTree(int[] preorder, int[] inorder) {
    HashMap<Integer, Integer> map = new HashMap<>();
    for (int i = 0; i < inorder.length; i++) {
        map.put(inorder[i], i);
    }
    return buildTreeHelper(preorder, 0, preorder.length, inorder, 0, inorder.length, map);
}

private TreeNode buildTreeHelper(int[] preorder, int p_start, int p_end, int[] inorder, int i_start, int i_end,
                                 HashMap<Integer, Integer> map) {
    if (p_start == p_end) {
        return null;
    }
    int root_val = preorder[p_start];
    TreeNode root = new TreeNode(root_val);
    int i_root_index = map.get(root_val);
    int leftNum = i_root_index - i_start;
    root.left = buildTreeHelper(preorder, p_start + 1, p_start + leftNum + 1, inorder, i_start, i_root_index, map);
    root.right = buildTreeHelper(preorder, p_start + leftNum + 1, p_end, inorder, i_root_index + 1, i_end, map);
    return root;
}

本以为已经完美了,在 这里 又看到了令人眼前一亮的思路,就是 StefanPochmann 大神,经常逛 Discuss 一定会注意到他,拥有 3 万多的赞。

他也发现了每次都得遍历一次去找中序遍历数组中的根节点的麻烦,但他没有用 HashMap就解决了这个问题,下边来说一下。

用pre变量保存当前要构造的树的根节点,从根节点开始递归的构造左子树和右子树,in变量指向当前根节点可用数字的开头,然后对于当前pre有一个停止点stop,从in到stop表示要构造的树当前的数字范围。

public TreeNode buildTree(int[] preorder, int[] inorder) {
    return buildTreeHelper(preorder,  inorder, (long)Integer.MAX_VALUE + 1);
}
int pre = 0;
int in = 0;
private TreeNode buildTreeHelper(int[] preorder, int[] inorder, long stop) {
    //到达末尾返回 null
    if(pre == preorder.length){
        return null;
    }
    //到达停止点返回 null
    //当前停止点已经用了,in 后移
    if (inorder[in] == stop) {
        in++;
        return null;
    }
    int root_val = preorder[pre++];
    TreeNode root = new TreeNode(root_val);   
    //左子树的停止点是当前的根节点
    root.left = buildTreeHelper(preorder,  inorder, root_val);
    //右子树的停止点是当前树的停止点
    root.right = buildTreeHelper(preorder, inorder, stop);
    return root;
}

代码很简洁,但如果细想起来真的很难理解了。

把他的原话也贴过来吧。

Consider the example again. Instead of finding the 1 in inorder, splitting the arrays into parts and recursing on them, just recurse on the full remaining arrays and stop when you come across the 1 in inorder. That's what my above solution does. Each recursive call gets told where to stop, and it tells its subcalls where to stop. It gives its own root value as stopper to its left subcall and its parent`s stopper as stopper to its right subcall.

本来很想讲清楚这个算法,但是各种画图,还是太难说清楚了。这里就画几个过程中的图,大家也只能按照上边的代码走一遍,理解一下了。 

      3
    /   \\
   9     7
  / \\
 20  15
 
前序遍历数组和中序遍历数组
preorder = [ 3, 9, 20, 15, 7 ]
inorder = [ 20, 9, 15, 3, 7 ]   
p 代表 pre,i 代表 in,s 代表 stop

首先构造根节点为 3 的树,可用数字是 i 到 s
s 初始化一个树中所有的数字都不会相等的数,所以代码中用了一个 long 来表示
3, 9, 20, 15, 7 
^  
p
20, 9, 15, 3, 7
^              ^
i              s

考虑根节点为 3 的左子树,                考虑根节点为 3 的树的右子树,
stop 值是当前根节点的值 3                只知道 stop 值是上次的 s
新的根节点是 9,可用数字是 i 到 s 
不包括 s
3, 9, 20, 15, 7                       3, 9, 20, 15, 7                
   ^                                    
   p
20, 9, 15, 3, 7                       20, 9, 15, 3, 7                     
^          ^                                         ^
i          s                                         s

递归出口的情况
3, 9, 20, 15, 7 
       ^  
       p
20, 9, 15, 3, 7
^    
i   
s   
此时 in 和 stop 相等,表明没有可用的数字,所以返回 null,并且表明此时到达了某个树的根节点,所以 i 后移。

总之他的思想就是,不再从中序遍历中寻找根节点的位置,而是直接把值传过去,表明当前子树的结束点。不过总感觉还是没有 get 到他的点,in 和 stop 变量的含义也是我赋予的,对于整个算法也只是勉强说通,大家有好的想法可以和我交流。

解法二、迭代 栈


参考 这里,我们可以利用一个栈,用迭代实现。

假设我们要还原的树是下图

      3
    /   \\
   9     7
  / \\
 20  15

首先假设我们只有先序遍历的数组,如果还原一颗树,会遇到什么问题。

preorder = [3, 9, 20, 15, 7 ]

首先我们把 3 作为根节点,然后到了 9 ,就出现一个问题,9 是左子树还是右子树呢?

所以需要再加上中序遍历的数组来确定。

inorder = [ 20, 9, 15, 3, 7 ]

我们知道中序遍历,首先遍历左子树,然后是根节点,最后是右子树。这里第一个遍历的是 20 ,说明先序遍历的 9 一定是左子树,利用反证法证明。

假如 9 是右子树,根据先序遍历 preorder = [ 3, 9, 20, 15, 7 ],说明根节点 3 的左子树是空的,左子树为空,那么中序遍历就会先遍历根节点 3,而此时是 20,假设不成立,说明 9 是左子树。接下来的 20 同理,所以可以目前构建出来的树如下。

      3
    /   
   9    
  / 
 20  

同时,还注意到此时先序遍历的 20 和中序遍历 20 相等了,说明什么呢?

说明中序遍历的下一个数 15 不是左子树了,如果是左子树,那么中序遍历的第一个数就不会是 20。

所以 15 一定是右子树了,现在还有个问题,它是 20 的右子树,还是 9 的右子树,还是 3 的右子树?

我们来假设几种情况,来想一下。

如果是 3 的右子树, 20 和 9 的右子树为空,那么中序遍历就是20 9 3 15。

如果是 9 的右子树,20 的右子树为空,那么中序遍历就是20 9 15。

如果是 20 的右子树,那么中序遍历就是20 15。

之前已经遍历的根节点是 3 9 20,把它倒过来,即20 9 3,然后和上边的三种中序遍历比较,会发现 15 就是最后一次相等的节点的右子树。

  1. 第 1 种情况,中序遍历是20 9 3 15,和20 9 3 都相等,所以 15 是3 的右子树。
  2. 第 2 种情况,中序遍历是20 9 15,只有20 9 相等,所以 15 是 9 的右子树。
  3. 第 3 种情况,中序遍历就是20 15,只有20 相等,所以 20 是 15 的右子树。

而此时我们的中序遍历数组是inorder = [ 20, 9 ,15, 3, 7 ],20 匹配,9匹配,最后一次匹配是 9,所以 15 是 9的右子树。 

     3
    /   
   9    
  / \\
 20  15

综上所述,我们用一个栈保存已经遍历过的节点,遍历前序遍历的数组,一直作为当前根节点的左子树,直到当前节点和中序遍历的数组的节点相等了,那么我们正序遍历中序遍历的数组,倒着遍历已经遍历过的根节点(用栈的 pop 实现),找到最后一次相等的位置,把它作为该节点的右子树。

上边的分析就是迭代总体的思想,代码的话还有一些细节注意一下。用一个栈保存已经遍历的节点,用 curRoot 保存当前正在遍历的节点。

public TreeNode buildTree(int[] preorder, int[] inorder) {
    if (preorder.length == 0) {
        return null;
    }
    Stack<TreeNode> roots = new Stack<TreeNode>();
    int pre = 0;
    int in = 0;
    //先序遍历第一个值作为根节点
    TreeNode curRoot = new TreeNode(preorder[pre]);
    TreeNode root = curRoot;
    roots.push(curRoot);
    pre++;
    //遍历前序遍历的数组
    while (pre < preorder.length) {
        //出现了当前节点的值和中序遍历数组的值相等,寻找是谁的右子树
        if (curRoot.val == inorder[in]) {
            //每次进行出栈,实现倒着遍历
            while (!roots.isEmpty() && roots.peek().val == inorder[in]) {
                curRoot = roots.peek();
                roots.pop();
                in++;
            }
            //设为当前的右孩子
            curRoot.right = new TreeNode(preorder[pre]);
            //更新 curRoot
            curRoot = curRoot.right;
            roots.push(curRoot);
            pre++;
        } else {
            //否则的话就一直作为左子树
            curRoot.left = new TreeNode(preorder[pre]);
            curRoot = curRoot.left;
            roots.push(curRoot);
            pre++;
        }
    }
    return root;
}

用常规的递归和 HashMap 做的话这道题是不难的,用 stop 变量省去 HashMap 的思路以及解法二的迭代可以了解一下吧,不是很容易想到。

以上是关于二叉树19:构造二叉树两道题的主要内容,如果未能解决你的问题,请参考以下文章

根据二叉树的先序遍历和中序遍历建立二叉树

[98]验证二叉搜索树&[105]从前序与中序遍历序列构造二叉树

模板已知二叉树两种序列求另外一种序列--非建树

二叉树9:二叉树的最大深度

重构二叉树&&判断二叉树的子结构

LeetCode Java刷题笔记—105. 从前序与中序遍历序列构造二叉树