【问题标题】:Understanding stack unwinding in recursion (tree traversal)了解递归中的堆栈展开(树遍历)
【发布时间】:2014-02-27 07:37:16
【问题描述】:

我正在编写一个程序来遍历二叉搜索树。这是我的代码:

Main.java

public class Main {

public static void main(String[] args) {
 BinaryTree binaryTree = new BinaryTree();
binaryTree.add(50);
binaryTree.add(40);
binaryTree.add(39);
binaryTree.add(42);
binaryTree.add(41);
binaryTree.add(43);
binaryTree.add(55);
binaryTree.add(65);
binaryTree.add(60);
binaryTree.inOrderTraversal(binaryTree.root);
} 
}

Node.java

 public class Node {
 int data;
 Node left;
 Node right;
 Node parent;


public Node(int d)
{
data = d;
left = null;
right = null;
}
}

BinaryTree.java

public class BinaryTree {
Node root = null;
public void add(int d)
{
Node newNode =  new Node(d);
if(root!=null)
{


    Node futureParent = root;
    while(true)
    {
    if(newNode.data < futureParent.data)      //going left
    {
        if(futureParent.left == null)
        {
            futureParent.left = newNode;
            newNode.parent = futureParent;
            break;
        }
        futureParent = futureParent.left;

    }
    else
    {
        if(futureParent.right == null)
        {
            futureParent.right = newNode;
            newNode.parent = futureParent;
            break;
        }
        futureParent = futureParent.right;
    }

    }

}
else
{
    root = newNode;
}
}
public void inOrderTraversal(Node node)
{
if(node!=null)
{
inOrderTraversal(node.left);
System.out.println(node.data);
inOrderTraversal(node.right);
}
}
}

我完全理解加法过程,但我无法理解遍历。 现在,为了更好地参考,我正在使用的树是这样的:

inOrderTraversal() 函数中的第一条语句访问 50,40 然后 39 最后命中 null 使 if 条件为 false 之后打印 39 并搜索右孩子。此后第一条语句停止执行并且堆栈展开第二和第三条语句(inOrderTraversal(node.right)print(node.data)),这导致打印 40 并遍历到 41,这是我不明白的部分,即编译器如何在语句 1(inOrderTraversal(node.left))停止后重新启动它堆栈中有新内容时立即执行。

【问题讨论】:

  • 您可以使用 IDE 的调试器来检查堆栈是如何创建的,并查看每个堆栈都有自己的副本

标签: java algorithm tree binary-search-tree inorder


【解决方案1】:

通过思考经典的递归示例,阶乘,您可以更清楚地了解递归和堆栈。

int factorial(x) {
   int result;
   if(x==1) 
       result = 1;
   else
       result = x * factorial(x - 1);
   return result;
 }

(我使用了result 变量,以便在手动单步执行代码时更容易标记位置)

使用纸片手动执行factorial(5)

首先将函数写在一张纸上,将“x”替换为 5。然后通读它,当你遇到函数调用时,在你的执行点上画一个铅笔标记,然后得到一张新的新函数调用的论文。

每次执行此操作时,将新纸放在前一张纸上。从字面上看,这是一叠纸,它准确地代表了一个计算机堆栈。每张纸都是一个堆栈条目,它记录了你在代码中的位置,以及局部变量的值是什么,当你创建它时。

了解这对于递归函数调用来说并不特殊,这一点很重要。所有函数调用都以这种方式创建一个堆栈条目。

程序执行无法浏览堆栈。只能访问最上面的那张纸 -- 后进先出 (LIFO)。当您到达factorial(1) 时,它不会再次调用自己,而您会到达return。发生这种情况时,丢弃最上面的一张纸,将返回值写入新的顶层,然后从您放置铅笔标记的位置继续执行顶层的函数。

这样继续下去,最终你会丢弃最后一张纸。这意味着您的程序已经完成并且您有一个最终结果。

顺便说一句,如果你的代码有问题,比如函数不调用自身,你会用完纸(或者你的纸堆会到达天花板)——这就是 堆栈溢出,此站点以该名称命名。堆栈变得大于规定的最大值,并且运行时拒绝再次调用该函数(在 Java 中,通过抛出异常)。您可能会在您的编程生涯中遇到这种情况——常见原因是编码错误的停止条件,或循环使用循环数据结构。

使用上面的实现,factorial(0) 可能会导致堆栈溢出。你知道为什么吗?

这是所有传统计算机程序的运行方式。您将一项放在堆栈上(在 C 和 Java 中,即main())。每次进行函数调用时,堆栈都会增长,而每次函数完成时,堆栈就会缩小。堆栈不断增长和缩小,直到最终缩小到零,此时程序完成。

对于像你这样的程序,在同一个函数中有两个递归调用,没有什么不同。这是一个很好的练习,可以用纸手动运行一个小的二叉树搜索,就像我们对 factorial() 所做的那样,看看它是否有效。

在调试器中暂停您的 Java 代码以查看当前堆栈的状态也很有指导意义——或者如果您不能这样做(尽快学会使用调试器!)在您的某处放置 Thread.dumpStack()代码来看看它的输出。

【讨论】:

    【解决方案2】:

    您的代码不能按原样工作,它将永远在节点 39 上迭代。方法 inOrderTraversal() 确实会转到左侧节点,但由于这段时间将永远循环在它上面。每个堆栈帧都有自己的变量副本。进入方法时,变量节点获取作为参数传递的对象引用的副本。

    考虑递归的一种方法是类似于使用 while 循环,但你有一个 if 而不是 while。该方法应如下所示:

        public void inOrderTraversal(Node node) {
        if (node != null) {
            inOrderTraversal(node.left);
            System.out.println(node.data);
            inOrderTraversal(node.right);
        }
    }
    

    当你遍历树时,你想先打印较小的值,它存储在最左边的节点中,所以你使用inOrderTraversal(node.left); 来获取 if。当你到达一个空节点时,这意味着它的父节点是最左边的节点,所以你打印它。之后,您转到正确的节点并重复该过程。这就像将树拆分为较小的子树,直到您无法再拆分它们并打印它们的值。

    每次调用方法(递归或非递归)时,都会分配一个新的堆栈帧(压入堆栈),在方法完成后,堆栈会被移除(弹出),从而释放空间用于垃圾回收。这些堆栈帧只是局部变量所在的临时空间。对象成员变量存在于另一个称为堆的地方,它的生命周期比堆栈长。

    JVM 处理这些空间的分配,垃圾收集器根据对象/变量的生命周期释放它们。根据他们的寿命,有几代人(这就是他们所说的)。一切都从伊甸园(年轻)一代开始,如果垃圾收集器因为它们还活着而没有回收空间,它们就会被移到幸存者代,如果它们仍然没有被收集,它们会移到最后一个,终身的一代。对象存活的时间越长,GC 对其进行检查的次数就越少。这意味着虽然伊甸园中的对象被收集得非常快,但其余代的检查并不那么频繁。还有另一个称为永久代 (permgen) 的空间,用于存在常量(如字符串文字)和存储类的地方。

    【讨论】:

    • 方法调用不会创建新堆栈。它将条目添加到现有堆栈中。否则,拥有堆栈而不仅仅是一些随机访问数据结构有什么用?
    • 你是对的,它创建了一个新的堆栈帧,它被放在现有的堆栈上。只有一层。
    【解决方案3】:

    首先,inOrderTraversal 中的while 语句是错误的。 while 循环中的所有语句都不会修改变量node,所以如果它是null,它将永远是,如果不是,它永远不会。将其更改为 if

    因此,查看递归的方法通常是归纳。我主张以下归纳假设:

    给定以node 为根的树TinOrderTraversal(node) 打印T 的中序遍历并返回。

    我们可以通过归纳证明这确实发生了。最简单的情况是node == null。这样的话inOrderTraversal什么都不打印直接返回,符合假设。

    现在,假设我们传递一棵非空树。通过归纳,inOrderTraversal(node.left) 打印左子树并返回。然后,node.data 被打印出来。最后,再次通过归纳,inOrderTraversal(node.right) 打印右子树并返回。请注意,到目前为止,当前树是按顺序遍历打印的。由于我将while 更改为if,因此该方法返回,从而满足归纳假设。

    这能回答你的问题吗?

    【讨论】:

    • 您对 if 语句的看法是正确的,我对其进行了更改。但是您给出的答案不是我要寻找的答案。我想在内存/堆栈级别上更多地理解它,至于为什么编译器是否停止执行语句 1(左)然后突然又开始执行。
    猜你喜欢
    • 2014-12-21
    • 1970-01-01
    • 2015-03-02
    • 1970-01-01
    • 2011-03-13
    • 2016-08-24
    • 1970-01-01
    • 2022-11-21
    • 1970-01-01
    相关资源
    最近更新 更多