Here are some thoughts of mine regarding the binary tree implemenation, some ways to handle your questions and my actual implementation with some more comments.
My thoughts on binary tree implementation
Intrigued by your multiple posts regarding binary trees in different variations I've spent the day implementing my own version of a BinaryTree
. The main issues I've seen overall in your code, which was the focus for my reimplementation are the following:
Simplify the build of the tree – Your code slices and dices the double list of a preorder and inorder list of values. This seems like a vaste of space and time, so I aimed for a version doing a single pass on the list, whilst maintaining the possibility to create an arbitrary tree (i.e. out of order if so wanted :-) ). This lead to the need for a better serialization of the binary tree, where "https://stackoverflow.com/questions/2675756/efficient-array-storage-for-binary-tree/" sums up some of the logic I'm using
Added: When reviewing both yours, Barrys and my own solution I came to wonder why do you have node
as a parameter at all in your recurse()
code? It is not used as a parameter, but is always set to a new value and could be a local variable. This would simplify your calling structure, and if you then allow for empty trees to enter you'll see that your code is quite similar to that which Barry has made (although he has skipped the intermediate storage of the sliced lists)
- Simplify order traversals using yields – Not exactly in this code, but some of the other questions you have made some rather complicated order traversals, which can be much neater given the nature of binary trees and the use of the yield operator. So I did some of those...
- Introduce an equality operator for the binary trees – When testing it is nice to compare the trees, and there exists at least three ways to do this. First option that the trees are identical with same address references, which I find not so useful. Secondly, to compare that the binary trees have the same structure and the same values across the trees. Thirdly, that the binary trees holds the same values in the same order, but not that the trees them selves are identical. I implemented the latter two in the code below.
Your questions
Structuring of code is, and will always be a bit of a challenge, but finding good, simple and nice solution are a part of it. That is, do research to find algorithms which matches what you want to accomplish, and do not be afraid of tossing away earlier attempts at solving the problem at hand.
If your code gets to complicated, or has to many variables (not neatly connected), then you most likely need to rethink your solution or modularisation. A function should have one concern or responsibility, and that task it should do very well.
In other words, one way to handle structure of code is to break it down into small pieces, and then reassemble these pieces into something which works. And if you find yourself doing copy-and-paste of code, seriously think about using functions or making a common function.
Regarding how to debug recursive functions, I tend to fall back to loads of debug output. In this particular case I did some changes to the binary structure to help me identify where I was. In addition to storing key
, left
and right
, I also incremented a class counter to act as a unique identifier for each and every node I created. This in addition with a simple print(self)
sprinkled around, made it a lot easier to debug.
My __str__
therefore looks like this:
def __str__(self):
return "BinaryTree - node id: {}, value: {}, left: {}, right: {}".format(
self._node_number,
self.value,
self.left._node_number if self.left else BinaryTree.nil_marker,
self.right._node_number if self.right else BinaryTree.nil_marker,
)
I'll come back to the nil_marker
, which currently defaults to .
as most of the testing was done with text strings as in your examples. However when I tested my class using integers, I change it to None
.
This implementation of __str__
gives me a full output of relevant information regarding a given node on one line, and is good help to look into the logic of the method I'm debugging.
My implementation
The code with tests stands currently at over 300 lines, so I'll just post parts of it (and will most likely use some other parts in other responses in due time). I've focused on the initialization, in order and pre order traversal, and finally on the building of the tree with some testing in a separate code segment below.
from collections import deque
from itertools import izip_longest
class BinaryTree(object):
"""Structure to hold a binary tree, for starters based on letter nodes."""
# Next two variables are used as default for the traversal
# methods to indicate if left or right equal to None should
# yield the nil_marker value. This is needed in order to
# produce exact representation when doing pre- or postorder traversal
yield_none_values = False
nil_marker = '.'
# When yielding none values, this is used as yield value within
# the level order traversal
level_shift_marker = ':'
# _node_count is an internal counter and identificator for how many
# BinaryTree nodes has been created. Used when outputting the nodes
# in the __str__ method to connect nodes
_node_count = 0
def __init__(self, value, left = None, right = None):
self.value = value
self.left = left
self.right = right
# Assign identifier to the node, and a loose count of nodes
BinaryTree._node_count += 1
self._node_number = BinaryTree._node_count
# def __str__: as above
def __eq__(self, other):
"""Check if both trees exists, and have same value and subtrees."""
# Return early if only one of the tree exists
# or if only one of them has a right tree
# or if only one of them has a left tree
if (bool(self) ^ bool(other)
or (bool(self.right) ^ bool(other.right))
or (bool(self.left) ^ bool(other.left))):
return False
# Otherwise compare the values, and that sub trees are equal
return (self.value == other.value
and self.right == other.right
and self.left == other.left)
def __ne__(self, other):
"""Negated version of BinaryTree.__eq__"""
return not self.__eq__(other)
def values_in_order(self):
"""Yield the inorder traversal of the binary tree."""
if self.left:
for value in self.left.values_in_order():
yield value
yield self.value
if self.right:
for node in self.right.values_in_order():
yield node
def values_pre_order(self,
yield_none_values = yield_none_values,
nil_marker=nil_marker):
"""Yield the pre order traversal of the binary tree."""
yield self.value
if self.left:
for value in self.left.values_pre_order(yield_none_values, nil_marker):
yield value
# used to get a marker when self.left is None
elif yield_none_values:
yield nil_marker
if self.right:
for node in self.right.values_pre_order(yield_none_values, nil_marker):
yield node
# used to get a marker when self.right is None
elif yield_none_values:
yield nil_marker
@classmethod
def from_preordered_list(cls, preordered_list, nil_marker=nil_marker):
"""Recreate binary tree from preordered list with nil markers.
Builds a complete binary tree based on a preordered traversal text
where missing left or right subtrees are marked with the nil marker.
"""
def decode():
"""Decodes the list into a binary tree.
For each call it decodes at most three element, one for the node
value, and two more for the left and right subtree.
"""
if node_list:
node_val = node_list.popleft()
else:
return None
if node_val != nil_marker:
node_left = decode()
node_right = decode()
node = BinaryTree(node_val, node_left, node_right)
return node
else:
return None
node_list = deque(preordered_list)
return decode()
Regarding traversal methods
Notice how neat the values_in_order()
and values_pre_order()
gets when you can rely on the yield operator to return at the proper timing. The values_post_order()
is equally simply, with just a move of the yield self.value
to the end of the method. Usually, you couls also remove the elif
in the values_pre_order()
method, but I left it in so you can see how I got to generate the preorder text strings used in the test code below.
Regarding __eq__
In this definition of the binary tree to be equal I require that the structure of the binary tree is equal as well as the value of the nodes. In order to check for equality without too much hassle of None
pointers I use a trick to simplify the if (self and not other) or (not self and other)
test into if bool(self) ^ bool(other)
. The latter uses the exclusive or operator to verify that either are both positive or both are negative. Also see "https://stackoverflow.com/questions/432842/how-do-you-get-the-logical-xor-of-two-variables-in-python"
Later on I test self.right == other.right
, which triggers a recursion of the equality operator down the trees, if they are not both None
already where it doesn't have to recurse to find the equality.
Another version of equality can be achieved using the in order traversal, and comparing value for value whilst tagging along using itertools.izip_longest
to join the iterations of both the trees to be compared.
Some binary trees and preorder text version of those
I'm not sure if the following block will help or confuse, but here are some examples of trees I've used when testing presented in beatiful ascii art, and followed by the preorder traversal string later used for composition of the tree and another notation (level no: value, left tree, right tree)
is used recursively. Note how the nil
corresponds to the .
marker in, and that it goes depth first to traverse and a find a full tree.
Some binary trees:
D D D G
/ \ / \ / \
B F B . . F C H
/ \ / / \ / \ \
A C E A . .. .. . G B F . I
complete left right / /
A . D . ..
\
. denotes some of the empty nodes . E
strange
Balanced tree: DBA..C..FE... -> (0: D, (1: B, (3: A, nil, nil), (3: C, nil, nil)), (2: F, (3: E, nil, nil), nil))
Left tree: DBA.... -> (0: D, (1: B, (2: A, nil, nil), nil), nil)
Right tree: D.F.G.. -> (0: D, nil, (1: F, nil, (2: G, nil, nil)))
Strange tree: GCBA...FD.E...H.I.. ->
(0:G,
(1: C,
(2: B,
(3: A, nil, nil),
nil),
(2: F,
(3: D,
nil,
(4: E, nil, nil)),
nil),
(1: H,
nil,
(2: I, nil, nil)
PS! I see now that Barry has added another answer, that your test tree has the preorder text of ABD..E..CF
as you've based your example on the level order sequence, whilst my balanced tree is based upon the in order sequence...
Some test code to finish it off
Here is some of the test code I've used, where I append all the trees I'm making into an array for later printing and comparison. I could definitive make the test cases neater, but it is getting late...
def string_join(join_list, join_text=''):
return join_text.join(str(i) for i in join_list)
def main():
trees = []
trees.append(('balanced',
BinaryTree('D',
BinaryTree('B',
BinaryTree('A'),
BinaryTree('C')),
BinaryTree('F',
BinaryTree('E')))))
trees.append(('balanced_DEC',
BinaryTree.from_preordered_list('DBA..C..FE')))
trees.append(('integer_tree',
BinaryTree.from_preordered_list([4, 2, 1, None, None,
3, None, None, 6, 5], None)))
trees.append(('strange_tree',
BinaryTree('G',
BinaryTree('C',
BinaryTree('B',
BinaryTree('A')),
BinaryTree('F',
BinaryTree('D',
None,
BinaryTree('E')))),
BinaryTree('H',
None,
BinaryTree('I')))))
trees.append(('strange_tree_vDecoded',
BinaryTree.from_preordered_list('GCBA...FD.E...H.I')))
print
previous_tree = None
for tree_name, tree in trees:
print('Tree name: {}'.format(tree_name))
print(' Equal to previous tree: {}'.format(previous_tree == tree))
print(' Pre order: {}'.format(string_join(tree.values_pre_order(yield_none_values = True))))
print(' In order: {}'.format(string_join(tree.values_in_order())))
print
previous_tree = tree
if __name__ == '__main__':
main()
As can be seen in the output below, the manually built and decoded version of the trees are equal, and the implementation can also be used for a binary tree of integers, or whatever list you'll want.
Tree name: balanced
Equal to previous tree: False
Pre order: DBA..C..FE...
In order: ABCDEF
Tree name: balanced_DEC
Equal to previous tree: True
Pre order: DBA..C..FE...
In order: ABCDEF
Tree name: integer_tree
Equal to previous tree: False
Pre order: 421..3..65...
In order: 123456
Tree name: strange_tree
Equal to previous tree: False
Pre order: GCBA...FD.E...H.I..
In order: ABCDEFGHI
Tree name: strange_tree_vDecoded
Equal to previous tree: True
Pre order: GCBA...FD.E...H.I..
In order: ABCDEFGHI
So, to finish off, this is my take on implementing the binary tree in a class, and using a somewhat simpler mechanism to build an arbitrary tree. Hope you get something useful out of it, at least I'm learning a lot doing reviews here on Code Review SE.
Addendum 1: Non-broken __repr__
Caridorc mention in his answer that your implementation of __repr__
doesn't work, well, here is one which does work according to my tests (even though it could possibly handle the None
cases better...) :
def __repr__(self):
"""Return a string which when eval'ed will rebuild tree"""
return '{}({}, {}, {})'.format(
self.__class__.__name__,
repr(self.value),
repr(self.left) if self.left else None,
repr(self.right) if self.right else None) \
.replace(', None, None)', ')') \
.replace(', None)', ')')