【问题标题】:Recursive functional explanation in binary tree二叉树中的递归函数解释
【发布时间】:2011-11-04 08:58:40
【问题描述】:

我正在阅读二叉树的教程。
而且我在使用递归函数时有点卡住了。比如说我需要计算树中的节点数

int countNodes( TreeNode *root )    
{   
       // Count the nodes in the binary tree to which  
       // root points, and return the answer.  
    if ( root == NULL )  
       return 0;  // The tree is empty.  It contains no nodes.  
    else
   {  
       int count = 1;   // Start by counting the root.  
       count += countNodes(root->left);  // Add the number of nodes   
                                        //     in the left subtree.   
       count += countNodes(root->right); // Add the number of nodes   
                                        //    in the right subtree.  
       return count;  // Return the total.  
    }  
 }   // end countNodes()

现在我的疑问是-> root->left->left of right 怎么算?或root->right->left->left?? 谢谢

【问题讨论】:

标签: c++ c binary-tree


【解决方案1】:

使用递归函数,您应该递归思考!这是我对这个函数的看法:

  • 我开始写函数的签名,也就是

    int countNodes( TreeNode *root ) 
    
  • 首先,不递归的情况。例如,如果给定的树是NULL,那么没有节点,所以我返回0。

  • 然后,我观察到我的树中的节点数是左子树的节点数加上右子树的节点数加上 1(根节点)。因此,我基本上调用左右节点的函数并添加值加 1。
    • 请注意,我假设该功能已经正常工作!

我为什么要这样做?很简单,该函数应该适用于任何二叉树,对吧?那么,根节点的左子树,其实是一棵二叉树!右子树也是二叉树。因此,我可以放心地假设使用相同的 countNodes 函数我可以计算这些树的节点。拥有它们后,我只需添加 left+right+1 即可得到结果。

递归函数是如何工作的?您可以使用笔和纸来遵循算法,但简而言之,它是这样的:

假设你用这棵树调用函数:

  a
 / \
b   c
   / \
  d   e

你看到根不为空,所以你调用左子树的函数:

b

然后是右子树

   c
  / \
 d   e

在调用右子树之前,需要评估左子树。

所以,你正在调用带有输入的函数:

b

你看到根不为空,所以你调用左子树的函数:

NULL

返回0,右子树:

NULL

它也返回 0。你计算树的节点数,它是 0+0+1 = 1。

现在,原始树的左子树为 1,它是

b

函数被调用

   c
  / \
 d   e

在这里,您再次为左子树调用该函数

d

类似于b的情况,返回1,然后是右子树

e

它也返回 1,您将树中的节点数计算为 1+1+1 = 3。

现在,您返回函数的第一次调用,并将树中的节点数计算为 1+3+1 = 5。

如您所见,对于每个左右,您再次调用该函数,如果它们有左或右孩子,则该函数被一次又一次地调用,并且每次它在树中更深。因此,root->left->leftroot->right->left->left 不是直接评估,而是在后续调用之后进行评估。

【讨论】:

  • 评论太好了。我想我现在知道编码在做什么了。我会再次查看代码,如果卡住了,我会再次 ping 你们,但 atm 似乎我已经理解了你的解释
  • @to 所有人,然后在下面的函数中 void printTree(struct node* node) { if (node == NULL) return;打印树(节点->左); printf("%d", 节点->数据);打印树(节点->右);在这个函数中,当它在 b 之后遇到 NUll 时,它会遇到 node == NULL 并因此返回(从函数退出)因此下面的代码如 printf 将不会执行,对吗?
  • @samprat,没错。当你从函数返回时,你只是返回并且不执行后面的任何内容。
【解决方案2】:

这基本上就是递归的作用,每次调用 countNodes 时它都会加 1,因为它到达子节点 (int count = 1;) 并在尝试转到 a 的下一个子节点时终止叶节点(因为叶没有子节点)。每个节点递归地为它的左右子节点调用 countNodes 并且计数缓慢增加并冒泡到顶部。

尝试这样看,其中每个节点添加 1,递归停止的不存在节点添加 0:

          1
        /   \
       1     1
      / \   / \
     1   0 0   0
    / \
   0   0

每个 1 相加,节点的父节点(每个递归级别的调用函数)相加 1 + left_size + right_size 并返回该结果。因此,每个阶段返回的值将是:

          4
        /   \
       2     1
      / \   / \
     1   0 0   0
    / \
   0   0

我不确定这是否更清楚,但我希望它确实如此。

【讨论】:

    【解决方案3】:

    假设您致电countNodes(myTree);。假设myTree不为空,countNodes最终会执行count += countNodes(root->left);,其中root就是myTree。它重新进入您的countNodes 函数,整个树都以root->left(即myTree->left)为根。然后逻辑重复自己;如果没有root->left,则该函数返回0。否则,它最终会再次调用count += countNodes(root->left);,但这次root实际上将是myTree->left。这样它就会计数myTree->left->left。后来它对正确的节点做同样的事情。

    【讨论】:

    • 但是根据我的理解,当 root == NULL 时,代码最终会达到条件,根据你的例子,它会发生 myTree->left->left 然后它会返回 0 所以当它会有机会返回计数吗?
    • 如果我的中序值为 12345 那么 root == 4 所以它会向左移动,即 root->left == 2 ,它不为空,所以它将进入 root->left-> left ,w 等于 1 之后没有叶子所以函数 countnodes(root->left->left->left) 将变为 null 并返回 0?
    • @samprat 了解递归的重要一点是,有多个countNodes 的副本同时执行,每个副本都有自己的count 值。当root == NULL时,它会返回0给调用函数。除非您在树的顶部,否则这将是另一帧 countNodes 为 root 表示的(子)树的父节点执行,其中 0 将添加到 count 的运行值中框架。当该帧返回时,返回的值将被添加到另一个count,对应于从根向上两级的节点等。
    【解决方案4】:

    这就是递归算法的美妙之处。该函数是在当前节点及其子节点上定义的。只要对左右孩子的递归调用是正确的,您只需说服自己当前调用是正确的。完全相同的推理也适用于孩子和他们的孩子,等等……一切都会奏效。

    【讨论】:

      【解决方案5】:

      它将从 root->left->(subnode->left) 等开始,直到该分支返回 0,例如如果它不是实际节点(树中的叶子);

      然后最深的节点将检查 root->right 并重复相同的过程。尝试用一棵小树来形象化:

      因此在这种情况下,您的函数将执行 A->D->B,然后正确的节点将全部返回 0,您将从 C 节点获得最后一个 +1。

      【讨论】:

        【解决方案6】:

        您编写的算法实现是详尽无遗的。它访问整棵树。

        如果树为空,则计数为零。 如果不是,我们得到左边的节点,我们称它为 L,我们将计数加 1。

        既然证明了树的子树本身就是一棵树,我们再次对以 L 为根的树执行相同的算法。

        我们现在对以根右节点为根的树进行此操作。

        现在...这确实有效。

        树的子树是一棵树,也适用于空节点或单个节点。 你应该看看树的定义。

        您可以使用数学归纳法来证明它,并根据归纳推理来表述您的问题。 递归算法通常使用与归纳推理非常相似的结构。

        【讨论】:

          【解决方案7】:

          递归函数的诀窍在于有一个基本情况和一个归纳步骤,就像mathematical induction

          基本情况是您的递归算法如何知道停止。在这种情况下,它是if (root == NULL)——这个节点不代表一棵树。此行在二叉树中的每个节点上执行,即使它当时调用每个节点root。树的所有节点都为 false,但是当您开始在 叶节点 的子节点上调用递归例程时——它们都是 NULL——那么它将返回 0作为节点数。

          归纳步骤是您的递归算法如何通过将未解决的问题转换为(一个或多个)已解决的问题,从一个已解决的状态移动到下一个未解决的状态。您的算法需要计算树中的节点数;当前节点需要1,然后您有两个 更简单的问题——左侧树中的节点数和右侧树中的节点数。获取这两个,将它们加在一起,为当前节点添加 1,并将其作为 this 树中的节点数返回。

          这个概念确实是many algorithms in computer science 的基础,所以在你完全理解之前非常值得研究它。另见quicksortFibonocci sequence

          【讨论】:

            【解决方案8】:

            认为程序首先进入最深的分支。然后它向后返回计数值给它的前一个成员。

                A
               / \
              B   C
             / \   \
            D   E   F
            

            所以程序首先运行直到

            count += countNodes(root->left);
            

            它暂停了到目前为止所做的事情(显然没有什么特别的)并进入 B。同样的情况发生在那里,它进入了 D。在 D 它做同样的事情。但是这里我们有一个特殊情况。程序在行的开头看到

            if ( root == NULL )
            

            D 的不存在的左孩子确实是NULL。因此,您返回 0。 然后我们回到上次暂停的地方,继续这样。上次我们在 B 所以我们继续过线

            count += countNodes(root->left);
            

            然后运行下一行

            count += countNodes(root->right);
            

            这种情况一直持续到您回到 A。但在 A 点,我们在搜索 A 的左侧离开后再次暂停。因此我们继续向右离开。一旦我们完成了整个分支,我们就会回到 A。

            此时我们没有任何未完成的事情(暂停),所以我们只返回我们在整个过程中收集的计数。

            【讨论】:

              【解决方案9】:

              每个子树都有自己的根,就像实际的树有根一样。计数与每个子树的计数相同。因此,您只需继续前进,直到到达叶节点并停止本地递归,在前进时返回并累加节点。

              【讨论】:

                【解决方案10】:

                绘制整棵树,然后为所有叶子节点分配1(叶子节点在N级)。之后,您应该能够通过求和来计算更高级别(N-1)上每个节点生成的节点数:如果节点有两个孩子,则为 1 + 1,如果节点只有一个孩子,则为 1。因此,对于第 N-1 层的每个节点,分配值 1 + sum(left,right)。在此之后,您应该能够计算整个树的节点数。您发布的递归,就是这样做的。

                【讨论】:

                  【解决方案11】:

                  http://www.jargon.net/jargonfile/r/recursion.html

                  更新: 关键是数据结构和程序都是递归的。

                  • 对于数据结构,这意味着:子树也是树。
                  • 代码的意思是:计算树数 := 计算子树数(并加一)

                  【讨论】:

                    猜你喜欢
                    • 2012-10-29
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2021-05-04
                    • 2019-09-09
                    • 2016-07-24
                    • 1970-01-01
                    • 2015-07-25
                    相关资源
                    最近更新 更多