如何在 BinarySearchTree 中使用 yield



我正在遵循《数据结构和算法》一书中的BinarySearchTree代码。 您想阅读此链接中的完整代码吗?

而且我不清楚这种方法是如何工作的

def __iter__(self):
if self.left != None:
for elem in self.left:
yield elem

yield self.val

if self.right != None:
for elem in self.right:
yield elem
  1. elem变量是Node类的实例还是浮点数(来自输入)?在调试中两者兼而有之,我想这个值因为行产量而改变,但我不明白。

  2. yield
  3. elemyield self.val有什么区别?在这种情况下有多少个生成器对象?

  4. 另外,您想分享一些调试生成器函数的经验吗?调试时我对产量感到困惑。

1.elem是一个Node实例。从 for 循环中,我们知道elem总是self.leftself.right.您可以在示例用法中看到,浮点值与tree.insert(float(x))一起插入到二叉树中,BinarySearchTree.insert()方法最终调用BinarySearchTree.Node(val)在这种情况下valfloat(x)。因此,self.leftself.right始终是Node实例。

  1. 正如评论中提到的,不要只谈论代码,elem是一个浮动。我以前没有看到这个,因为我认为迭代self.left会产生一个Node元素的列表。但是,这是不正确的。实际上,在这种情况下,通过调用self.left.__iter__()来迭代self.left工作。我将这个__iter__()函数分解为 3 种情况,几乎就像递归函数一样。(它实际上不是递归的,因为它调用Node类的不同实例的__iter__()方法,但它的行为是相似的。

    • 首先,节点没有leftright子节点。这很简单:迭代器只会产生self.val,这是一个浮点数。
    • 其次,节点有left子节点。在这种情况下,for 循环将以几乎递归的方式向下遍历所有剩余的子节点,直到到达没有left子节点。然后我们回到第一种情况。
    • 第三,节点有right子节点。在这种情况下,在返回自己的节点self.val后,迭代器将继续到第一个right节点,并重复。
  2. 只有一个生成器,即Node.__iter__(),因为生成器是函数。它使用多个yield语句根据情况返回不同的值。yield elemyield self.val如果当前节点具有左分支或右分支或当前节点的值,则只返回Node

  3. 我没有特别调试yield语句的具体技巧。通常,我在构建代码时使用IPython进行交互式工作,并使用其内置的%debug魔术运算符。您可能还会发现橡皮鸭调试很有用。

使用 IPython,您可以在单元格中运行以下命令以交互方式进行调试。

In [37]: %%debug
...: for x in tree.root:
...:     print(x)
...:
NOTE: Enter 'c' at the ipdb>  prompt to continue execution.

然后,可以在调试器提示符下使用s命令,ipdb>单步执行代码,跳转到函数调用。

ipdb> s
--Call--
> <ipython-input-1-c4e297595467>(30)__iter__()
28         # of the nodes of the tree yielding all the values. In this way, we get
29         # the values in ascending order.
---> 30         def __iter__(self):
31             if self.left != None:
32                 for elem in self.left:

调试时,可以通过在表达式前面加上感叹号!来计算表达式。

ipdb> !self
BinarySearchTree.Node(5.5,BinarySearchTree.Node(4.4,BinarySearchTree.Node(3.3,BinarySearchTree.Node(2.2,BinarySearchTree
.Node(1.1,None,None),None),None),None),None)

首先,您共享的代码中存在缩进问题:yield self.val不应该在if块中:

def __iter__(self):
if self.left != None:
for elem in self.left:
yield elem

yield self.val  #  Unconditional. This is also the base case

if self.right != None:
for elem in self.right:
yield elem

要理解此代码,首先开始想象一个只有一个节点的树。让我们暂时忽略BinarySearchTree类,并说我们可以直接访问Node类。我们可以创建一个节点,然后迭代它:

node = Node(1)
for value in node:
print(value)

此循环将调用__iter__方法,在这种情况下,该方法不会执行任何if块,因为它没有子块,并且只执行yield self.val。这就是value在上面的循环中将获得的值,并被打印出来。

现在用另外 2 个节点扩展这个小练习:

node = Node(1, 
Node(0),
Node(2)
)
for value in node:
print(value)

在这里,我们创建了这棵树,node指的是它的根

1   <-- node
/ 
0   2

for..in循环现在调用__iter__时,它将首先进入第一个if块,在那里我们得到一种递归形式。有了for语句,我们再次执行__iter__,但这次是在node的左子节点,即值为 0 的节点。但这是我们已经知道的情况:这个节点没有子节点,我们从上面的第一个例子中知道,这会导致一次迭代,其中循环变量将是该节点的值,即 0,并且生成该值。这意味着主程序获得value等于 0 的迭代,该迭代被打印出来。

所以elem是数字。最好将其称为valueval以消除任何混乱。

在那之后if块执行,我们就可以yield self.val了。self在这里node,所以我们得到1。这意味着主程序可以执行第二次迭代,这次value等于 1。

最后执行第二个if块,现在node的右子块是递归__iter__调用的主题。这与左孩子的原理相同。这将产生值 2,主程序打印值 2。

我们可以再次用更多的节点扩展树,但原理是相同的:通过递归调用__iter__,产生树的所有值。

yield from

有一种语法可以简化代码,并且在与None进行比较时,使用is运算符也是更常见的做法:

def __iter__(self):
if self.left is not None:
yield from self.left

yield self.val

if self.right is not None:
yield from self.right

这会导致相同的行为。yield from将生成来自可迭代对象的所有值。由于节点实例具有__iter__方法,因此可以迭代,因此可以按预期工作。

最新更新