Skip to content

Latest commit

 

History

History
167 lines (118 loc) · 10.3 KB

04.Binary-Tree-Reduction.md

File metadata and controls

167 lines (118 loc) · 10.3 KB

1. 二叉树的还原简介

二叉树的还原:指的是通过二叉树的遍历序列,还原出对应的二叉树。

从二叉树的遍历过程可以看出,给定一棵非空二叉树,它的前序、中序、后续遍历所得到的遍历序列都是唯一的。那么反过来,如果已知节点的某种遍历序列,能否确定这棵二叉树呢?并且确定的二叉树是否是唯一的呢?

我们先来回顾一下二叉树的前序遍历、中序遍历、后序遍历规则。

  • 非空二叉树的前序遍历规则:
    1. 访问根节点。
    2. 以前序遍历的方式遍历根节点的左子树。
    3. 以前序遍历的方式遍历根节点的右子树。
  • 非空二叉树的中序遍历规则:
    1. 以中序遍历的方式遍历根节点的左子树。
    2. 访问根节点。
    3. 以中序遍历的方式遍历根节点的右子树。
  • 非空二叉树的后序遍历规则:
    1. 以后序遍历的方式遍历根节点的左子树。
    2. 以后序遍历的方式遍历根节点的右子树。
    3. 访问根节点。

先来看二叉树的前序遍历,前序遍历过程中首先访问的是根节点,所以通过前序遍历序列,我们可以确定序列的第 $1$ 个节点肯定是根节点。但是从第 $2$ 个节点开始就不确定它是根节点的左子树还是根节点的右子树了。所以单凭前序遍历序列是无法恢复一棵二叉树的。

再来看二叉树的后序遍历,后序遍历也是只能确定序列的最后一个节点为根节点,而无法确定其他节点在二叉树中的位置。所以单凭后序遍历序列也是无法恢复一棵二叉树的。

最后我们来看二叉树的中序遍历,中序遍历是先遍历根节点的左子树,然后访问根节点,最后遍历根节点的右子树。这样,根节点在中序遍历序列中必然将中序序列分割成前后两个子序列,其中前一个子序列是根节点的左子树的中序遍历序列,后一个子序列是根节点的右子树的中序遍历序列。当然单凭中序遍历序列也是无法恢复一棵二叉树的。

但是如果我们可以将「前序遍历序列」和「中序遍历序列」相结合,那么我们就可以通过上面中序遍历序列中的两个子序列,在前序遍历序列中找到对应的左子序列和右子序列。在前序遍历序列中,左子序列的第 $1$ 个节点是左子树的根节点,右子序列的第 $1$ 个节点是右子树的根节点。这样,就确定了二叉树的 $3$ 个节点。

同时,左子树和右子树的根节点在中序遍历序列中又可以将左子序列和右子序列分别划分成两个子序列。如此递归下去,当确定了前序遍历序列中的所有节点时,我们就得到了一棵二叉树。

还有一个问题,通过前序序列和中序序列还原的二叉树是唯一的吗?

这个唯一性可以利用归纳法加以证明。感兴趣的读者可以试试自己证明或者参考有关资料。

通过上述过程说明:如果已知一棵二叉树的前序序列和中序序列,可以唯一地确定这棵二叉树。

同理,如果已知一棵二叉树的中序序列和后序序列,也可以唯一地确定这棵二叉树。 方法和通过二叉树的前序序列和中序序列构造二叉树类似,唯一不同点在于二叉树的根节点是根据后序遍历序列的最后一个元素确定的。

类似的,已知二叉树的「中序遍历序列」和「层序遍历序列」,也可以唯一地确定一棵二叉树。

需要注意的是:如果已知二叉树的「前序遍历序列」和「后序遍历序列」,是不能唯一地确定一棵二叉树的。 这是因为没有中序遍历序列无法确定左右部分,也就无法进行子序列的分割。

只有二叉树中每个节点度为 $2$ 或者 $0$ 的时候,已知前序遍历序列和后序遍历序列,才能唯一地确定一颗二叉树,如果二叉树中存在度为 $1$ 的节点时是无法唯一地确定一棵二叉树的,这是因为我们无法判断该节点是左子树还是右子树。

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

  • 描述:已知一棵二叉树的前序遍历序列和中序遍历序列。
  • 要求:构造出该二叉树。
  • 注意:假设树中没有重复的元素。

2.1 从前序与中序遍历序列构造二叉树实现过程

前序遍历的顺序是:根节点 - 左子树 - 右子树。中序遍历的顺序是:左子树 - 根节点 - 右子树。

根据前序遍历的顺序,可以找到根节点位置。然后在中序遍历的结果中可以找到对应的根节点位置,就可以从根节点位置将二叉树分割成左子树、右子树。同时能得到左右子树的节点个数。

此时构建当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历进行上述步骤,直到节点为空,具体操作步骤如下:

  1. 从前序遍历顺序中得到当前根节点的位置在 $postorder[0]$
  2. 通过在中序遍历中查找上一步根节点对应的位置 $inorder[k]$,从而将二叉树的左右子树分隔开,并得到左右子树节点的个数。
  3. 从上一步得到的左右子树个数将前序遍历结果中的左右子树分开。
  4. 构建当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历并执行上述三步,直到节点为空。

2.2 从前序与中序遍历序列构造二叉树实现代码

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        def createTree(preorder, inorder, n):
            if n == 0:
                return None
            k = 0
            while preorder[0] != inorder[k]:
                k += 1
            node = TreeNode(inorder[k])
            node.left = createTree(preorder[1: k + 1], inorder[0: k], k)
            node.right = createTree(preorder[k + 1:], inorder[k + 1:], n - k - 1)
            return node
        return createTree(preorder, inorder, len(inorder))

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

  • 描述:已知一棵二叉树的中序遍历序列和后序遍历序列。
  • 要求:构造出该二叉树。
  • 注意:假设树中没有重复的元素。

3.1 从中序与后序遍历序列构造二叉树实现过程

中序遍历的顺序是:左子树 - 根节点 - 右子树。后序遍历的顺序是:左子树 - 右子树 - 根节点。

根据后序遍历的顺序,可以找到根节点位置。然后在中序遍历的结果中可以找到对应的根节点位置,就可以从根节点位置将二叉树分割成左子树、右子树。同时能得到左右子树的节点个数。

此时构建当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历进行上述步骤,直到节点为空,具体操作步骤如下:

  1. 从后序遍历顺序中当前根节点的位置在 $postorder[n-1]$
  2. 通过在中序遍历中查找上一步根节点对应的位置 $inorder[k]$,从而将二叉树的左右子树分隔开,并得到左右子树节点的个数。
  3. 从上一步得到的左右子树个数将后序遍历结果中的左右子树分开。
  4. 构建当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历并执行上述三步,直到节点为空。

3.2 从中序与后序遍历序列构造二叉树实现代码

class Solution:
    def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode:
        def createTree(inorder, postorder, n):
            if n == 0:
                return None
            k = 0
            while postorder[n - 1] != inorder[k]:
                k += 1
            node = TreeNode(inorder[k])
            node.right = createTree(inorder[k + 1: n], postorder[k: n - 1], n - k - 1)
            node.left = createTree(inorder[0: k], postorder[0: k], k)
            return node
        return createTree(inorder, postorder, len(postorder))

4. 从前序与后序遍历序列构造二叉树

前边我们说过:已知二叉树的前序遍历序列和后序遍历序列,是不能唯一地确定一棵二叉树的。 而如果不要求构造的二叉树是唯一的,只要求构造出一棵二叉树,还是可以进行构造的。

  • 描述:已知一棵二叉树的前序遍历序列和后序遍历序列。

  • 要求:重构并返回该二叉树。

  • 注意:假设树中没有重复的元素。如果存在多个答案,则可以返回其中任意一个。

4.1 从前序与后序遍历序列构造二叉树实现过程

我们可以默认指定前序遍历序列的第 $2$ 个值为左子树的根节点,由此递归划分左右子序列。具体操作步骤如下:

  1. 从前序遍历序列中可知当前根节点的位置在 $preorder[0]$

  2. 前序遍历序列的第 $2$ 个值为左子树的根节点,即 $preorder[1]$。通过在后序遍历中查找上一步根节点对应的位置 $postorder[k]$(该节点右侧为右子树序列),从而将二叉树的左右子树分隔开,并得到左右子树节点的个数。

  3. 从上一步得到的左右子树个数将后序遍历结果中的左右子树分开。

  4. 构建当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历并执行上述三步,直到节点为空。

4.2 从前序与后序遍历序列构造二叉树实现代码

class Solution:
    def constructFromPrePost(self, preorder: List[int], postorder: List[int]) -> TreeNode:
        def createTree(preorder, postorder, n):
            if n == 0:
                return None
            node = TreeNode(preorder[0])
            if n == 1:
                return node
            k = 0
            while postorder[k] != preorder[1]:
                k += 1
            node.left = createTree(preorder[1: k + 2], postorder[: k + 1], k + 1)
            node.right = createTree(preorder[k + 2: ], postorder[k + 1: -1], n - k - 2)
            return node
        return createTree(preorder, postorder, len(preorder))

参考资料

  1. 【书籍】数据结构教程 第 3 版 - 唐发根 著
  2. 【书籍】算法训练营 陈小玉 著
  3. 【博文】二叉树的构造系列 - 知乎
  4. 【评论】889. 根据前序和后序遍历构造二叉树 - 力扣)