【问题标题】:How to construct BST given post-order traversal如何在给定后序遍历的情况下构造 BST
【发布时间】:2012-10-21 11:27:21
【问题描述】:

我知道有一些方法可以从前序遍历(作为数组)构造一棵树。考虑到中序和前序遍历,更常见的问题是构造它。在这种情况下,虽然中序遍历是多余的,但它肯定会让事情变得更容易。谁能给我一个想法如何进行后序遍历?需要迭代和递归解决方案。

我尝试使用堆栈迭代地执行此操作,但根本无法正确逻辑,所以得到了一个可怕的混乱树。递归也是如此。

【问题讨论】:

标签: algorithm recursion binary-tree binary-search-tree


【解决方案1】:

如果你有一个来自 BST 的后序遍历的数组,你就知道根是数组的最后一个元素。根的左孩子占据了数组的第一部分,并且由小于根的条目组成。然后是右孩子,由大于根的元素组成。 (两个孩子都可能是空的)。

________________________________
|             |              |R|
--------------------------------
 left child     right child   root

所以主要问题是找到左孩子结束和右孩子开始的点。

这两个孩子也是从他们的后序遍历中获得的,因此以相同的方式递归地构造它们。

BST fromPostOrder(value[] nodes) {
    // No nodes, no tree
    if (nodes == null) return null;
    return recursiveFromPostOrder(nodes, 0,  nodes.length - 1);
}

// Construct a BST from a segment of the nodes array
// That segment is assumed to be the post-order traversal of some subtree
private BST recursiveFromPostOrder(value[] nodes, 
                                   int leftIndex, int rightIndex) {
    // Empty segment -> empty tree
    if (rightIndex < leftIndex) return null;
    // single node -> single element tree
    if (rightIndex == leftIndex) return new BST(nodes[leftIndex]);

    // It's a post-order traversal, so the root of the tree 
    // is in the last position
    value rootval = nodes[rightIndex];

    // Construct the root node, the left and right subtrees are then 
    // constructed in recursive calls, after finding their extent
    BST root = new BST(rootval);

    // It's supposed to be the post-order traversal of a BST, so
    // * left child comes first
    // * all values in the left child are smaller than the root value
    // * all values in the right child are larger than the root value
    // Hence we find the last index in the range [leftIndex .. rightIndex-1]
    // that holds a value smaller than rootval
    int leftLast = findLastSmaller(nodes, leftIndex, rightIndex-1, rootval);

    // The left child occupies the segment [leftIndex .. leftLast]
    // (may be empty) and that segment is the post-order traversal of it
    root.left = recursiveFromPostOrder(nodes, leftIndex, leftLast);

    // The right child occupies the segment [leftLast+1 .. rightIndex-1]
    // (may be empty) and that segment is the post-order traversal of it
    root.right = recursiveFromPostOrder(nodes, leftLast + 1, rightIndex-1);

    // Both children constructed and linked to the root, done.
    return root;
}

// find the last index of a value smaller than cut in a segment of the array
// using binary search
// supposes that the segment contains the concatenation of the post-order
// traversals of the left and right subtrees of a node with value cut,
// in particular, that the first (possibly empty) part of the segment contains
// only values < cut, and the second (possibly empty) part only values > cut
private int findLastSmaller(value[] nodes, int first, int last, value cut) {

    // If the segment is empty, or the first value is larger than cut,
    // by the assumptions, there is no value smaller than cut in the segment,
    // return the position one before the start of the segment
    if (last < first || nodes[first] > cut) return first - 1;

    int low = first, high = last, mid;

    // binary search for the last index of a value < cut
    // invariants: nodes[low] < cut 
    //             (since cut is the root value and a BST has no dupes)
    // and nodes[high] > cut, or (nodes[high] < cut < nodes[high+1]), or
    // nodes[high] < cut and high == last, the latter two cases mean that
    // high is the last index in the segment holding a value < cut
    while (low < high && nodes[high] > cut) {

        // check the middle of the segment
        // In the case high == low+1 and nodes[low] < cut < nodes[high]
        // we'd make no progress if we chose mid = (low+high)/2, since that
        // would then be mid = low, so we round the index up instead of down
        mid = low + (high-low+1)/2;

        // The choice of mid guarantees low < mid <= high, so whichever
        // case applies, we will either set low to a strictly greater index
        // or high to a strictly smaller one, hence we won't become stuck.
        if (nodes[mid] > cut) {
            // The last index of a value < cut is in the first half
            // of the range under consideration, so reduce the upper
            // limit of that. Since we excluded mid as a possible
            // last index, the upper limit becomes mid-1
            high = mid-1;
        } else {
            // nodes[mid] < cut, so the last index with a value < cut is
            // in the range [mid .. high]
            low = mid;
        }
    }
    // now either low == high or nodes[high] < cut and high is the result
    // in either case by the loop invariants
    return high;
}

【讨论】:

  • 你能解释一下你的算法吗?一些内联 cmets 会很好......
  • 你来了,添加 cmets 甚至发现了一个多余的if
  • findLastSmaller(nodes, 0, nodes.length - 2, rootval); 行不是findLastSmaller(nodes, leftindex, rightindex - 2, rootval);吗?
  • 它是rightIndex-1,因为根值位于rightIndex。但原则上,是的。当我决定不需要特别处理根并且忘记调整参数时,我将调用从公共转移到递归。
  • @MohitJain O(n*log n)。对于每个节点,我们需要一个复杂的二分搜索O(log n) [更清晰的界限是O(log t),其中t 是根是我们当前正在处理的节点的子树的大小,这不会改变复杂性,然而,它只产生一个常数因子]。
【解决方案2】:

您实际上并不需要中序遍历。仅给定后序遍历,有一种简单的方法可以重建树:

  1. 取输入数组中的最后一个元素。这是根。
  2. 遍历剩余的输入数组,寻找元素从小于根变为更大的点。在该点拆分输入数组。这也可以通过二分搜索算法来完成。
  3. 从这两个子数组递归重建子树。

这可以很容易地使用堆栈递归或迭代地完成,并且您可以使用两个索引来指示当前子数组的开始和结束,而不是实际拆分数组。

【讨论】:

  • 你先生是个天才
【解决方案3】:

后序遍历是这样的:

visit left
visit right
print current.

这样的顺序:

visit left
print current
visit right

举个例子:

        7
     /     \
    3      10
   / \     / \
  2   5   9   12
             /
            11

顺序为:2 3 5 7 9 10 11 12

后序为:2 5 3 9 11 12 10 7

以相反的顺序迭代后序数组,并继续围绕该值所在的位置拆分中序数组。递归地执行此操作,这将是您的树。例如:

current = 7, split inorder at 7: 2 3 5 | 9 10 11 12

看起来很眼熟?左边是左子树,右边是右子树,就 BST 结构而言是伪随机顺序。但是,您现在知道您的根是什么。现在对两半做同样的事情。在后序遍历中从左半部分开始查找元素的第一次出现(从末尾开始)。那将是 3。在 3 左右拆分:

current = 3, split inorder at 3: 2 | 5 ...

所以你知道你的树到目前为止看起来像这样:

   7
 /
3

这是基于这样一个事实,即后序遍历中的值将始终在其子项出现之后出现,并且中序遍历中的值将出现在其子项值之间。

【讨论】:

    【解决方案4】:

    不要循环任何东西。 最后一个元素是你的根。 然后将数组向后取,遵循 BST 的插入规则。

    eg:-   
    given just the postorder -- 2 5 3 9 11 12 10 7
    
    
    
            7
             \
              10
    
            ----
            7
             \
              10
               \
                12
             -----
            7
             \
              10
               \
                12
               /
              11
             -------
            7
             \
              10
             /  \
            9    12
               /
              11
             --------
            7
          /  \
         3    10
        / \  /  \
       2   5 9  12
               /
              11
    

    【讨论】:

    • 这个答案以某种方式给出了正确的结果,但我找不到证据甚至直觉。这总能给出正确答案吗?
    • 是的。它会。总是。只需要一点点认识。
    • '不要循环任何东西' - 这是什么意思?你不是为每个元素循环遍历同一棵树吗?它的时间复杂度是多少?
    【解决方案5】:

    没有一个答案显示工作代码或提供时间复杂度分析,从来没有hammar's brilliant answer。挥手让我很困扰,所以让我们开始谈些更正式的事情吧。

    Hammer 在 Python 中的解决方案:

    def from_postorder(nodes: Sequence[int]) -> BinaryTree[int]:
        def build_subtree(subtree_nodes: Sequence[int]) -> BinaryTree[int]:
            if not subtree_nodes:
                return None
    
            n = len(subtree_nodes)
            # Locates the insertion point for x to maintain sorted order.
            # This is the first element greater than root.
            x = bisect.bisect_left(subtree_nodes, subtree_nodes[-1], hi=n - 1)
    
            root = BinaryTree(subtree_nodes[-1])
            root.left = build_subtree(subtree_nodes[:x])
            # slice returns empty list if end is <= start
            root.right = build_subtree(subtree_nodes[x:n - 1])
    
            return root
    
        return build_subtree(nodes)
    

    在每一步,二分查找都需要log(n) 时间,我们将问题减少一个元素(根)。因此,整体时间复杂度为nlog(n)

    替代解决方案:

    我们创建两个数据结构,一个是 BST 的中序遍历,另一个是每个节点到其在后序遍历序列中的索引的映射。

    对于形成子树的给定范围的节点,根是在后序遍历中最后出现的节点。 为了有效地找到根,我们使用之前创建的映射将每个节点映射到它的索引,然后找到最大值。

    找到根后,我们在中序遍历序列中进行二分查找;从给定范围的下界到根的左侧的元素形成其左子树,从根的右侧到范围的右边界的元素形成其右子树。我们在左右子树上递归。

    换句话说,我们使用后序遍历序列找到根,使用中序遍历序列找到左右子树。

    时间复杂度: 在每一步,找到根需要O(n) 时间。二进制搜索需要log(n) 时间。我们还将问题分成两个大致相等的子问题(完整 BST 的最坏情况)。因此,T(n) &lt;= 2 . T(n/2) + O(n) + log(n) = T(n/2) + O(n),使用 Master 定理给了我们O(n log(n))

    def from_postorder_2(nodes: Sequence[int]) -> BinaryTree[int]:
        inorder: Sequence[int] = sorted(nodes)
        index_map: Mapping[int, int] = dict([(x, i) for i, x in enumerate(nodes)])
    
        # The indices refer to the inorder traversal sequence
        def build_subtree(lo: int, hi: int) -> BinaryTree[int]:
            if hi <= lo:
                return None
            elif hi - lo == 1:
                return BinaryTree(inorder[lo])
    
            root = max(map(lambda i: index_map[inorder[i]], range(lo, hi)))
            root_node = BinaryTree(nodes[root])
            x = bisect.bisect_left(inorder, root_node.val, lo, hi)
            root_node.left = build_subtree(lo, x)
            root_node.right = build_subtree(x + 1, hi)
    
            return root_node
    
        return build_subtree(0, len(nodes))
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-07-14
      • 1970-01-01
      • 2018-11-06
      • 1970-01-01
      • 1970-01-01
      • 2018-06-29
      • 1970-01-01
      相关资源
      最近更新 更多