问题描述
我试图了解以下代码的工作方式。该代码将按应有的方式执行,但我不理解其中的一部分。
这是一种按顺序遍历二叉搜索树的方法:
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.right
是None
,因此代码中的下一个if
语句将被跳过(参见[1]
)。这是代码在结束之前最后一次再次调用自身(据我所知?)。
如果跳过了该if
语句-上一次调用io
函数-递归如何继续?
此外,从输出的下一行可以看到(请参见[2]
),它继续显示node=4
,node.left=3
,node.right=6
,即{{ 1}}的父级,并且早已传递给函数(请参见node=3
)。
那么[3]
函数又如何被调用?为什么io
是输入?
解决方法
此代码是编写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,),Node(17,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",None)
)
)
for node,depth in tree.inorder():
print(" " * (depth * 2) + str(node.data))
这给出了树的侧视图:
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",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,9,17]
postorder [3,17,9]
现在我们可以将上面的输出与您的代码连接起来,并消除一些混乱。
首先,让我们翻译输出标签以匹配上面显示的标签:
-
restart
与b4 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
令人困惑-它是有序打印,但仅适用于具有正确子节点的节点。我不确定这会增加什么价值,所以我会忽略它。
请注意,您没有exiting
(弹出调用堆栈帧/后置订单)打印语句。
在这种情况下,让我们回答您的问题:
这是代码在结束之前最后一次再次调用自身(据我所知?)。
是的,该节点即将退出。需要明确的是, function 之所以调用自身,是因为它是递归的,但是树中每个节点只有一个调用。
如果跳过了该
if
语句-上一次调用io
函数-递归如何继续?
弹出调用堆栈,并继续执行,直到上级中断。这不是最后一次调用io
的原因,因为父母可以(及其父母或父母的孩子)可以(并且确实)产生其他呼叫。
那么
io
函数又如何被调用?为什么node=4
是输入?
没有再次调用-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
已经遍历了所有的左后代(只有一个),然后在继续其右子之前访问了它自己的值。
尝试在上面的代码中使用不同的树结构,并将打印的调试/教学遍历与您的理解进行比较。