2

I am studying how to use Generators in Python in David Beazly's excellent Python Cookbook text. The following code recipe defines Depth First Tree Traversal using generators very elegantly:

# example.py
#
# Example of depth-first search using a generator

class Node:
    def __init__(self, value):
        self._value = value
        self._children = []

    def __repr__(self):
        return 'Node({!r})'.format(self._value)

    def add_child(self, node):
        self._children.append(node)

    def __iter__(self):
        return iter(self._children)

    def depth_first(self):
        yield self
        for c in self:
            yield from c.depth_first()

# Example
if __name__ == '__main__':
    root = Node(0)
    child1 = Node(1)
    child2 = Node(2)
    root.add_child(child1)
    root.add_child(child2)
    child1.add_child(Node(3))
    child1.add_child(Node(4))
    child2.add_child(Node(5))

    for ch in root.depth_first():
        print(ch)
    # Outputs: Node(0), Node(1), Node(3), Node(4), Node(2), Node(5)

I am trying to come up with an equally elegant method

def breadth_first(self):
    pass

I am deliberately not posting the crazy stuff that I have been trying since everything that I have tried requires maintaining 'state' within it. I don't want to use the traditional queue based solutions. The whole point of this academic exercise is to learn how generators behave in depth. Therefore, I want to create a parallel 'breadth_first' method using generators for the tree above.

Any pointers/solutions are welcome.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Bharat
  • 2,409
  • 6
  • 32
  • 57

4 Answers4

7

You can't use recursion (stack) for bfs without some serious hacks, but a queue would work:

def breadth_first(self):
    q = [self]
    while q:
        n = q.pop(0)
        yield n
        for c in n._children:
            q.append(c)
AChampion
  • 29,683
  • 4
  • 59
  • 75
  • I tried your solution. It works fine :). I was hoping that I could do it as elegantly as David's depth_first method, but could not. Oh. well. – Bharat May 12 '18 at 15:04
0

The depth first solution you implement is essentially "iterate then recurse"

def depth_first(self):
    yield self
    for c in self.children:
        yield from c.depth_first():

Inspired by this blog post the activestate post it references, you obtain a breadth first search as "recurse then iterate":

def breadth_first(self):
    yield self
    for c in self.breadth_first():
        if not c.children:
            return  # stop the recursion as soon as we hit a leaf
        yield from c.children

Edit: it turns out that the linked blog says "Termination check is absent", replaced with an activestate link, which I've adapted to use yield from above.

Eric
  • 95,302
  • 53
  • 242
  • 374
  • 1
    How do you avoid infinite recursion with `breadth_first`? Seems like it will just keep yielding `self` over and over before complaining about hitting maximum recursion depth. – Andrew Macheret May 07 '20 at 17:35
  • If `self` is acyclic (it represents a tree structure), BFS will terminate. You cannot traverse ``up'' in a tree. – Jeremy Dec 16 '21 at 17:45
  • It looks like this does indeed recurse forever after all nodes have been reached... – Eric Dec 16 '21 at 21:18
  • I've added the missing termination condition. – Eric Dec 16 '21 at 21:31
0

I find it both useful and elegant generating the whole breadth, one level at a time. Python 3 generator below:

def get_level(t: Node) -> Iterable[List]:
    curr_level = [t] if t else []
    while len(curr_level) > 0:
        yield [node._value for node in curr_level]
        curr_level = [child for parent in curr_level
                            for child in parent._children
                            if child]
kush
  • 51
  • 1
  • 4
0

It would be easy if itertools had:

# zip_chain('ABCD', 'xy') --> A x B y C D

It's almost a itertools.chain(itertools.zip_longest()), but not quite.

Anyway, zip_chain allows:

def bf(self):
    yield self
    yield from zip_chain(*map(Node.bf, self.children))

It doesn't create a whole row at a time either, I think,

Him
  • 5,257
  • 3
  • 26
  • 83