无递归的二叉树遍历的直观解释

时间:2016-08-02 15:20:55

标签: algorithm recursion tree language-agnostic

我已经看过许多文章和书籍(和Stack Overflow答案),它们展示了如何使用显式堆栈而不是递归来迭代地执行预订,顺序和后序深度优先树遍历。 例如:https://en.wikipedia.org/wiki/Tree_traversal#Depth-first_search_2

前序遍历很简单,但我认为其他的很复杂而且很明显。

是否有任何来源(最好是文章或书籍)直观地解释这些算法,所以你可以看到有人会在一开始就提出这些算法?

2 个答案:

答案 0 :(得分:2)

  • 预购:通过访问节点处理节点,然后处理每个子节点。

  • Inorder:通过处理左子节点,访问节点,然后处理正确的子节点来处理节点。

  • PostOrder(DFS):通过处理每个子节点,然后访问节点来处理节点。

在所有情况下,堆栈用于存储您不能立即完成的工作。预订案例最简单,因为您只需要推迟一种工作 - 处理子节点。

  

预购:堆栈包含要处理的节点。要处理节点,请访问它,在堆栈上推送正确的子节点,然后处理左子节点。如果没有剩下的孩子,那么从筹码中抓一个。

顺序也很简单。堆栈必须存储节点以访问要处理的节点,但要处理的节点始终是刚访问过的节点的正确子节点,因此:

  

Inorder:堆栈包含要访问的节点。当我们从堆栈中获取一个节点时,我们访问它,然后处理它的右子节点。当我们处理一个节点时,我们把它放在堆栈上然后处理它的左子节点。

Postorder比较棘手,因为堆栈必须存储节点才能访问节点进行处理,并且它们并不总是像Inorder情况那样简单相关。堆栈必须以某种方式指示哪个是哪个。

你可以这样做:

  

后序:堆栈包含要访问或处理的节点以及已处理的子节点数。要从堆栈处理条目n,请访问节点(n,x+1),如果它具有< = x children。否则将{{1}}放在堆栈上并处理节点的第一个未处理的子节点。

答案 1 :(得分:2)

如何提出一种无需堆栈的迭代解决方案

实现迭代树遍历不需要堆栈!通过将父指针保留在树节点数据结构中,可以摆脱任何堆栈。这是您的想法:

什么是迭代解决方案?迭代解决方案是这样的解决方案,其中代码的固定部分在循环中重复执行(几乎是迭代的确定性)。回路的输入是系统的状态s1,输出是状态s2,回路将系统从状态s1转移到状态s2。您从初始状态s开始,并在达到最终所需状态s时完成。

所以我们的问题减少到寻找:

    系统状态的表征,可以帮助我们实现这一目标。初始状态将与我们的初始条件一致,而终端状态将与我们的期望结果一致
  • 查找作为循环一部分重复执行的指令

(这实际上将树变成了状态机。)

在遍历树时,每一步都会访问一个节点。树的每个节点最多访问三次-一次从父级访问,一次从leftChild访问,一次从右侧Child访问。我们在特定步骤对节点执行的操作取决于这三种情况中的哪一种。

因此,如果我们捕获所有这些信息:我们正在访问哪个节点,以及在哪种情况下,我们就具有了系统的特征。

捕获此信息的一种方法是存储对先前节点/状态的引用:

Node current;
Node previous;

如果previous = current.parent,则我们正在从父级访问。如果previous = current.leftChild,则从左侧访问;如果previous = current.rightChiild,则从右侧访问。

我们可以捕获此信息的另一种方法:

Node current; 
boolean visitedLeft;
boolean visitedRight;

如果visitedLeft和visitedRight都为false,则我们正在从父级进行访问;如果visitedLeft为true,但visitedRight为false,则从左边进行访问;如果visitedLeft和visitedRight都为true,则从右边进行访问(第四个状态:visitedLeft为false,而visitedRight为false,在preOrder中从未达到)。

最初,我们以viisitedLeft = false,visitedRight = false和current = root开始。遍历完成后,我们期望VisitedLeft = true,visitedRight = true和current = null。

在循环中重复执行的指令中,系统必须从一种状态转移到另一种状态。因此,在说明中,我们只是告诉系统遇到任何状态时要做什么,以及何时结束执行。

您可以将所有三个遍历结合在一个函数中,方法如下:

void traversal(String typeOfTraversal){

    boolean visitedLeft = false;
    boolean visitedRight = false;
    TreeNode currentNode = this.root;

    while(true){

        if (visitedLeft == false && currentNode.leftChild != null){
            if(typeOfTraversal == "preOrder"){
                System.out.println(currentNode.key);
            }
            currentNode = currentNode.leftChild;
            continue;
        }

        if (visitedLeft == false && currentNode.leftChild == null){
            if(typeOfTraversal == "preOrder"){
                System.out.println(currentNode.key);
            }
            visitedLeft = true;
            continue;
        }

        if (visitedLeft == true && visitedRight == false && currentNode.rightChild != null){
            if(typeOfTraversal == "inOrder"){
                System.out.println(currentNode.key);
            }
            currentNode = currentNode.rightChild;
            visitedLeft = false;
            continue;
        }

        if (visitedLeft == true && visitedRight == false && currentNode.rightChild == null){
            if(typeOfTraversal == "inOrder"){
                System.out.println(currentNode.key);
            }
            visitedRight = true;
            continue;
        }

        if (visitedLeft == true && visitedRight == true && currentNode.parent != null){
            if(typeOfTraversal == "postOrder"){
                System.out.println(currentNode.key);
            }

            if (currentNode == currentNode.parent.leftChild){
                visitedRight = false;
            }
            currentNode = currentNode.parent;
        }

        if (visitedLeft == true && visitedRight == true && currentNode.parent == null){       
            if(typeOfTraversal == "postOrder"){
                System.out.println(currentNode.key);
            }
            break; //Traversal is complete.
        }

如果为您提供了节点级锁,则此算法允许并发遍历和更新树。除了分离非叶子节点以外,任何原子操作都是安全的。


如何提出基于堆栈的解决方案

在考虑将递归解决方案转换为迭代解决方案或为递归定义的问题提出迭代解决方案时,堆栈是有用的数据结构。调用堆栈是一种堆栈数据结构,用于存储有关计算机程序活动子例程的信息,这是大多数高级编程语言如何在后台实现递归的方法。因此,在迭代解决方案中显式使用堆栈时,我们只是在模仿编写递归代码时处理器的工作方式。 Matt Timmermans的答案对为什么使用堆栈以及如何提出基于堆栈的显式解决方案提供了很好的直觉。

我已经在这里写了关于如何提出带有两个堆栈的postOrder解决方案的信息:Understanding the logic in iterative Postorder traversal implementation on a Binary tree


基于父指针的方法比基于堆栈的方法消耗更多的内存。在堆栈上,指向尚待处理的节点的指针是临时的,只需要O(log n)堆栈空间即可,因为您只需要为树下的单个路径保留足够多的指针即可(实际上,则可能会更少)。相比之下,将父指针与节点一起存储需要固定的O(n)空间。