python - 了解 BST 遍历的打印输出

标签 python recursion binary-search-tree tree-traversal

我正在尝试了解以下代码的工作原理。代码按应有的方式执行,但我不理解其中的部分内容。

这是一种中序遍历二叉搜索树的方法:

def traverse(self):
    def io(node):
        print("restart") #added to code see whats happening
        if node is not None: print("b4 app", node.data) #added to code see whats happening
        if node.left: io(node.left)
        result.append(node.data)
        if node is not None: print("aft app",node.data,node.right is None) #added to code see whats happening
        if node.right: #[1] skipped bc node.right is None
            print("inside right") #added to code see whats happening
            io(node.right)
    if self.root is None:
        return None
    else:
        result=[]
        io(self.root)
        return result

这是二叉搜索树的 Node 类的结构:

class Node:
    def __init__(self, data, left=None, right=None):
        self.data = data
        self.left=left
        self.right=right

这是遍历 BST 的输出:

restart
b4 app 9
restart
b4 app 4 #[3]
restart
b4 app 3 
aft app 3 True # <--- I thought it would end here? [0]
aft app 4 False #[2]
inside right
restart
b4 app 6
aft app 6 True
aft app 9 False
inside right
restart
b4 app 17
aft app 17 True
[3, 4, 6, 9, 17] #<-- ordered list of nodes (this output is correct)

它正在遍历的 BST:

"""
         9
        / \
       4   17
      / \
     3   6
"""

函数到达我指向的点后(参见[0]),node.rightNone,因此代码中的下一个 if 语句被跳过(参见 [1])。这是代码在结束前最后一次再次调用自身(据我所知?)。

如果跳过此 if 语句——上次调用 io 函数——递归如何继续?

此外,从输出的下一行可以看出(参见 [2]),它继续 node=4node.left =3node.right=6,这是node=3的父级,早早传入了函数(参见[3] )。

那么io函数又是如何被调用的,为什么输入是node=4

最佳答案

这段代码是一种非常困惑的写法 tree traversal但它看起来基本上是正确的。此外,输出打印在不寻常的位置并带有误导性标签,因此让我们在继续您的问题之前重写它。

这是编写中序二叉树遍历的直接方法:

from collections import namedtuple

class Tree:
    def __init__(self, root):
        self.root = root

    def inorder(self):
        def traverse(node):
            if node:
                yield from traverse(node.left)
                yield node
                yield from traverse(node.right)

        return traverse(self.root)

if __name__ == "__main__":
    Node = namedtuple("Node", "data left right")
    """
        9
       / \
      4   17
     / \
    3   6
    """
    tree = Tree(
        Node(
            9,
            Node(
                4,
                Node(3, None, None),  
                Node(6, None, None), 
            ),
            Node(17, None, None)
        )
    )

    for node in tree.inorder():
        print(node.data, end=" ") # => 3 4 6 9 17

我们唯一需要的分支是检查根是否为 None——最好避免担心子递归调用。如果它们是 None,这个单一分支将在子节点中处理该条件 stack frame .

上面的代码返回一个 generator这比创建列表更便于内存,而且语法更简单。

我还会在函数之外继续打印。传递回调是避免产生 side effect 的常用方法。 ,但使用上面的生成器方法,相同的结果是通过循环完成的,让我们将打印保留在调用者中。

如果您出于调试目的确实需要打印,我建议使用空格缩进,这样可以将输出变成树状并且更容易理解:

from collections import namedtuple

class Tree:
    def __init__(self, root):
        self.root = root

    def inorder(self):
        def traverse(node, depth=0):
            if node:
                yield from traverse(node.left, depth + 1)
                yield node, depth
                yield from traverse(node.right, depth + 1)

        return traverse(self.root)

if __name__ == "__main__":
    Node = namedtuple("Node", "data left right")
    """
        9
       / \
      4   17
     / \
    3   6
    """
    tree = Tree(
        Node(
            9,
            Node(
                4,
                Node(3, None, None),  
                Node(6, None, None), 
            ),
            Node(17, None, None)
        )
    )

    for node, depth in tree.inorder():
        print(" " * (depth * 2) + str(node.data))

这给出了树的侧 View :

    3
  4
    6
9
  17

通过这种缩进技巧可以更轻松地可视化树,这是您的前/中/后订单打印的清理版本,它应该清楚地显示遍历:

from collections import namedtuple

class Tree:
    def __init__(self, root):
        self.root = root

    def print_traversals_pedagogical(self):
        preorder = []
        inorder = []
        postorder = []

        def traverse(node, depth=0):
            if node:
                preorder.append(node.data)
                print("    " * depth + f"entering {node.data}")
                traverse(node.left, depth + 1)
                inorder.append(node.data)
                print("    " * depth + f"visiting {node.data}")
                traverse(node.right, depth + 1)
                postorder.append(node.data)
                print("    " * depth + f"exiting {node.data}")

        traverse(self.root)
        print("\npreorder ", preorder)
        print("inorder  ", inorder)
        print("postorder", postorder)

if __name__ == "__main__":
    Node = namedtuple("Node", "data left right")
    """
        9
       / \
      4   17
     / \
    3   6
    """
    tree = Tree(
        Node(
            9,
            Node(
                4,
                Node(3, None, None),  
                Node(6, None, None), 
            ),
            Node(17, None, None)
        )
    )
    tree.print_traversals_pedagogical()

输出:

entering 9
    entering 4
        entering 3
        visiting 3
        exiting 3
    visiting 4
        entering 6
        visiting 6
        exiting 6
    exiting 4
visiting 9
    entering 17
    visiting 17
    exiting 17
exiting 9

preorder  [9, 4, 3, 6, 17]
inorder   [3, 4, 6, 9, 17]
postorder [3, 6, 4, 17, 9]

现在我们可以将上面的输出与您的代码联系起来并消除一些混淆。

首先,让我们翻译您的输出标签以匹配上面显示的标签:

  • restartb4 app 做同样的事情所以你应该忽略它以避免混淆。 if node is not None: print("b4 app", node.data) 始终为真——我们在调用方中保证 node 不会为 None。
  • b4 app -> entering(或将新调用插入堆栈)。
  • aft app -> visiting(顺序)。再一次,if node is not None: 保证为真,应该被删除。父调用会检查这一点,即使他们没有检查,程序也会在上面使用 node.data 的行上崩溃。
  • inside right 令人困惑——它是一个中序打印,但仅适用于具有右子节点的节点。我不确定这会增加什么值(value),所以我会忽略它。

请注意,您没有exiting(弹出调用堆栈帧/postorder)打印语句。

在这种情况下,让我们回答您的问题:

This is the last time where the code calls itself again before it ends (as far as I can see?).

是的,这个节点即将退出。需要明确的是,函数 调用自身是因为它是递归的,但树中的每个节点只有一次调用。

If this if statement is skipped -- last time the io function is called -- how does the recursion continue?

调用堆栈弹出,执行从它在父级中停止的地方继续。这不是最后一次调用 io,因为父级(及其父级或其父级的子级)可以(并且确实)产生其他调用。

So how was the io function called again and why is node=4 the input?

它没有被再次调用——node=4 的调用框架已暂停,等待其子级解决,当控制返回给它时,它会从中断处继续。

让我们将我的输出与您的输出联系起来:

    visiting 3  # this is your `aft app 3 True [0]`
    exiting 3   # you don't have this print for leaving the node
visiting 4      # this is your `aft app 4 False #[2]`

您可以看到我们退出了 node=3 的调用帧。此时,node=4 已完成遍历其所有左后代(只有一个),然后在继续其右子节点之前按顺序访问其自身的值。

尝试在上面的代码中使用不同的树结构,并将打印的调试/教学遍历与您的理解进行比较。

关于python - 了解 BST 遍历的打印输出,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63962871/

相关文章:

python - unicodedata.unidata_version 打印错误的 unicode 版本?

python - 在Python中计算稀疏张量的余弦相似度的有效方法?

python - 如何列出模块所依赖的用户创建的 python 文件?

c - C中如何知道avl节点状态?

c - 此函数如何计算二叉树中的节点数

python - python ebay sdk中如何添加多张图片

python - 使用列表位置中包含的项目进行计算(遗传算法适合度)

python - 倒计时然后向上

java - 将递归方法(递归在循环内完成)转换为迭代方法

Java BST 最有效地搜索最大值