yielding data members in binary search tree - python

I am trying to implement an iterator to my binary search tree. To achieve this, I am attempting to do an in-order traversal through the tree and yield each individual data member. This will allow me to iterate through each item of the tree.
My function:
def __iter__(self):
"""
in-order traversal of a binary search tree
"""
if self.root is not None:
self.check(self.root)
def check(self, cur_node):
if cur_node is not None:
self.check(cur_node.left)
yield cur_node.data #if I were to print this data member, it would be fine
self.check(cur_node.right)
When testing this function with an iteration such as
for i in tree:
I am receiving this error:
TypeError: iter() returned non-iterator of type 'NoneType'

To implement a recursive generator you cannot just "call" yourself, you need to extract elements and yield them.
Python has a special syntax for this:
yield from expr
where expr is iterable, and it can be seen as a shorthand for
for x in expr:
yield x
Using this you can implement in-order traversal of a tree with something like:
class Node:
def __init__(self, data, left, right):
self.data = data
self.left = left
self.right = right
def __iter__(self):
if self.left:
yield from self.left
yield self.data
if self.right:
yield from self.right

The clue is
iter() returned ....
So you need to return an iterator. Your class is an iterator, so return self
def __iter__(self):
"""
in-order traversal of a binary search tree
"""
if self.root is not None:
self.check(self.root)
return self
You should probably implement __next__ as well to actually yield the value.
So the solution might look like
class Tree:
def __init__(...): ...
def __iter__(self):
return self
def __next__(self):
if self.left is not None:
yield from self.left
yield self.data
if self.right is not None:
yield from self.right
You use yield from here to delegate to the child nodes. See https://docs.python.org/3/whatsnew/3.3.html#pep-380-syntax-for-delegating-to-a-subgenerator
You do in fact need three yield statements, because you need to traverse both the left and right children, as well as producing the value of the current node.

You generally want your iterator as a separate entity from your data structure, so you can have multiple iterators over your data, and so you can iterate over your data multiple times. Below, I show how you can implement a simple DFS algorithm for a basic BST class.
class Node:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right
def __iter__(self):
return BSTIterator(self)
class BSTIterator:
def __init__(self, root):
self.stack = []
curr = root
while curr:
self.stack.append(curr)
curr = curr.left
def __next__(self):
if not self.stack:
raise StopIteration()
node = self.stack.pop()
val = node.val
node = node.right
while node:
self.stack.append(node)
node = node.left
return val
def __iter__(self):
return self
root = Node(5, Node(3, Node(1), Node(4)), Node(10, (Node(6, None, Node(7)))))
list(root)
# [1, 3, 4, 5, 6, 7, 10]

Related

is it optimal to create class instance of iterable using __iter__ and __next__ compared using a list instance attribute?

Intuitively, using __iter__ and __next__ seems likely to be significantly for efficient, but I curious of the technical specifics of why, and how one maybe go about benchmarking to measure and compare the space and runtime of these?
A common case I can think of is a Linked List,
The following is an implementation illustration the use of __iter__ and __next__:
I have tried various benchmarking tools without much success being able to generate data to more closely analyze they're internals; specifically, how exactly the magic methods function, and if they're truly as significantly more efficient as I surmise they are.
class Node(object):
def __init__(self, value: int, next_node):
self.value = value
self.next = next_node
def __repr__(self):
return str(self.value)
class LinkedList(object):
def __init__(self, head=None):
self.head = None
self._current = None
def __repr__(self):
return '<{} {}>'.format(type(self).__name__, self.head)
def __iter__(self):
self._current = self.head
return self
def __next__(self):
if self._current is None:
raise StopIteration
current, self._current = self._current, self._current.next
return current
#property
def is_empty(self):
return self.head is None
def prepend_node(self, valuetoadd: int):
self.head = IntNode(valuetoadd, self.head)
Compared to the following:
class ListNode:
"""
A node in a singly-linked list.
"""
def __init__(self, data=None, next=None):
self.data = data
self.next = next
​
def __repr__(self):
return repr(self.data)
​
​
class SinglyLinkedList:
def __init__(self):
"""
Create a new singly-linked list.
Takes O(1) time.
"""
self.head = None
​
​
def prepend(self, data):
"""
Insert a new element at the beginning of the list.
Takes O(1) time.
"""
self.head = ListNode(data=data, next=self.head)
def find(self, key):
"""
Search for the first element with `data` matching
`key`. Return the element or `None` if not found.
Takes O(n) time.
"""
curr = self.head
while curr and curr.data != key:
curr = curr.next
return curr # Will be None if not found
def remove(self, key):
"""
Remove the first occurrence of `key` in the list.
Takes O(n) time.
"""
# Find the element and keep a
# reference to the element preceding it
curr = self.head
prev = None
while curr and curr.data != key:
prev = curr
curr = curr.next
# Unlink it from the list
if prev is None:
self.head = curr.next
elif curr:
prev.next = curr.next
curr.next = None

Using comparison operator on custom objects

I have the following code:
from typing import Optional
class Tree:
class Tree:
def __init__(self):
self.root: Optional[Node] = None
def __str__(self) -> str:
return f"{self.root.data}"
class Node:
def __init__(self, data):
self.data = data
self.left = None
self.right = None
I have this class. I am trying to find maximum data element in my tree. The algorithm I created keeps a temporary element and updates the temporary element if it finds a value greater than this element. At first, I make my temporary element value 0 and then do the following:
if tree.data > temp:
temp = tree.data
but I get the following error:
TypeError: '>' not supported between instances of 'Node' and 'Node'
How can I use the ">" operator in this situation?
Give an implementation of the __gt__ dunder method for the Node object. This tells Python what basis to compare Node objects on:
class Node:
def __init__(self, data):
self.data = data
self.left = None
self.right = None
def __gt__(self, other):
return self.data > other.data

How to search in a tree?

I have this follow tree implementation:
class Node:
def __init__(self, *, val):
self.val = val
def __repr__(self):
return "{}(val={})".format(type(self).__name__, self.val)
class Tree:
def __init__(self, *, val, left=None, right=None):
self.val = val
self.left = left
self.right = right
def __repr__(self):
return "{}(val={}, left={}, right={})".format(type(self).__name__, self.val, self.left, self.right)
Now I try to add a method that checks if a element exists:
def exists(self, val):
if self.val == val:
return True
return self.left.exists(val) or self.right.exists(val)
Then if I try to search 1, it's fine. But with 2, It errors out.
Traceback (most recent call last):
File "...", line 30, in <module>
print(tree.exists(2))
File "...", line 20, in exists
return self.left.exists(val) or self.right.exists(val)
File "...", line 20, in exists
return self.left.exists(val) or self.right.exists(val)
AttributeError: 'Node' object has no attribute 'exists'
For reference, here's the tree I made:
tree = Tree(val=1,
left=Tree(val=Node(val=2), left=Node(val=3), right=Node(val=4)),
right=Tree(val=Node(val=2), left=Node(val=3), right=Node(val=4)))
How can I recursively search in a tree?
Just jettison the Node class which serves no purpose whatsoever:
class Tree:
def __init__(self, *, val, left=None, right=None):
self.val = val
self.left = left
self.right = right
def exists(self, val):
if self.val == val:
return True
if self.left and self.left.exists(val):
return True
if self.right and self.right.exists(val):
return True
return False
tree = Tree(val=1,
left=Tree(val=2, left=Tree(val=3), right=Tree(val=4)),
right=Tree(val=2, left=Tree(val=3), right=Tree(val=4)))
The bottom of you recursion is not only if you found the value (if self.val == val) but also if you reach a leaf of your tree (if self.isLeaf return false)
The error says "Node" doesn't have a method "exists" which is correct. You didn't write Node's version of "exists." You need to work out what data and method belong to each node, and what data and methods should be on the tree as a whole.
But additionally, you can't look at left or right without checking to see if they are None.
self.left.exists()
should be replaced with
self.left and self.left.exists()
And the same for right

Recursion within a method

In my code I am counting the number of nodes in a complete binary tree and I decided to use a recursive approach. However, when I call my method within itself, I get NameError: name 'countNodes' is not defined.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def countNodes(self, root: TreeNode, count = 0) -> int:
if not root.left and not root.right:
return 1
else:
count += countNodes(self, root.left, count) + countNodes(self, root.right, count)
To answer this question.
When accessing a method of the class instance inside the class you have to use self.method. So in your case you have to use self.countNodes instead of countNodes.

Removing a node from a linked list with "del" doesn't work

The problematic part is in the function remove. Even after del is called, the relevant node is not removed from the Linked List. Did I misunderstand something about del?
class Node:
def __init__(self, val):
self.val = val
self.next = None
def add(self, val):
if not (self.next):
self.next = Node(val)
else:
self.next.add(val)
def remove(self, val):
if self.val == val:
if self.next:
self.val = self.next.val
self.next = self.next.next
else:
del self # this doesn't remove the node from linked list
else:
if self.next:
self.next.remove(val)
else:
print "no such val found %d" % val
def __str__(self):
output = []
while self is not None:
output.append(str(self.val))
self = self.next
return " -> ".join(output)
head = Node(1)
head.add(2)
print head
head.remove(3)
head.add(3)
head.add(4)
print head
head.remove(3)
head.remove(4)
print head
The statement del self only removes the name self from the local scope (and decrements the reference count). It has no effect on the other references to it nor to those objects.
To remove the node from the linked list, you must update the node(s) that refer to it. Since you have a singly linked list, you must traverse the list from the beginning to find the node whose node.next == self, then change it to node.next = self.next to remove self from the sequence of links.

Categories

Resources