【问题标题】:Understanding the logic in iterative Postorder traversal implementation on a Binary tree理解二叉树上迭代后序遍历实现的逻辑
【发布时间】:2018-03-21 14:37:00
【问题描述】:

我试图了解使用 2 个堆栈实现后序遍历是如何如此直观。某人是如何想出它的,它只是一种观察或某种特定的思维方式,可以帮助人们想出这种方法。如果是,请解释如何朝着正确的方向思考。

【问题讨论】:

  • 你了解递归遍历吗?迭代方法只是使用显式堆栈来执行递归方法隐式执行的操作。不过,我不会称之为直观。有关该方法的讨论,请参阅geeksforgeeks.org/iterative-postorder-traversal。另请注意,有一个单栈解决方案。

标签: data-structures tree binary-tree traversal tree-traversal


【解决方案1】:

让我解释一下我是如何偶然发现解决方案的:

您从这个简单的观察开始:从表面上看,前序遍历和后序遍历仅在操作顺序上有所不同

preOrder(node):
    if(node == null){
         return
    }
    visit(node)
    preOrder(node.leftChild)
    preOrder(node.rightChild)

postOrder(node):
    if(node == null){
         return
    }
    postOrder(node.leftChild)
    postOrder(node.rightChild)
    visit node

preOrder 函数执行一些计算(访问节点)和对自身的两次递归调用 - 让我们将此订单称为“VLR”。 postOrder 函数进行两次递归调用,然后访问节点('LRV')。

preOrder 适合于简单的迭代实现,我们在其中访问节点,并将与递归调用对应的计算推送到堆栈上。

    var nodeStack = new Stack();
    nodeStack.push(this.root())
    while(!nodeStack.isEmpty()){
        var currentNode = nodeStack.pop();
        visit(currentNode);
        if (currentNode.rightChild){
            nodeStack.push(currentNode.rightChild)
        }

        if (currentNode.leftChild){
            nodeStack.push(currentNode.leftChild)
        }
    }

堆栈包含未访问节点的列表。我们弹出一个节点,访问它,推动它的右孩子,推动左孩子。重复。这给了我们预购。

(注意:对于 VLR,我们将右孩子推到左孩子之前,因为堆栈是 LIFO - 我们在访问之前推的孩子之前访问最后推的孩子 - 我们希望在右孩子之前访问左孩子孩子。对于 VRL,我们将不得不颠倒顺序)

假设我们这样做,我们推右孩子,推左孩子,然后才访问节点(有点像(1)递归调用和(2)postOrder中的节点访问的相对顺序。我们尝试使VLR 到 LRV 通过在末尾移动 V)。

    var nodeStack = new Stack();
    nodeStack.push(this.root())
    while(!nodeStack.isEmpty()){
        var currentNode = nodeStack.pop();
        if (currentNode.rightChild){
            nodeStack.push(currentNode.rightChild)
        }

        if (currentNode.leftChild){
            nodeStack.push(currentNode.leftChild)
        }
        visit(currentNode);
    }   

这应该给我们postOrder,对吧?不,这仍然是预购。定义 preOrder 的不是我们在将子节点压入堆栈之前访问了一个节点,而是我们在从堆栈中永久删除该节点(并因此访问它之后)之后弹出(并因此访问了)堆栈上的子节点。堆栈中的孩子最终在某个时候被弹出和访问,但他们的父级已经被弹出和访问 - 因此这是 preOrder。这种方法似乎是一条死胡同。

但还有另一种方法!我们可以通过颠倒 VLR 中 L 和 R 的顺序(使其成为 VRL),将“VLR”变为“LRV”,然后将得到的“VRL”颠倒为将其设为“LRV”。

从左到右的 postOrder 遍历 (LRV) 生成的序列与 'VRL' 的逆向相同 - 从右到左的 preOrder 遍历生成的序列。

让我们通过 VRL 来调整我们的迭代 preOrder:

    var nodeStack = new Stack();
    nodeStack.push(this.root())
    while(!nodeStack.isEmpty()){
        var currentNode = nodeStack.pop();

        if (currentNode.leftChild){
            nodeStack.push(currentNode.leftChild)
        }

        if (currentNode.rightChild){
            nodeStack.push(currentNode.rightChild)
        }

        visit(currentNode);
    }   

这将给我们一个 VRL preOrder(一个反向为 LRV,postOrder 的序列)。 我们只需要反向遍历它! 这就是需要第二个堆栈的地方。如果我们将访问的每个节点推送到另一个堆栈,然后从上到下遍历它,我们将获得我们的 postOrder!

这是我的最终解决方案:

    var reverseStack = new Stack();
        while(!nodeStack.isEmpty()){
            var currentNode = nodeStack.pop()
            reverseStack.push(currentNode)
            if (currentNode.leftChild){
                nodeStack.push(currentNode.leftChild)
            }

            if (currentNode.rightChild){
                nodeStack.push(currentNode.rightChild)
            }
        }

        while(!reverseStack.isEmpty()){
            console.log(reverseStack.pop().getElement())
        }

您可以进行一些小的优化 - 这将为您提供您在网上看到的标准两栈解决方案。请注意,两个堆栈不是必需的 - 可以只使用一个堆栈来实现 postOrder - 例如,如果我们跟踪访问的最后一个节点。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多