diff --git a/.travis.yml b/.travis.yml index af537e2..4ce66bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,11 @@ install: - pip install pytest - pip install pytest-cov - pip install python-coveralls + - pip install flake8 - python setup.py install script: - - py.test --cov-report= --cov=binarytree tests.py + - python -m flake8 binarytree/__init__.py + - python -m doctest binarytree/__init__.py + - py.test tests.py --cov=binarytree after_success: - coveralls diff --git a/README.rst b/README.rst index 3c725c2..c299638 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -BinaryTree: Python Library for Learning Binary Trees +Binarytree: Python Library for Learning Binary Trees ---------------------------------------------------- .. image:: https://travis-ci.org/joowani/binarytree.svg?branch=master @@ -27,7 +27,7 @@ BinaryTree: Python Library for Learning Binary Trees | -.. image:: https://user-images.githubusercontent.com/2701938/29019910-161f6cda-7b15-11e7-8bfb-49ea0a4179f9.gif +.. image:: https://user-images.githubusercontent.com/2701938/34109703-4a8810aa-e3b9-11e7-8138-68eec47cfddb.gif :alt: Demo GIF Introduction @@ -35,11 +35,27 @@ Introduction Are you studying binary trees for your next exam, assignment or technical interview? -**BinaryTree** is a minimal Python library which gives you a simple API to -generate, visualize and inspect binary trees so you can skip the tedious -work of mocking up test objects and dive right into practising your algorithms! -Heaps and BSTs (binary search trees) are also supported. +**Binarytree** is a Python library which provides a simple API to generate, +visualize, inspect and manipulate binary trees. It allows you to skip the +tedious work of setting up test data, and dive straight into practising +your algorithms! Heaps and BSTs (binary search trees) are also supported. +Announcements +============= + +* **Binarytree** has been completely overhauled in version `3.0`_! +* Please see the releases_ page for details on the latest updates. + +.. _3.0: https://github.com/joowani/binarytree/releases/tag/3.0.0 +.. _releases: https://github.com/joowani/binarytree/releases + +Requirements +============ + +- Python 2.7, 3.4, 3.5 or 3.6 +- Pip_ installer + +.. _Pip: https://pip.pypa.io Installation ============ @@ -66,173 +82,290 @@ You may need to use ``sudo`` depending on your environment setup. Getting Started =============== -By default, **BinaryTree** uses the following class to represent a tree node: +By default, **binarytree** uses the following class to represent a node: .. code-block:: python class Node(object): - def __init__(self, value): - self.value = value - self.left = None - self.right = None + def __init__(self, value, left=None, right=None): + self.value = value # The node value + self.left = left # Left child + self.right = right # Right child Generate and pretty-print various types of binary trees: .. code-block:: python - from binarytree import tree, bst, heap, show - - # Generate a random binary tree and return its root - my_tree = tree(height=5, is_balanced=False) - - # Generate a random binary search tree (BST) and return its root - my_bst = bst(height=5) - - # Generate a random max heap and return its root - my_heap = heap(height=3, is_max=True) - - # Pretty-print the trees in stdout - show(my_tree) - show(my_bst) - show(my_heap) - - -`List representations`_ are supported as well: - -.. _List representations: - https://en.wikipedia.org/wiki/Binary_tree#Arrays - + >>> from binarytree import tree, bst, heap + >>> + >>> # Generate a random binary tree and return its root node + >>> my_tree = tree(height=3, is_perfect=False) + >>> + >>> # Generate a random BST and return its root node + >>> my_bst = bst(height=3, is_perfect=True) + >>> + >>> # Generate a random max heap and return its root node + >>> my_heap = heap(height=3, is_max=True, is_perfect=False) + >>> + >>> # Pretty-print the trees in stdout + >>> print(my_tree) + # + # _______1_____ + # / \ + # 4__ ___3 + # / \ / \ + # 0 9 13 14 + # / \ \ + # 7 10 2 + # + >>> print(my_bst) + # + # ______7_______ + # / \ + # __3__ ___11___ + # / \ / \ + # 1 5 9 _13 + # / \ / \ / \ / \ + # 0 2 4 6 8 10 12 14 + # + >>> print(my_heap) + # + # _____14__ + # / \ + # ____13__ 9 + # / \ / \ + # 12 7 3 8 + # / \ / + # 0 10 6 + # + +Use the `binarytree.Node`_ class to build your own trees: + +.. _binarytree.Node: + http://binarytree.readthedocs.io/en/latest/api.html#class-binarytree-node .. code-block:: python - from heapq import heapify - from binarytree import tree, convert, show - - my_list = [7, 3, 2, 6, 9, 4, 1, 5, 8] - - # Convert the list into a tree and return its root - my_tree = convert(my_list) - - # Convert the list into a heap and return its root - heapify(my_list) - my_tree = convert(my_list) - - # Convert the tree back to a list - my_list = convert(my_tree) - - # Pretty-printing also works on lists - show(my_list) - - -Inspect a tree to quickly see its various properties: + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.right = Node(4) + >>> + >>> print(root) + # + # __1 + # / \ + # 2 3 + # \ + # 4 + # + + +Inspect tree properties: .. code-block:: python - from binarytree import tree, inspect - - my_tree = tree(height=10) - - result = inspect(my_tree) - print(result['height']) - print(result['node_count']) - print(result['leaf_count']) - print(result['min_value']) - print(result['max_value']) - print(result['min_leaf_depth']) - print(result['max_leaf_depth']) - print(result['is_bst']) - print(result['is_max_heap']) - print(result['is_min_heap']) - print(result['is_height_balanced']) - print(result['is_weight_balanced']) - print(result['is_full']) - - -Use the `binarytree.Node` class to build your own trees: + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.left = Node(4) + >>> root.left.right = Node(5) + >>> + >>> print(root) + # + # __1 + # / \ + # 2 3 + # / \ + # 4 5 + # + >>> root.height + 2 + >>> root.is_balanced + True + >>> root.is_bst + False + >>> root.is_complete + True + >>> root.is_max_heap + False + >>> root.is_min_heap + True + >>> root.is_perfect + False + >>> root.is_strict + True + >>> root.leaf_count + 3 + >>> root.max_leaf_depth + 2 + >>> root.max_node_value + 5 + >>> root.min_leaf_depth + 1 + >>> root.min_node_value + 1 + >>> root.size + 5 + + >>> root.properties # To see all at once: + {'height': 2, + 'is_balanced': True, + 'is_bst': False, + 'is_complete': True, + 'is_max_heap': False, + 'is_min_heap': True, + 'is_perfect': False, + 'is_strict': True, + 'leaf_count': 3, + 'max_leaf_depth': 2, + 'max_node_value': 5, + 'min_leaf_depth': 1, + 'min_node_value': 1, + 'size': 5} + + >>> root.leaves + [Node(3), Node(4), Node(5)] + + >>> root.levels + [[Node(1)], [Node(2), Node(3)], [Node(4), Node(5)]] + +Use `level-order (breath-first)`_ indexes to manipulate nodes: + +.. _level-order (breath-first): + https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search .. code-block:: python - from binarytree import Node, show - - root = Node(1) - root.left = Node(2) - root.right = Node(3) - root.left.left = Node(4) - root.left.right = Node(5) - - show(root) - - -If the default `binarytree.Node` class does not meet your requirements, you can -define and use your own custom node specification: + >>> from binarytree import Node + >>> + >>> root = Node(1) # index: 0, value: 1 + >>> root.left = Node(2) # index: 1, value: 2 + >>> root.right = Node(3) # index: 2, value: 3 + >>> root.left.right = Node(4) # index: 4, value: 4 + >>> root.left.right.left = Node(5) # index: 9, value: 5 + >>> + >>> print(root) + # + # ____1 + # / \ + # 2__ 3 + # \ + # 4 + # / + # 5 + # + >>> # Use binarytree.Node.pprint instead of print to display indexes + >>> root.pprint(index=True) + # + # _________0-1_ + # / \ + # 1-2_____ 2-3 + # \ + # _4-4 + # / + # 9-5 + # + >>> # Return the node/subtree at index 9 + >>> root[9] + Node(5) + + >>> # Replace the node/subtree at index 4 + >>> root[4] = Node(6, left=Node(7), right=Node(8)) + >>> root.pprint(index=True) + # + # ______________0-1_ + # / \ + # 1-2_____ 2-3 + # \ + # _4-6_ + # / \ + # 9-7 10-8 + # + >>> # Delete the node/subtree at index 1 + >>> del root[1] + >>> root.pprint(index=True) + # + # 0-1_ + # \ + # 2-3 + + +Traverse the trees using different algorithms: .. code-block:: python - from binarytree import Node, customize, tree, show - - # Define your own null/sentinel value - my_null = -1 - - # Define your own node class - class MyNode(object): - - def __init__(self, data, left, right): - self.data = data - self.l_child = left - self.r_child = right - - # Call customize in the beginning of your code to apply your specification - customize( - node_init=lambda val: MyNode(val, my_null, my_null), - node_class=MyNode, - null_value=my_null, - value_attr='data', - left_attr='l_child', - right_attr='r_child' - ) - my_custom_tree = tree() - show(my_custom_tree) + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.left = Node(4) + >>> root.left.right = Node(5) + >>> + >>> print(root) + # + # __1 + # / \ + # 2 3 + # / \ + # 4 5 + # + >>> root.inorder + [Node(4), Node(2), Node(5), Node(1), Node(3)] + + >>> root.preorder + [Node(1), Node(2), Node(4), Node(5), Node(3)] + + >>> root.postorder + [Node(4), Node(5), Node(2), Node(3), Node(1)] + + >>> root.levelorder + [Node(1), Node(2), Node(3), Node(4), Node(5)] + + +`List representations`_ are also supported: +.. _List representations: + https://en.wikipedia.org/wiki/Binary_tree#Arrays -**New in 2.0.0**: Utility functions you can play around with: .. code-block:: python - from binarytree import tree, show_ids, show_all, subtree, prune, leafs - - my_tree = tree(height=5, is_balanced=False) + >>> from binarytree import build + >>> + >>> # Build a tree from list representation + >>> root = build([7, 3, 2, 6, 9, None, 1, 5, 8]) + >>> print(root) + # + # __7 + # / \ + # __3 2 + # / \ \ + # 6 9 1 + # / \ + # 5 8 + # + >>> # Convert the tree back to list representation + >>> list(root) + [7, 3, 2, 6, 9, None, 1, 5, 8] - # Show the level-order node IDs instead of values - show_ids(my_tree) - # Show both the node IDs and the values - show_all(my_tree) - # Return the root of the subtree by its level-order ID - subtree(my_tree, node_id=2) +Check out the documentation_ for more details! - # Prune a node (and its children) by its level-order ID - prune(my_tree, node_id=1) +.. _documentation: http://binarytree.readthedocs.io/en/latest/index.html - # Return the leaf nodes of the tree - leafs(my_tree, values_only=True) +Contributing +============ -**New in 2.0.0**: The default `binarytree.Node` class comes with additional goodies: - -.. code-block:: python - - from binarytree import tree - - my_tree = tree(height=5, is_balanced=False) +Please have a look at this page_ before submitting a pull request. Thanks! - # If you want to use these methods in your own custom class, your class - # will have to inherit from the default binarytree.Node class - my_tree.inspect() - my_tree.show() - my_tree.leafs() - my_tree.subtree(node_id=2).show() - my_tree.subtree(node_id=1).convert() - my_tree.prune(node_id=1).show_all() +.. _page: http://binarytree.readthedocs.io/en/latest/contributing.html \ No newline at end of file diff --git a/binarytree/__init__.py b/binarytree/__init__.py index a735904..fc4188e 100644 --- a/binarytree/__init__.py +++ b/binarytree/__init__.py @@ -1,307 +1,188 @@ -from inspect import isclass -from heapq import heapify -from random import sample, random +from __future__ import absolute_import, unicode_literals, division +__version__ = '3.0.0' +__all__ = ['Node', 'tree', 'bst', 'heap', 'build'] -def no_op(value): # pragma: no cover - return value +import heapq +import random +from binarytree.exceptions import ( + InvalidNodeValueError, + InvalidNodeIndexError, + InvalidNodeTypeError, + OperationForbiddenError, + NodeNotFoundError, + InvalidTreeHeightError, + CyclicNodeReferenceError, +) -_node_init = no_op -_node_cls = None -_null = None -_left_attr = 'left' -_right_attr = 'right' -_parent_attr = 'parent' -_value_attr = 'value' -_id_attr = 'level_order_id' +def _is_balanced(root): + """Return the height if the binary tree is balanced, -1 otherwise. -class Node(object): - """Represents a binary tree node.""" - - def __init__(self, value, parent=_null): - self.__setattr__(_value_attr, value) - self.__setattr__(_left_attr, _null) - self.__setattr__(_right_attr, _null) - if parent is not _null and isinstance(parent, Node): - self.__setattr__(_parent_attr, parent) - else: - self.__setattr__(_parent_attr, _null) - - def __repr__(self): - return 'Node({})'.format( - self.__getattribute__(_value_attr) - ) - - def __setattr__(self, name, value): - # Magically set the parent to self when a child is created - if (name in [_left_attr, _right_attr] - and value is not _null - and isinstance(value, Node)): - value.parent = self - object.__setattr__(self, name, value) - - def __getitem__(self, index): - return _get_node_with_id(self, index) - - def __setitem__(self, index, value): - return _set_node_with_id(self, index, value) - - def convert(self): - return convert(self) - - def inspect(self): - return inspect(self) - - def subtree(self, node_id): - return subtree(self, node_id) - - def prune(self, node_id): - return prune(self, node_id) - - def is_root(self): - return self.parent is _null - - def is_leaf(self): - return self.right is _null and self.left is _null - - def is_child_of(self, parent): - return self.parent is parent - - def is_left_child_of(self, parent): - return self is _left_of(parent) - - def is_right_child_of(self, parent): - return self is _right_of(parent) - - def leafs(self, values_only=False): - return leafs(self, values_only) - - def level(self): - return 0 if self.parent == _null else self.parent.level() + 1 - - def show(self): - show(self) - - def show_ids(self): - show_ids(self) - - def show_all(self): - show_all(self) - - -def _create_node(value, parent=_null): - """Create and return a new node.""" - if _node_init != no_op: - return _node_init(value) - return (_node_cls or Node)(value, parent=parent) - - -def _is_list(obj): - """Return ``True`` if the object is a list, else ``False``.""" - return isinstance(obj, list) - - -def _is_node(obj): - """Return ``True`` if the object is a node, else ``False``.""" - return isinstance(obj, _node_cls or Node) - - -def _id_of(node): - """Return the level-order ID of the node.""" - return getattr(node, _id_attr) - - -def _value_of(node): - """Return the value of the node.""" - return getattr(node, _value_attr) - - -def _parent_of(node): - """Return the parent of the node.""" - return getattr(node, _parent_attr) - - -def _left_of(node): - """Return the left child of the node.""" - return getattr(node, _left_attr) - - -def _right_of(node): - """Return the right child of the node.""" - return getattr(node, _right_attr) - - -def _set_left(node, child): - """Set the child to the left of the node.""" - setattr(node, _left_attr, child) - - -def _set_right(node, child): - """Set the child to the right of the node.""" - setattr(node, _right_attr, child) - - -def _set_id(node, node_id): - """Set the level-order ID of the node.""" - setattr(node, _id_attr, node_id) - - -def _copy_with_id(node, node_id): - """Return a copy of the node with the level-order ID injected.""" - node_copy = _create_node(_value_of(node), parent=_parent_of(node)) - _set_id(node_copy, node_id) - return node_copy - - -def _prune_left(node): - """Prune the left subtree of the node.""" - node.__setattr__(_left_attr, _null) - - -def _prune_right(node): - """Prune the right subtree of the node.""" - node.__setattr__(_right_attr, _null) - - -def _is_balanced(node): - """Return the depth if balanced else -1.""" - if node == _null: + :param root: The root node of the binary tree. + :type root: binarytree.Node | None + :return: The height or -1. + :rtype: int + """ + if root is None: return 0 - - left = _is_balanced(_left_of(node)) + left = _is_balanced(root.left) if left < 0: return -1 - - right = _is_balanced(_right_of(node)) - if right < 0 or abs(left - right) > 1: + right = _is_balanced(root.right) + if right < 0: return -1 + return -1 if abs(left - right) > 1 else max(left, right) + 1 - return max(left, right) + 1 +def _is_bst(root, min_value=float('-inf'), max_value=float('inf')): + """Check if the binary tree is a BST (binary search tree). -def _is_bst(node, min_val=float('-inf'), max_val=float('inf')): - """Return True if and only if the tree is a binary search tree.""" - if node == _null: + :param root: The root node of the binary tree. + :type root: binarytree.Node | None + :param min_value: The minimum node value seen. + :type min_value: int | float + :param max_value: The maximum node value seen. + :type max_value: int | float + :return: True if the binary tree is a BST, False otherwise. + :rtype: bool + """ + if root is None: return True + return ( + min_value < root.value < max_value and + _is_bst(root.left, min_value, root.value) and + _is_bst(root.right, root.value, max_value) + ) - if (min_val != _null and _value_of(node) <= min_val): - return False - if (max_val != _null and _value_of(node) >= max_val): - return False +def _validate_tree_height(height): + """Check if the height of the binary tree is valid. - return _is_bst(_left_of(node), min_val, _value_of(node)) and \ - _is_bst(_right_of(node), _value_of(node), max_val) - - -def _build_list(root): - """Build a list from a tree and return it.""" - result = [] - current_nodes = [root] - level_not_empty = True + :param height: The height of the binary tree (must be 0 - 9 inclusive). + :type height: int + :raise binarytree.exceptions.InvalidTreeHeightError: + If an invalid tree height is given. + """ + if not (isinstance(height, int) and 0 <= height <= 9): + raise InvalidTreeHeightError( + 'The height must be an integer between 0 - 9' + ) - while level_not_empty: - level_not_empty = False - next_nodes = [] - for node in current_nodes: - if node == _null: - result.append(_null) - next_nodes.append(_null) - next_nodes.append(_null) - else: - result.append(_value_of(node)) +def _generate_perfect_bst(height): + """Generate a perfect BST (binary search tree) and return its root node. - left_child = _left_of(node) - right_child = _right_of(node) + :param height: The height of the binary tree to build. + :type height: int + :return: The root node of the BST. + :rtype: binarytree.Node + """ + max_node_count = 2 ** (height + 1) - 1 + node_values = list(range(max_node_count)) + return _build_bst_from_sorted_values(node_values) - if left_child != _null: - level_not_empty = True - if right_child != _null: - level_not_empty = True - next_nodes.append(left_child) - next_nodes.append(right_child) +def _build_bst_from_sorted_values(sorted_values): + """Recursively build a perfect BST from odd number of sorted values. - current_nodes = next_nodes + :param sorted_values: Odd number of sorted values. + :type sorted_values: [int] + :return: The root node of the BST. + :rtype: binarytree.Node + """ + if len(sorted_values) == 0: + return None + mid_index = len(sorted_values) // 2 + root = Node(sorted_values[mid_index]) + root.left = _build_bst_from_sorted_values(sorted_values[:mid_index]) + root.right = _build_bst_from_sorted_values(sorted_values[mid_index + 1:]) + return root - while result and result[-1] == _null: - result.pop() - return result +def _generate_random_leaf_count(height): + """Return a random leaf count for building binary trees. -def _build_tree(values): - """Build a tree from a list and return its root.""" - if not values: - return _null + :param height: The height of the binary tree to build. + :type height: int + :return: Randomly generated leaf count. + :rtype: int + """ + max_leaf_count = 2 ** height + half_leaf_count = max_leaf_count // 2 - nodes = [_null for _ in values] - if values[0] == _null: - raise ValueError('Node missing at index 0') + # A very naive way of mimicking normal distribution + roll_1 = random.randint(0, half_leaf_count) + roll_2 = random.randint(0, max_leaf_count - half_leaf_count) + return roll_1 + roll_2 or half_leaf_count - root = _create_node(values[0], parent=_null) - nodes[0] = root - index = 1 - while index < len(values): - value = values[index] - if value != _null: - parent_index = int((index + 1) / 2) - 1 - parent_node = nodes[parent_index] - if parent_node == _null: - raise ValueError( - 'Node missing at index {}' - .format(parent_index) - ) - child_node = _create_node(value, parent=parent_node) - if index % 2: # is odd - _set_left(parent_node, child_node) - else: - _set_right(parent_node, child_node) - nodes[index] = child_node - index += 1 +def _generate_random_node_values(height): + """Return random node values for building binary trees. - return root + :param height: The height of the binary tree to build. + :type height: int + :return: Randomly generated node values. + :rtype: [int] + """ + max_node_count = 2 ** (height + 1) - 1 + node_values = list(range(max_node_count)) + random.shuffle(node_values) + return node_values -def _build_repr(node, with_ids=False, with_values=True): - """Recursive function used for pretty-printing the binary tree. +def _build_tree_string(root, curr_index, index=False, delimiter='-'): + """Recursively traverse down the binary tree build a pretty-print string. In each recursive call, a "box" of characters visually representing the - current subtree is constructed line by line. Each line is padded with - whitespaces to ensure all lines have the same length. The box, its width, - and the start-end positions of its root (used for drawing branches) are - sent up to the parent call, which then combines left and right sub-boxes - to build a bigger box etc. + current (sub)tree is constructed line by line. Each line is padded with + whitespaces to ensure all lines in the box have the same length. Then the + box, its width, and start-end positions of its root value repr (required + for drawing branches) are sent up to the parent call. The parent call then + combines its left and right sub-boxes to construct a larger box etc. + + :param root: The root node of the binary tree. + :type root: binarytree.Node | None + :param curr_index: The level-order_ index of the current node (root is 0). + :type curr_index: int + :param index: If set to True, include the level-order_ node indexes + using the following format: ``{index}{delimiter}{value}`` + (default: False). + :type index: bool + :param delimiter: The delimiter character between the node index and value + (default: '-'). + :type delimiter: + :return: The box of characters visually representing the current subtree, + the width of the box, and the start-end positions of the new root value + repr string. + :rtype: ([str], int, int, int) + + .. _level-order: + https://en.wikipedia.org/wiki/Tree_traversal """ - if node == _null: + if root is None: return [], 0, 0, 0 - if with_ids and with_values: - node_repr = "{}:{}".format(_id_of(node), _value_of(node)) - elif with_ids and not with_values: - node_repr = str(_id_of(node)) - elif not with_ids and with_values: - node_repr = str(_value_of(node)) - else: # pragma: no cover - node_repr = "O" - line1 = [] line2 = [] + if index: + node_repr = '{}{}{}'.format(curr_index, delimiter, root.value) + else: + node_repr = str(root.value) + new_root_width = gap_size = len(node_repr) - # Get the left and right sub-boxes, their widths and their root positions + # Get the left and right sub-boxes, their widths, and root repr positions l_box, l_box_width, l_root_start, l_root_end = \ - _build_repr(_left_of(node), with_ids, with_values) + _build_tree_string(root.left, 2 * curr_index + 1, index, delimiter) r_box, r_box_width, r_root_start, r_root_end = \ - _build_repr(_right_of(node), with_ids, with_values) + _build_tree_string(root.right, 2 * curr_index + 2, index, delimiter) - # Draw the branch connecting the new root to the left sub-box, - # padding with whitespaces where necessary + # Draw the branch connecting the current root to the left sub-box + # Pad with whitespaces where necessary if l_box_width > 0: - l_root = -int(-(l_root_start + l_root_end) / 2) + 1 # ceiling + l_root = (l_root_start + l_root_end) // 2 + 1 line1.append(' ' * (l_root + 1)) line1.append('_' * (l_box_width - l_root)) line2.append(' ' * l_root + '/') @@ -311,14 +192,14 @@ def _build_repr(node, with_ids=False, with_values=True): else: new_root_start = 0 - # Draw the representation of the new root + # Draw the representation of the current root line1.append(node_repr) line2.append(' ' * new_root_width) - # Draw the branch connecting the new root to the right sub-box, - # padding with whitespaces where necessary + # Draw the branch connecting the current root to the right sub-box + # Pad with whitespaces where necessary if r_box_width > 0: - r_root = int((r_root_start + r_root_end) / 2) # floor + r_root = (r_root_start + r_root_end) // 2 line1.append('_' * r_root) line1.append(' ' * (r_box_width - r_root + 1)) line2.append(' ' * r_root + '\\') @@ -338,796 +219,1661 @@ def _build_repr(node, with_ids=False, with_values=True): return new_box, len(new_box[0]), new_root_start, new_root_end -def _bst_insert(root, value): - """Insert a node into the BST.""" - depth = 1 - node = root - while True: - if _value_of(node) > value: - left_child = _left_of(node) - if left_child == _null: - _set_left(node, _create_node(value, parent=node)) - break - node = left_child - else: - right_child = _right_of(node) - if right_child == _null: - _set_right(node, _create_node(value, parent=node)) - break - node = right_child - depth += 1 - return depth - - -def _random_insert(root, value): - """Insert a node randomly into the binary tree.""" - depth = 1 - node = root - while True: - if random() < 0.5: - left_child = _left_of(node) - if left_child == _null: - _set_left(node, _create_node(value, parent=node)) - break - node = left_child - else: - right_child = _right_of(node) - if right_child == _null: - _set_right(node, _create_node(value, parent=node)) - break - node = right_child - depth += 1 - return depth - - -def _inject_ids(root): - """Return a new copy of the tree with node IDs injected.""" - root_copy = _copy_with_id(root, 0) - id_counter = 1 - - current_nodes = [root] - current_copies = [root_copy] - - while current_nodes: - next_nodes = [] - next_copies = [] - - index = 0 - while index < len(current_nodes): - node = current_nodes[index] - node_copy = current_copies[index] - - left_child = _left_of(node) - right_child = _right_of(node) - - if left_child != _null: - next_nodes.append(left_child) - left_child_copy = _copy_with_id(left_child, id_counter) - _set_left(node_copy, left_child_copy) - next_copies.append(left_child_copy) - id_counter += 1 - - if right_child != _null: - next_nodes.append(right_child) - right_child_copy = _copy_with_id(right_child, id_counter) - _set_right(node_copy, right_child_copy) - next_copies.append(right_child_copy) - id_counter += 1 - index += 1 - - current_nodes = next_nodes - current_copies = next_copies - - return root_copy - - -def _validate_tree(root): - """Check if the tree is malformed.""" - current_nodes = [root] - - while current_nodes: - next_nodes = [] - for node in current_nodes: - if _is_node(node): - if _value_of(node) == _null: - raise ValueError('A node cannot have a null value') - next_nodes.append(_left_of(node)) - next_nodes.append(_right_of(node)) - elif node != _null: - # Halt if the node is not NULL nor a node instance - raise ValueError('Found an invalid node in the tree') - current_nodes = next_nodes - - return root - - -def _prepare_tree(bt): - """Prepare the binary tree for tree algorithms.""" - if _is_list(bt): - return _build_tree(bt) - if _is_node(bt): - return _validate_tree(bt) - raise ValueError('Expecting a list or a node') - - -def _validate_id(node_id): - """Check if the ID is valid.""" - if not isinstance(node_id, int): - raise ValueError('The node ID must be an integer') - if node_id < 0: - raise ValueError('The node ID must start from 0') - - -def _generate_values(height, multiplier=1): - """Generate and return a list of random node values.""" - if not isinstance(height, int) or height < 0: - raise ValueError('Height must be a non-negative integer') - count = 2 ** (height + 1) - 1 - return sample(range(count * multiplier), count) - - -def _get_node_with_id(root, index): - """Calculate index in the tree relative to the root node - - :param root: The root node of the tree - :type: Node - :param index: index of the node to replace - :type: Int - """ - root = _prepare_tree(root) - if not isinstance(index, int) or index < 0: - raise ValueError("Requested id must be a non-negative integer.") - current_nodes = [root] - current_id = -1 - current_index = 0 - - while current_nodes and current_id < index: - next_nodes = [] - current_index = 0 - - while current_index < len(current_nodes) and current_id < index: - node = current_nodes[current_index] - if node is not _null: - left_child = _left_of(node) - right_child = _right_of(node) - else: - left_child = _null - right_child = _null - - if left_child != _null: - next_nodes.append(left_child) - else: - next_nodes.append(_null) - if right_child != _null: - next_nodes.append(right_child) - else: - next_nodes.append(_null) - current_index += 1 - current_id += 1 - current_nodes = next_nodes - - if node is _null: - raise IndexError("Requested node id not present in tree.") - return node - - -def _get_parent_of_node_with_id(root, index): - """Get the index in the tree of the parent of a node. - Used for setting new nodes with the setitem method. - - :param root: The root node of the tree - :type: Node - :param index: index of the node whose parent you're looking for - :type: Int - """ - root = _prepare_tree(root) - current_nodes = [root] - current_id = 0 - current_index = 0 - - while current_nodes and current_id <= index: - next_nodes = [] - current_index = 0 - - while current_index < len(current_nodes) and current_id <= index: - node = current_nodes[current_index] - if _is_node(node): - left_child = _left_of(node) - right_child = _right_of(node) - else: - left_child = _null - right_child = _null - - if left_child != _null: - next_nodes.append(left_child) - elif _is_node(node): - next_nodes.append((current_id, 'left')) - else: - next_nodes.append(_null) - if right_child != _null: - next_nodes.append(right_child) - elif _is_node(node): - next_nodes.append((current_id, 'right')) - else: - next_nodes.append(_null) - current_index += 1 - current_id += 1 - current_nodes = next_nodes - - if node is _null: - raise IndexError("Requested node id's parent not present in tree.") - elif _is_node(node): - return _parent_of(node), 'right' if node.is_right_child_of(_parent_of(node)) else 'left' - else: - return _get_node_with_id(root, node[0]), node[1] - - -def _set_node_with_id(root, index, value): - """Set the node with the given id with the passed in value - - :param root: The root node of the tree - :type: Node - :param index: index of the node to replace - :type: Int - :param value: New node to replace the node at the given index - :type: Node - """ - if not _is_node(value): - raise ValueError("Value must be of type Node.") - if not isinstance(index, int) or index < 0: - raise ValueError("Index must be a positive integer.") - elif index == 0: - raise ValueError("Cannot replace root node of tree. " - "Index must be a positive integer.") - parent, left_or_right = _get_parent_of_node_with_id(root, index) - if left_or_right is 'right': - _set_right(parent, value) - else: - _set_left(parent, value) - - -def customize(node_class, - node_init, - null_value, - value_attr, - left_attr, - right_attr): - """Set up a custom specification for the binary tree node. - - :param node_class: The binary tree node class. - :type node_class: type - :param node_init: The node initializer function which must take the - node value as the only argument and return an instance of node_class. - :type node_init: callable - :param null_value: The null/sentinel value. - :type null_value: object - :param value_attr: The attribute name reserved for the node value. - :type value_attr: str | unicode - :param left_attr: The attribute name reserved for the left child. - :type left_attr: str | unicode - :param right_attr: The attribute name reserved for the right child. - :type right_attr: str | unicode - :raises ValueError: If an invalid set of arguments is given. - """ - global _node_cls - global _node_init - global _null - global _value_attr - global _left_attr - global _right_attr - - # Do some sanity checking on the arguments - if not isclass(node_class): - raise ValueError('Invalid class given for the node') - try: - node = node_init(2 if null_value == 1 else 1) - except: - raise ValueError( - 'The node initializer function must be a callable which ' - 'takes the node value as its only argument' - ) - if not isinstance(node, node_class): - raise ValueError( - 'The node initializer function must be a callable which ' - 'returns an instance of "{}"'.format(node_class.__name__) - ) - for attribute in [value_attr, left_attr, right_attr]: - if not hasattr(node, attribute): - raise ValueError( - 'The node class does not have one of the required ' - 'attributes "{}"'.format(attribute) - ) - if getattr(node, left_attr) != null_value: - raise ValueError( - 'The node class does not initialize instances with expected ' - 'null/sentinel value "{}" for its left child node attribute ' - '"{}"'.format(null_value, left_attr) - ) - if getattr(node, right_attr) != null_value: - raise ValueError( - 'The node class does not initialize instances with expected ' - 'null/sentinel value "{}" for its right child node attribute ' - '"{}"'.format(null_value, right_attr) - ) - - _node_cls = node_class - _node_init = node_init - _null = null_value - _value_attr = value_attr - _left_attr = left_attr - _right_attr = right_attr - - -def tree(height=4, is_balanced=False): - """Generate a random binary tree and return its root. +def _get_tree_properties(root): + """Inspect the binary tree and return its properties (e.g. height). - :param height: The height of the tree (default: 4). - :type height: int - :param is_balanced: The tree is weight-balanced (default: ``False``). - :type is_balanced: bool - :return: The root of the generated binary tree. - :rtype: binarytree.Node - :raises ValueError: If an invalid binary tree is given. - """ - values = _generate_values(height) - if is_balanced: - return _build_tree(values) - - root = _create_node(values[0]) - for index in range(1, len(values)): - depth = _random_insert(root, values[index]) - if depth == height: - break - return root - - -def bst(height=4): - """Generate a random binary search tree and return its root. - - :param height: The height of the tree (default: 4). - :type height: int - :return: The root node of the generated binary search tree. + :param root: The root node of the binary tree. :rtype: binarytree.Node - :raises ValueError: If an invalid binary tree is given. - """ - values = _generate_values(height) - root = _create_node(values[0]) - for index in range(1, len(values)): - depth = _bst_insert(root, values[index]) - if depth == height: - break - return root - - -def heap(height=4, is_max=False): - """Generate a random min/max heap and return its root. - - :param height: The height of the tree (default: 4). - :type height: int - :param is_max: Whether to generate a max or min heap. - :type is_max: bool - :return: The root node of the generated heap. - :rtype: binarytree.Node - :raises ValueError: If an invalid binary tree is given. - """ - values = _generate_values(height) - if is_max: - negated = [-v for v in values] - heapify(negated) - return _build_tree([-v for v in negated]) - else: - heapify(values) - return _build_tree(values) - - -def stringify(bt, with_ids=False, with_values=True): - """Return the string representation of the binary tree. - - :param bt: The binary tree. - :type bt: list | binarytree.Node - :param with_ids: Add level-order IDs into the nodes. - :type with_ids: bool - :param with_values: Display node values. - :type with_values: bool - :return: The string representation. - :rtype: str | unicode - :raises ValueError: If an invalid binary tree is given. - """ - if bt == _null: - return '' - if _is_list(bt) and not bt: - return '' - - bt = _prepare_tree(bt) - if with_ids: - bt = _inject_ids(bt) - return '\n' + '\n'.join(_build_repr(bt, with_ids, with_values)[0]) - - -def show(bt): - """Pretty print the binary tree (the node values). - - :param bt: The binary tree to pretty-print. - :type bt: list | binarytree.Node - :return: None - :rtype: None - :raises ValueError: If an invalid binary tree is given. - """ - print(stringify(bt, with_ids=False, with_values=True)) - - -def show_all(bt): - """Pretty print the binary tree with both the level-order IDs and values. - - :param bt: The binary tree to pretty-print. - :type bt: list | binarytree.Node - :return: None - :rtype: None - :raises ValueError: If an invalid binary tree is given. - """ - print(stringify(bt, with_ids=True, with_values=True)) - - -def show_ids(bt): - """Pretty print the binary tree showing just the level-order node IDs. - - :param bt: The binary tree to pretty-print. - :type bt: list | binarytree.Node - :return: None - :rtype: None - :raises ValueError: If an invalid binary tree is given. - """ - print(stringify(bt, with_ids=True, with_values=False)) - - -def pprint(bt): - """Pretty print the binary tree. - - Equivalent to `show`. Still here for backwards compatibility. - - :param bt: The binary tree to pretty-print. - :type bt: list | binarytree.Node - :return: None - :rtype: None - :raises ValueError: If an invalid binary tree is given. - """ - show(bt) - - -def convert(bt): - """Convert a binary tree into a list, or vice versa. - - :param bt: The binary tree to convert. - :type bt: list | binarytree.Node - :return: The converted form of the binary tree. - :rtype: list | binarytree.Node - :raises ValueError: If an invalid binary tree is given. - """ - if bt == _null: - return [] - if _is_list(bt): - return _build_tree(bt) - if _is_node(bt): - return _build_list(_validate_tree(bt)) - raise ValueError('Expecting a list or a node') - - -def get_level(bt, level, show_values=False, show_nulls=False): - """Return the requested level of the binary tree, ordered from left to right. - - If a node other than the root node is passed in, then this function - returns the requested level of the tree relative to the passed in node. - - :param bt: The binary tree. - :type bt: binarytree.Node - :param level: The requested level to return. - :type level: int - :param show_values: whether to convert nodes to values before returning. - :type show_values: boolean - :param show_nulls: whether to show where empty nodes are. - :type show_nulls: boolean - :return: dictionary of form {0:[rootnode], 1:[1st, level, nodes]} - :rtype: dictionary - """ - bt = _prepare_tree(bt) - if not isinstance(level, int) or level < 0: - raise ValueError("Requested level must be a non-negative integer.") - current_nodes = [bt] - current_level = 0 - - while current_level < level: - next_nodes = [] - index = 0 - - while index < len(current_nodes): - node = current_nodes[index] - left_child = _left_of(node) - right_child = _right_of(node) - - if left_child != _null: - next_nodes.append(left_child) - elif show_nulls is True: - next_nodes.append(Node(_null)) - if right_child != _null: - next_nodes.append(right_child) - elif show_nulls is True: - next_nodes.append(Node(_null)) - index += 1 - - if len(next_nodes) == 0 or all(node.value is _null for node in next_nodes): - raise ValueError("Requested level not present in tree.") - current_nodes = next_nodes - current_level += 1 - - return [node.value for node in current_nodes] if show_values else current_nodes - - -def get_levels(bt, show_values=False, show_nulls=False): - """Return the levels of the binary tree, ordered from left to right. - - If a node other than the root node is passed in, then this function - returns the levels of the tree relative to the passed in node. - - :param bt: The binary tree. - :type bt: binarytree.Node - :param show_values: whether to convert nodes to values before returning. - :type show_values: boolean - :param show_nulls: whether to show where empty nodes are. - :type show_nulls: boolean - :return: dictionary of form {0:[rootnode], 1:[1st, level, nodes]} - :rtype: dictionary - """ - bt = _prepare_tree(bt) - - current_nodes = [bt] - levels = [] - current_level = 0 - - while len(current_nodes) != 0 and not all(node.value is _null for node in current_nodes): - levels.append(current_nodes) - next_nodes = [] - index = 0 - - while index < len(current_nodes): - node = current_nodes[index] - left_child = _left_of(node) - right_child = _right_of(node) - - if left_child != _null: - next_nodes.append(left_child) - elif show_nulls is True: - next_nodes.append(_create_node(_null)) - if right_child != _null: - next_nodes.append(right_child) - elif show_nulls is True: - next_nodes.append(_create_node(_null)) - index += 1 - - current_nodes = next_nodes - current_level += 1 - - if show_values is True: - for level in range(len(levels)): - levels[level] = [node.value for node in levels[level]] - return levels - - -def inspect(bt): - """Return the properties of the binary tree. - - :param bt: The binary tree to inspect. - :type bt: list | binarytree.Node - :return: The various properties of the binary tree. + :return: The properties of the binary tree. :rtype: dict - :raises ValueError: If an invalid binary tree is given. """ - bt = _prepare_tree(bt) - - is_full = True is_descending = True is_ascending = True - is_left_padded = True - min_value = float('inf') - max_value = float('-inf') - node_count = 0 + min_node_value = root.value + max_node_value = root.value + size = 0 leaf_count = 0 min_leaf_depth = 0 - current_depth = -1 - current_nodes = [bt] - - while current_nodes: + max_leaf_depth = -1 + is_strict = True + is_complete = True + current_nodes = [root] + non_full_node_seen = False - null_encountered = False - current_depth += 1 + while len(current_nodes) > 0: + max_leaf_depth += 1 next_nodes = [] for node in current_nodes: - num_of_children = 0 - node_count += 1 - node_value = _value_of(node) - min_value = min(node_value, min_value) - max_value = max(node_value, max_value) - - left_child = _left_of(node) - right_child = _right_of(node) - - for child in (left_child, right_child): - if child != _null and null_encountered: - is_left_padded = False - elif child == _null and not null_encountered: - null_encountered = True - - if left_child == _null and right_child == _null: + size += 1 + value = node.value + min_node_value = min(value, min_node_value) + max_node_value = max(value, max_node_value) + + # The node is a leaf. + if node.left is None and node.right is None: if min_leaf_depth == 0: - min_leaf_depth = current_depth + min_leaf_depth = max_leaf_depth leaf_count += 1 - if left_child != _null: - if _value_of(left_child) > node_value: + if node.left is not None: + if node.left.value > value: is_descending = False - elif _value_of(left_child) < node_value: + elif node.left.value < value: is_ascending = False - next_nodes.append(left_child) - num_of_children += 1 + next_nodes.append(node.left) + is_complete = not non_full_node_seen + else: + non_full_node_seen = True - if right_child != _null: - if _value_of(right_child) > node_value: + if node.right is not None: + if node.right.value > value: is_descending = False - elif _value_of(right_child) < node_value: + elif node.right.value < value: is_ascending = False - next_nodes.append(right_child) - num_of_children += 1 - if num_of_children == 1: - is_full = False + next_nodes.append(node.right) + is_complete = not non_full_node_seen + else: + non_full_node_seen = True - current_nodes = next_nodes + # If we see a node with only one child, it is not strict + is_strict &= (node.left is None) == (node.right is None) - is_balanced = _is_balanced(bt) >= 0 + current_nodes = next_nodes return { - 'is_height_balanced': current_depth - min_leaf_depth < 2, - 'is_weight_balanced': is_balanced, - 'is_max_heap': is_descending and is_left_padded and is_balanced, - 'is_min_heap': is_ascending and is_left_padded and is_balanced, - 'is_bst': _is_bst(bt), - 'height': current_depth, + 'height': max_leaf_depth, + 'size': size, + 'is_max_heap': is_complete and is_descending, + 'is_min_heap': is_complete and is_ascending, + 'is_perfect': leaf_count == 2 ** max_leaf_depth, + 'is_strict': is_strict, + 'is_complete': is_complete, 'leaf_count': leaf_count, - 'node_count': node_count, + 'min_node_value': min_node_value, + 'max_node_value': max_node_value, 'min_leaf_depth': min_leaf_depth, - 'max_leaf_depth': current_depth, - 'min_value': min_value, - 'max_value': max_value, - 'is_full': is_full + 'max_leaf_depth': max_leaf_depth, } -def subtree(bt, node_id): - """Return the node and its children (i.e. subtree) of the level-order ID. - - If the binary tree is given as a list, it is automatically converted - into a tree form first. - - :param bt: The binary tree. - :type bt: list | binarytree.Node - :param node_id: The level-order ID of the node. - :type node_id: int - :return: The root of the subtree. - :rtype: binarytree.Node - :raises ValueError: If an invalid binary tree or node ID is given. +class Node(object): + """Represents a binary tree node. + + This class provides methods and properties for managing the calling node, + and the binary tree which the calling node is the root of. Whenever a + docstring in this class says "binary tree", it is referring to the calling + node instance and its descendants. + + :param value: The node value. Only integers are supported. + :type value: int + :param left: The left child node (default: None). + :type left: binarytree.Node | None + :param right: The right child node (default: None). + :type right: binarytree.Node | None + :raise binarytree.exceptions.InvalidNodeValueError: + If the node value is not an integer. + :raise binarytree.exceptions.InvalidNodeTypeError: + If the left or right child is not an instance of + :class:`binarytree.Node`. """ - bt = _prepare_tree(bt) - _validate_id(node_id) - current_nodes = [bt] - current_id = 0 + def __init__(self, value, left=None, right=None): + if not isinstance(value, int): + raise InvalidNodeValueError('The node value must be an integer') + if left is not None and not isinstance(left, Node): + raise InvalidNodeTypeError( + 'The left child node is not a binarytree.Node instance') + if right is not None and not isinstance(right, Node): + raise InvalidNodeTypeError( + 'The right child node is not a binarytree.Node instance') - while current_nodes: - next_nodes = [] + self.value = value + self.left = left + self.right = right - for node in current_nodes: - if current_id == node_id: - return node - current_id += 1 - - left_child = _left_of(node) - right_child = _right_of(node) - - if left_child != _null: - next_nodes.append(left_child) - if right_child != _null: - next_nodes.append(right_child) + def __repr__(self): + """Return the string representation of the node. + + :return: The string representation. + :rtype: str | unicode + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> Node(1) + Node(1) + """ + return 'Node({})'.format(self.value) + + def __str__(self): + """Return the pretty-print string for the binary tree. + + :return: The pretty-print string. + :rtype: str | unicode + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.right = Node(4) + >>> + >>> print(root) + + __1 + / \\ + 2 3 + \\ + 4 + + + .. note:: + + To include `level-order (breath-first)`_ indexes in the string, use + :func:`binarytree.Node.pprint` instead. + + .. _level-order (breath-first): + https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search + """ + lines = _build_tree_string(self, 0, False, '-')[0] + return '\n' + '\n'.join((line.rstrip() for line in lines)) + + def __setattr__(self, attribute, obj): + """Modified version of **__setattr__** with extra sanity checks + around class attributes **left**, **right** and **value**. + + :param attribute: The name of the class attribute. + :type attribute: str | unicode + :param obj: The object to set. + :type obj: object + :raise binarytree.exceptions.InvalidNodeTypeError: + If the left or right child is not an instance of + :class:`binarytree.Node`. + :raise binarytree.exceptions.InvalidNodeValueError: + If the node value is not an integer. + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> node = Node(1) + >>> node.left = 'invalid' # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidNodeTypeError: The node is not a binarytree.Node instance + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> node = Node(1) + >>> node.value = 'invalid' # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidNodeValueError: The node value must be an integer + """ + if attribute == 'left' or attribute == 'right': + if obj is not None and not isinstance(obj, Node): + raise InvalidNodeTypeError( + 'The node is not a binarytree.Node instance') + elif attribute == 'value' and not isinstance(obj, int): + raise InvalidNodeValueError('The node value must be an integer') + object.__setattr__(self, attribute, obj) + + def __iter__(self): + """Return the `list representation`_ of the binary tree. + + .. _list representation: + https://en.wikipedia.org/wiki/Binary_tree#Arrays + + :return: The list representation consisting of node values or None's. + If a node has an index i, its left child is at index 2i + 1, right + child at index 2i + 2, and parent at index floor((i - 1) / 2). None + signifies the absence of a node. See example below for an + illustration. + :rtype: [int | None] + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.right = Node(4) + >>> + >>> list(root) + [1, 2, 3, None, 4] + """ + current_nodes = [self] + has_more_nodes = True + values = [] + + while has_more_nodes: + has_more_nodes = False + next_nodes = [] + for node in current_nodes: + if node is None: + values.append(None) + next_nodes.extend((None, None)) + continue + + if node.left is not None or node.right is not None: + has_more_nodes = True + + values.append(node.value) + next_nodes.extend((node.left, node.right)) + + current_nodes = next_nodes + + # Get rid of the trailing None entries + while values and values[-1] is None: + values.pop() + + return iter(values) + + def __len__(self): + """Return the total number of nodes in the binary tree. + + :return: The total number of nodes. + :rtype: int + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> + >>> len(root) + 3 + + .. note:: + + This method is equivalent to :attr:`binarytree.Node.size`. + """ + return self.properties['size'] - current_nodes = next_nodes + def __getitem__(self, index): + """Return the node/subtree at the give `level-order (breath-first)`_ + index. + + :param index: The node index. + :type index: int + :return: The node at the given index. + :rtype: binarytree.Node + :raise binarytree.exceptions.InvalidNodeIndexError: + If an invalid index is given. + :raise binarytree.exceptions.NodeNotFoundError: + If the target node does not exist. + + .. _level-order (breath-first): + https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) # index: 0, value: 1 + >>> root.left = Node(2) # index: 1, value: 2 + >>> root.right = Node(3) # index: 2, value: 3 + >>> + >>> root[0] + Node(1) + >>> root[1] + Node(2) + >>> root[2] + Node(3) + >>> root[3] # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + NodeNotFoundError: Node missing at index 3 + """ + if not isinstance(index, int) or index < 0: + raise InvalidNodeIndexError( + 'The node index must be a non-negative integer') + + current_nodes = [self] + current_index = 0 + has_more_nodes = True + + while has_more_nodes: + has_more_nodes = False + next_nodes = [] + + for node in current_nodes: + if current_index == index: + if node is None: + break + else: + return node + current_index += 1 + + if node is None: + next_nodes.extend((None, None)) + continue + next_nodes.extend((node.left, node.right)) + if node.left is not None or node.right is not None: + has_more_nodes = True + + current_nodes = next_nodes + + raise NodeNotFoundError('Node missing at index {}'.format(index)) + + def __setitem__(self, index, node): + """Insert the node/subtree into the binary tree at the given + `level-order (breath-first)`_ index. + + * An exception is raised if the parent node does not exist. + * Any existing node/subtree is overwritten. + * The root node (calling node) cannot be replaced. + + :param index: The node index. + :type index: int + :param node: The new node to insert. + :type node: binarytree.Node + :raise binarytree.exceptions.OperationForbiddenError: + If the user tries to overwrite the root node (calling node). + :raise binarytree.exceptions.NodeNotFoundError: + If the parent for the new node does not exist. + :raise binarytree.exceptions.InvalidNodeTypeError: + If the new node is not an instance of :class:`binarytree.Node`. + + .. _level-order (breath-first): + https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) # index: 0, value: 1 + >>> root.left = Node(2) # index: 1, value: 2 + >>> root.right = Node(3) # index: 2, value: 3 + >>> + >>> root[0] = Node(4) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + OperationForbiddenError: Cannot modify the root node + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) # index: 0, value: 1 + >>> root.left = Node(2) # index: 1, value: 2 + >>> root.right = Node(3) # index: 2, value: 3 + >>> + >>> root[11] = Node(4) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + NodeNotFoundError: Parent node missing at index 5 + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) # index: 0, value: 1 + >>> root.left = Node(2) # index: 1, value: 2 + >>> root.right = Node(3) # index: 2, value: 3 + >>> + >>> root[1] = Node(4) + >>> + >>> root.left + Node(4) + """ + if index == 0: + raise OperationForbiddenError('Cannot modify the root node') + + parent_index = (index - 1) // 2 + try: + parent = self.__getitem__(parent_index) + except NodeNotFoundError: + raise NodeNotFoundError( + 'Parent node missing at index {}'.format(parent_index)) + setattr(parent, 'left' if index % 2 else 'right', node) + + def __delitem__(self, index): + """Remove the node/subtree at the given `level-order (breath-first)`_ + index from the binary tree. + + * An exception is raised if the target node does not exist. + * The descendants of the target node (if any) are also removed. + * The root node (calling node) cannot be deleted. + + :param index: The node index. + :type index: int + :raise binarytree.exceptions.OperationForbiddenError: + If the user tries to delete the root node (calling node). + :raise binarytree.exceptions.NodeNotFoundError: + If the target node or its parent does not exist. + + .. _level-order (breath-first): + https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) # index: 0, value: 1 + >>> root.left = Node(2) # index: 1, value: 2 + >>> root.right = Node(3) # index: 2, value: 3 + >>> + >>> del root[0] # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + OperationForbiddenError: Cannot delete the root node + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) # index: 0, value: 1 + >>> root.left = Node(2) # index: 1, value: 2 + >>> root.right = Node(3) # index: 2, value: 3 + >>> + >>> del root[2] + >>> + >>> root[2] # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + NodeNotFoundError: Node missing at index 2 + """ + if index == 0: + raise OperationForbiddenError('Cannot delete the root node') + + parent_index = (index - 1) // 2 + try: + parent = self.__getitem__(parent_index) + except NodeNotFoundError: + raise NodeNotFoundError( + 'No node to delete at index {}'.format(index)) + + child_attr = 'left' if index % 2 == 1 else 'right' + if getattr(parent, child_attr) is None: + raise NodeNotFoundError( + 'No node to delete at index {}'.format(index)) + setattr(parent, child_attr, None) + + def pprint(self, index=False, delimiter='-'): + """Pretty-print the binary tree. + + :param index: If set to True (default: False), display the + `level-order (breath-first)`_ indexes using the following + format: "{index}{delimiter}{value}". + :type index: bool + :param delimiter: The delimiter character between the node index, and + the node value (default: "-"). + :type delimiter: str | unicode + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) # index: 0, value: 1 + >>> root.left = Node(2) # index: 1, value: 2 + >>> root.right = Node(3) # index: 2, value: 3 + >>> root.left.right = Node(4) # index: 4, value: 4 + >>> + >>> root.pprint() + + __1 + / \\ + 2 3 + \\ + 4 + + >>> root.pprint(index=True) # Format: {index}-{value} + + _____0-1_ + / \\ + 1-2_ 2-3 + \\ + 4-4 + + + .. note:: + If you don't need to see the node indexes, you can use + :func:`binarytree.Node.__str__`. + """ + lines = _build_tree_string(self, 0, index, delimiter)[0] + print('\n' + '\n'.join((line.rstrip() for line in lines))) + + def validate(self): + """Check if the binary tree is malformed. + + :raise binarytree.exceptions.CyclicNodeReferenceError: + If there is a cyclic reference to a node in the binary tree. + :raise binarytree.exceptions.InvalidNodeTypeError: + If a node is not an instance of :class:`binarytree.Node`. + :raise binarytree.exceptions.InvalidNodeValueError: + If a node value is not an integer. + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = root # Cyclic reference to root + >>> + >>> root.validate() # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + CyclicNodeReferenceError: Cyclic node reference at index 0 + """ + has_more_nodes = True + nodes_visited = set() + current_nodes = [self] + current_index = 0 - raise ValueError('Cannot find node with ID {}'.format(node_id)) + while has_more_nodes: + has_more_nodes = False + next_nodes = [] + for node in current_nodes: + if node is None: + next_nodes.extend((None, None)) + else: + if node in nodes_visited: + raise CyclicNodeReferenceError( + 'Cyclic node reference at index {}' + .format(current_index) + ) + if not isinstance(node, Node): + raise InvalidNodeTypeError( + 'Invalid node instance at index {}' + .format(current_index) + ) + if not isinstance(node.value, int): + raise InvalidNodeValueError( + 'Invalid node value at index {}' + .format(current_index) + ) + if node.left is not None or node.right is not None: + has_more_nodes = True + nodes_visited.add(node) + next_nodes.extend((node.left, node.right)) + current_index += 1 + + current_nodes = next_nodes + + @property + def leaves(self): + """Return the leaves of the binary tree. + + :return: The list of leaf nodes. + :rtype: [binarytree.Node] + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.right = Node(4) + >>> + >>> print(root) + + __1 + / \\ + 2 3 + \\ + 4 + + >>> root.leaves + [Node(3), Node(4)] + """ + current_nodes = [self] + leaves = [] + + while len(current_nodes) > 0: + next_nodes = [] + for node in current_nodes: + if node.left is None and node.right is None: + leaves.append(node) + continue + if node.left is not None: + next_nodes.append(node.left) + if node.right is not None: + next_nodes.append(node.right) + current_nodes = next_nodes + return leaves + + @property + def levels(self): + """Return the nodes in the binary tree level by level. + + :return: The per-level lists of nodes. + :rtype: [[binarytree.Node]] + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.right = Node(4) + >>> + >>> print(root) + + __1 + / \\ + 2 3 + \\ + 4 + + >>> + >>> root.levels + [[Node(1)], [Node(2), Node(3)], [Node(4)]] + """ + current_nodes = [self] + levels = [] + + while len(current_nodes) > 0: + next_nodes = [] + for node in current_nodes: + if node.left is not None: + next_nodes.append(node.left) + if node.right is not None: + next_nodes.append(node.right) + levels.append(current_nodes) + current_nodes = next_nodes + return levels + + @property + def height(self): + """Return the height of the binary tree. + + :return: The height of the binary tree. + :rtype: int + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.left.left = Node(3) + >>> + >>> print(root) + + 1 + / + 2 + / + 3 + + >>> root.height + 2 + + .. note:: + + A binary tree with only a root node has a height of 0. + """ + return _get_tree_properties(self)['height'] + + @property + def size(self): + """Return the total number of nodes in the binary tree. + + :return: The total number of nodes. + :rtype: int + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.right = Node(4) + >>> + >>> root.size + 4 + + .. note:: + + This method is equivalent to :func:`binarytree.Node.__len__`. + """ + return _get_tree_properties(self)['size'] + + @property + def leaf_count(self): + """Return the total number of leaves in the binary tree. + + :return: The total number of leaves. + :rtype: int + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.right = Node(4) + >>> + >>> root.leaf_count + 2 + """ + return _get_tree_properties(self)['leaf_count'] + + @property + def is_balanced(self): + """Return True if the binary tree is height-balanced, False otherwise. + + :return: True if the binary tree is balanced, False otherwise. + :rtype: bool + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.left.left = Node(3) + >>> + >>> print(root) + + 1 + / + 2 + / + 3 + + >>> root.is_balanced + False + """ + return _is_balanced(self) >= 0 + + @property + def is_bst(self): + """Return True if the binary tree is a BST (binary search tree), + False otherwise. + + :return: True if the binary tree is a BST, False otherwise. + :rtype: bool + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(2) + >>> root.left = Node(1) + >>> root.right = Node(3) + >>> + >>> print(root) + + 2 + / \\ + 1 3 + + >>> root.is_bst + True + """ + return _is_bst(self, float('-inf'), float('inf')) + + @property + def is_max_heap(self): + """Return True if the binary tree is a max heap, False otherwise. + + :return: True if the binary tree is a max heap, False otherwise. + :rtype: bool + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(3) + >>> root.left = Node(1) + >>> root.right = Node(2) + >>> + >>> print(root) + + 3 + / \\ + 1 2 + + >>> root.is_max_heap + True + """ + return _get_tree_properties(self)['is_max_heap'] + + @property + def is_min_heap(self): + """Return True if the binary tree is a min heap, False otherwise. + + :return: True if the binary tree is a min heap, False otherwise. + :rtype: bool + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> + >>> print(root) + + 1 + / \\ + 2 3 + + >>> root.is_min_heap + True + """ + return _get_tree_properties(self)['is_min_heap'] + + @property + def is_perfect(self): + """Return True if the binary tree is perfect (i.e. all levels are + completely filled), False otherwise. + + :return: True if the binary tree is perfect, False otherwise. + :rtype: bool + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.left = Node(4) + >>> root.left.right = Node(5) + >>> root.right.left = Node(6) + >>> root.right.right = Node(7) + >>> + >>> print(root) + + __1__ + / \\ + 2 3 + / \\ / \\ + 4 5 6 7 + + >>> root.is_perfect + True + """ + return _get_tree_properties(self)['is_perfect'] + + @property + def is_strict(self): + """Return True if the binary tree is strict (i.e. all non-leaf nodes + have both children), False otherwise. + + :return: True if the binary tree is strict, False otherwise. + :rtype: bool + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.left = Node(4) + >>> root.left.right = Node(5) + >>> + >>> print(root) + + __1 + / \\ + 2 3 + / \\ + 4 5 + + >>> root.is_strict + True + + .. note:: + + Strictly binary nodes are also called **full** nodes. + """ + return _get_tree_properties(self)['is_strict'] + + @property + def is_complete(self): + """Return True if the binary tree is complete (i.e. all levels except + possibly the last are completely filled, and the last level is always + left-justified), False otherwise. + + :return: True if the binary tree is complete, False otherwise. + :rtype: bool + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.left = Node(4) + >>> root.left.right = Node(5) + >>> + >>> print(root) + + __1 + / \\ + 2 3 + / \\ + 4 5 + + >>> root.is_complete + True + """ + return _get_tree_properties(self)['is_complete'] + + @property + def min_node_value(self): + """Return the minimum node value in the binary tree. + + :return: The minimum node value. + :rtype: int + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> + >>> root.min_node_value + 1 + """ + return _get_tree_properties(self)['min_node_value'] + + @property + def max_node_value(self): + """Return the maximum node value in the binary tree. + + :return: The maximum node value. + :rtype: int + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> + >>> root.max_node_value + 3 + """ + return _get_tree_properties(self)['max_node_value'] + + @property + def max_leaf_depth(self): + """Return the maximum leaf node depth in the binary tree. + + :return: The maximum leaf node depth. + :rtype: int + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.right.left = Node(4) + >>> root.right.left.left = Node(5) + >>> + >>> print(root) + + 1____ + / \\ + 2 3 + / + 4 + / + 5 + + >>> root.max_leaf_depth + 3 + """ + return _get_tree_properties(self)['max_leaf_depth'] + + @property + def min_leaf_depth(self): + """Return the minimum leaf node depth in the binary tree. + + :return: The minimum leaf node depth. + :rtype: int + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.right.left = Node(4) + >>> root.right.left.left = Node(5) + >>> + >>> print(root) + + 1____ + / \\ + 2 3 + / + 4 + / + 5 + + >>> root.min_leaf_depth + 1 + """ + return _get_tree_properties(self)['min_leaf_depth'] + + @property + def properties(self): + """Return various properties of the the binary tree all at once. + + :return: The properties of the binary tree. + :rtype: dict + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.left = Node(4) + >>> root.left.right = Node(5) + >>> props = root.properties + >>> + >>> props['height'] # equivalent to root.height + 2 + >>> props['size'] # equivalent to root.size + 5 + >>> props['max_leaf_depth'] # equivalent to root.max_leaf_depth + 2 + >>> props['min_leaf_depth'] # equivalent to root.min_leaf_depth + 1 + >>> props['max_node_value'] # equivalent to root.max_node_value + 5 + >>> props['min_node_value'] # equivalent to root.min_node_value + 1 + >>> props['leaf_count'] # equivalent to root.leaf_count + 3 + >>> props['is_balanced'] # equivalent to root.is_balanced + True + >>> props['is_bst'] # equivalent to root.is_bst + False + >>> props['is_complete'] # equivalent to root.is_complete + True + >>> props['is_max_heap'] # equivalent to root.is_max_heap + False + >>> props['is_min_heap'] # equivalent to root.is_min_heap + True + >>> props['is_perfect'] # equivalent to root.is_perfect + False + >>> props['is_strict'] # equivalent to root.is_strict + True + """ + properties = _get_tree_properties(self) + properties.update({ + 'is_bst': _is_bst(self), + 'is_balanced': _is_balanced(self) >= 0 + }) + return properties + + @property + def inorder(self): + """Return the nodes in the binary tree using in-order_ (left, root, + right) traversal. + + .. _in-order: + https://en.wikipedia.org/wiki/Tree_traversal + + :return: The list of nodes. + :rtype: [binarytree.Node] + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.left = Node(4) + >>> root.left.right = Node(5) + >>> + >>> print(root) + + __1 + / \\ + 2 3 + / \\ + 4 5 + + >>> root.inorder + [Node(4), Node(2), Node(5), Node(1), Node(3)] + """ + node_stack = [] + result = [] + node = self + + while True: + if node is not None: + node_stack.append(node) + node = node.left + elif len(node_stack) > 0: + node = node_stack.pop() + result.append(node) + node = node.right + else: + break -def prune(bt, node_id): - """Delete the node and all of its children from the binary tree. + return result + + @property + def preorder(self): + """Return the nodes in the binary tree using pre-order_ (root, left, + right) traversal. + + .. _pre-order: + https://en.wikipedia.org/wiki/Tree_traversal + + :return: The list of nodes. + :rtype: [binarytree.Node] + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.left = Node(4) + >>> root.left.right = Node(5) + >>> + >>> print(root) + + __1 + / \\ + 2 3 + / \\ + 4 5 + + >>> root.preorder + [Node(1), Node(2), Node(4), Node(5), Node(3)] + """ + node_values = [] + node_stack = [self] + + while len(node_stack) > 0: + node = node_stack.pop() + node_values.append(node) + + if node.right is not None: + node_stack.append(node.right) + if node.left is not None: + node_stack.append(node.left) + + return node_values + + @property + def postorder(self): + """Return the nodes in the binary tree using post-order_ (left, right, + root) traversal. + + .. _post-order: + https://en.wikipedia.org/wiki/Tree_traversal + + :return: The list of nodes. + :rtype: [binarytree.Node] + + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.left = Node(4) + >>> root.left.right = Node(5) + >>> + >>> print(root) + + __1 + / \\ + 2 3 + / \\ + 4 5 + + >>> root.postorder + [Node(4), Node(5), Node(2), Node(3), Node(1)] + """ + node_values = [] + node_stack = [] + node = self + + while True: + while node is not None: + if node.right is not None: + node_stack.append(node.right) + node_stack.append(node) + node = node.left + + node = node_stack.pop() + if (node.right is not None and + len(node_stack) > 0 and + node_stack[-1] is node.right): + node_stack.pop() + node_stack.append(node) + node = node.right + else: + node_values.append(node) + node = None - If the binary tree is given as a list, it is automatically converted - into a tree form first. + if len(node_stack) == 0: + break - :param bt: The binary tree. - :type bt: list | binarytree.Node - :param node_id: The level-order ID of the node. - :type node_id: int - :return: The root node of the binary tree with the node pruned. + return node_values + + @property + def levelorder(self): + """Return the nodes in the binary tree using + `level-order (breath-first)`_ traversal. + + .. _level-order (breath-first): + https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search + + :return: The list of nodes. + :rtype: [binarytree.Node] + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.left = Node(4) + >>> root.left.right = Node(5) + >>> + >>> print(root) + + __1 + / \\ + 2 3 + / \\ + 4 5 + + >>> root.levelorder + [Node(1), Node(2), Node(3), Node(4), Node(5)] + """ + current_nodes = [self] + node_values = [] + + while len(current_nodes) > 0: + next_nodes = [] + for node in current_nodes: + node_values.append(node) + if node.left is not None: + next_nodes.append(node.left) + if node.right is not None: + next_nodes.append(node.right) + current_nodes = next_nodes + + return node_values + + +def build(values): + """Build a binary tree from a `list representation`_ (i.e. a list of + node values and/or None's in breath-first order) and return its root. + + :param values: The list representation (i.e. a list of node values and/or + None's in breath-first order). If a node has an index i, its left child + is at index 2i + 1, right child at index 2i + 2, and parent at index + floor((i - 1) / 2). None signifies the absence of a node. See example + below for an illustration. + :type values: [int | None] + :return: The root of the binary tree. :rtype: binarytree.Node - :raises ValueError: If an invalid binary tree or node ID is given. + :raise binarytree.exceptions.NodeNotFoundError: + If the list representation is malformed and a parent node is missing. + + .. _list representation: + https://en.wikipedia.org/wiki/Binary_tree#Arrays + + **Example**: + + .. doctest:: + + >>> from binarytree import build + >>> + >>> root = build([1, 2, 3, None, 4]) + >>> + >>> print(root) + + __1 + / \\ + 2 3 + \\ + 4 + + + .. doctest:: + + >>> from binarytree import build + >>> + >>> root = build([None, 2, 3]) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + NodeNotFoundError: Parent node missing at index 0 """ - bt = _prepare_tree(bt) - if node_id == 0: - raise ValueError('Cannot prune the root node') - _validate_id(node_id) - - current_parents = {} - current_nodes = [bt] - current_id = 0 - - while current_nodes: - next_nodes = [] - next_parents = {} - - for node in current_nodes: - if current_id == node_id: - parent = current_parents[node] - if _left_of(parent) == node: - _prune_left(parent) - else: - _prune_right(parent) - return bt + nodes = [None if v is None else Node(v) for v in values] + + for index in range(1, len(nodes)): + node = nodes[index] + if node is not None: + parent_index = (index - 1) // 2 + parent = nodes[parent_index] + if parent is None: + raise NodeNotFoundError( + 'Parent node missing at index {}' + .format(parent_index) + ) + setattr(parent, 'left' if index % 2 else 'right', node) - left_child = _left_of(node) - right_child = _right_of(node) + return nodes[0] if nodes else None - if left_child != _null: - next_nodes.append(left_child) - next_parents[left_child] = node - if right_child != _null: - next_nodes.append(right_child) - next_parents[right_child] = node - current_id += 1 - current_nodes = next_nodes - current_parents = next_parents +def tree(height=3, is_perfect=False): + """Generate a random binary tree and return its root node. - raise ValueError('Cannot find node with ID {}'.format(node_id)) + :param height: The height of the tree (default: 3, range: 0 - 9 inclusive). + :type height: int + :param is_perfect: If set to True (default: False), a perfect binary tree + with all levels filled is returned. When set to False, a perfect binary + tree may still be generated and returned by chance. + :type is_perfect: bool + :return: The root node of the generated tree. + :rtype: binarytree.Node + :raise binarytree.exceptions.InvalidTreeHeightError: + If an invalid tree height is given. + + **Example**: + + .. doctest:: + + >>> from binarytree import tree + >>> + >>> root = tree() + >>> + >>> root.height + 3 + + .. doctest:: + + >>> from binarytree import tree + >>> + >>> root = tree(height=5, is_perfect=True) + >>> + >>> root.height + 5 + >>> root.is_perfect + True + + .. doctest:: + + >>> from binarytree import tree + >>> + >>> root = tree(height=20) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidTreeHeightError: The height must be an integer between 0 - 9 + """ + _validate_tree_height(height) + values = _generate_random_node_values(height) + if is_perfect: + return build(values) + + leaf_count = _generate_random_leaf_count(height) + root = Node(values.pop(0)) + leaves = set() + + for value in values: + node = root + depth = 0 + inserted = False + + while depth < height and not inserted: + attr = random.choice(('left', 'right')) + if getattr(node, attr) is None: + setattr(node, attr, Node(value)) + inserted = True + node = getattr(node, attr) + depth += 1 + + if inserted and depth == height: + leaves.add(node) + if len(leaves) == leaf_count: + break + return root -def leafs(bt, values_only=False): - """Return the leaf nodes of the binary tree. - If the binary tree is given as a list, it is automatically converted - into a tree form first. +def bst(height=3, is_perfect=False): + """Generate a random BST (binary search tree) and return its root node. - :param bt: The binary tree. - :type bt: list | binarytree.Node - :param values_only: Return the node values only rather than the nodes. - :type values_only: bool - :return: The list of leaf nodes. - :rtype: [binarytree.Node] | [int] - :raises ValueError: If an invalid binary tree is given. + :param height: The height of the BST (default: 3, range: 0 - 9 inclusive). + :type height: int + :param is_perfect: If set to True (default: False), a perfect BST with all + levels filled is returned. When set to False, a perfect BST may still + be generated and returned by chance. + :type is_perfect: bool + :return: The root node of the generated BST. + :rtype: binarytree.Node + :raise binarytree.exceptions.InvalidTreeHeightError: + If an invalid tree height is given. + + **Example**: + + .. doctest:: + + >>> from binarytree import bst + >>> + >>> root = bst() + >>> + >>> root.height + 3 + >>> root.is_bst + True + + .. doctest:: + + >>> from binarytree import bst + >>> + >>> root = bst(10) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidTreeHeightError: The height must be an integer between 0 - 9 """ - bt = _prepare_tree(bt) + _validate_tree_height(height) + if is_perfect: + return _generate_perfect_bst(height) + + values = _generate_random_node_values(height) + leaf_count = _generate_random_leaf_count(height) + + root = Node(values.pop(0)) + leaves = set() + + for value in values: + node = root + depth = 0 + inserted = False + + while depth < height and not inserted: + attr = 'left' if node.value > value else 'right' + if getattr(node, attr) is None: + setattr(node, attr, Node(value)) + inserted = True + node = getattr(node, attr) + depth += 1 + + if inserted and depth == height: + leaves.add(node) + if len(leaves) == leaf_count: + break - current_nodes = [bt] - leaf_nodes = [] + return root - while current_nodes: - next_nodes = [] - for node in current_nodes: - left_child = _left_of(node) - right_child = _right_of(node) +def heap(height=3, is_max=True, is_perfect=False): + """Generate a heap and return its root node. - if left_child == _null and right_child == _null: - leaf_nodes.append(node) - if left_child != _null: - next_nodes.append(left_child) - if right_child != _null: - next_nodes.append(right_child) + :param height: The height of the heap (default: 3, range: 0 - 9 inclusive). + :type height: int + :param is_max: If set to True (default: True), generate a max heap. + Otherwise, generate a min heap. Note that a binary tree with only the + root is both a min and max heap. + :type is_max: bool + :param is_perfect: If set to True (default: False), a perfect heap with all + levels filled is returned. When set to False, a perfect heap may still + be generated and returned by chance. + :type is_perfect: bool + :return: The root node of the generated heap. + :rtype: binarytree.Node + :raise binarytree.exceptions.InvalidTreeHeightError: + If an invalid tree height is given. + + **Example**: + + .. doctest:: + + >>> from binarytree import heap + >>> + >>> root = heap() + >>> + >>> root.height + 3 + >>> root.is_max_heap + True + + .. doctest:: + + >>> from binarytree import heap + >>> + >>> root = heap(4, is_max=False) + >>> + >>> root.height + 4 + >>> root.is_min_heap + True + + .. doctest:: + + >>> from binarytree import heap + >>> + >>> root = heap(5, is_max=False, is_perfect=True) + >>> + >>> root.height + 5 + >>> root.is_min_heap + True + >>> root.is_perfect + True + + .. doctest:: + + >>> from binarytree import heap + >>> + >>> root = heap(-1) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidTreeHeightError: The height must be an integer between 0 - 9 + """ + _validate_tree_height(height) + values = _generate_random_node_values(height) - current_nodes = next_nodes + if not is_perfect: + # Randomly cut some of the leaf nodes away + random_cut = random.randint(2 ** height, len(values)) + values = values[:random_cut] - return [_value_of(n) for n in leaf_nodes] if values_only else leaf_nodes + if is_max: + negated = [-v for v in values] + heapq.heapify(negated) + return build([-v for v in negated]) + else: + heapq.heapify(values) + return build(values) diff --git a/binarytree/exceptions.py b/binarytree/exceptions.py new file mode 100644 index 0000000..6f81cf3 --- /dev/null +++ b/binarytree/exceptions.py @@ -0,0 +1,33 @@ +from __future__ import absolute_import, unicode_literals + + +class BinaryTreeError(Exception): + """Base exception.""" + + +class InvalidNodeValueError(BinaryTreeError): + """Raised if a node has an invalid value.""" + + +class InvalidNodeIndexError(BinaryTreeError): + """Raised if an invalid level-order index is given.""" + + +class InvalidNodeTypeError(BinaryTreeError): + """Raised if a node is not an instance of :class:`binarytree.Node`.""" + + +class OperationForbiddenError(BinaryTreeError): + """Raised if the user tries to overwrite or delete the root node.""" + + +class NodeNotFoundError(BinaryTreeError): + """Raised if a node is missing from the binary tree.""" + + +class InvalidTreeHeightError(BinaryTreeError): + """Raised if an invalid tree height is given.""" + + +class CyclicNodeReferenceError(BinaryTreeError): + """Raised if the binary tree has a cyclic reference to a node.""" diff --git a/demo.gif b/demo.gif index a05cbec..a4871db 100644 Binary files a/demo.gif and b/demo.gif differ diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..6a19874 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = python -msphinx +SPHINXPROJ = binarytree +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..f95525e --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,43 @@ +API Specification +----------------- + +This page contains the API specification for the +:ref:`binarytree.Node ` class, and utility functions +:ref:`binarytree.build `, +:ref:`binarytree.tree `, +:ref:`binarytree.bst ` and +:ref:`binarytree.heap `. + + +Class: binarytree.Node +====================== + +.. autoclass:: binarytree.Node + :members: + :special-members: + :private-members: + :exclude-members: __weakref__, __repr__, __setattr__ + + +Function: binarytree.build +========================== + +.. autofunction:: binarytree.build + + +Function: binarytree.tree +========================= + +.. autofunction:: binarytree.tree + + +Function: binarytree.bst +======================== + +.. autofunction:: binarytree.bst + + +Function: binarytree.heap +========================= + +.. autofunction:: binarytree.heap diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..34c1fba --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# binarytree documentation build configuration file, created by +# sphinx-quickstart on Thu Nov 9 02:56:43 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +from binarytree import __version__ + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.autosectionlabel'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = [] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'binarytree' +copyright = '2016, Joohwan Oh' +author = 'Joohwan Oh' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = __version__ +# The full version, including alpha/beta/rc tags. +release = __version__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = [] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# This is required for the alabaster theme +# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars +html_sidebars = { + '**': [ + 'about.html', + 'navigation.html', + 'relations.html', # needs 'show_related': True theme option to display + 'searchbox.html', + 'donate.html', + ] +} + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'binarytreedoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'binarytree.tex', 'binarytree Documentation', + 'Joohwan Oh', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'binarytree', 'binarytree Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'binarytree', 'binarytree Documentation', + author, 'binarytree', 'One line description of project.', + 'Miscellaneous'), +] + +autodoc_member_order = 'bysource' diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000..97c98df --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1,112 @@ +Contributing +------------ + +Instructions +============ + +Before submitting a pull request on GitHub_, please make sure you meet the +following requirements: + +* The pull request points to the dev_ (development) branch. +* All changes are squashed into a single commit (I like to use + ``git rebase -i`` to do this). +* The commit message is in present tense (ok: "Add feature", not ok: + "Added feature"). +* Correct and consistent style: Sphinx_-compatible docstrings, using snake + vs. camel casing properly_ and PEP8_ compliance (see below). +* No classes/methods/functions with missing docstrings or commented-out lines. + You can refer to existing ones for examples. +* The test coverage_ remains at %100. Sometimes you may find yourself having to + write superfluous unit tests to keep this number up. If a piece of code is + trivial and has no need for unittests, use this_ to exclude it from coverage. +* No build failures on TravisCI_. The builds automatically trigger on PR + submissions. +* Does not break backward-compatibility (unless there is a really good reason). +* Compatible with Python versions 2.7, 3.4, 3.5 and 3.6. + +.. warning:: + The dev branch is occasionally rebased_, and its commit history may be + overwritten in the process (I try very hard never to do this). So before + you begin your feature work, git fetch/pull to ensure that branches have + not diverged. If you see git conflicts and just want to start from scratch, + run this command: + + .. code-block:: bash + + ~$ git checkout dev + ~$ git fetch origin + ~$ git reset --hard origin/dev # THIS WILL WIPE AL LOCAL CHANGES + +Style +===== + +To ensure PEP8_ compliance, run flake8_: + +.. code-block:: bash + + ~$ pip install flake8 + ~$ git clone https://github.com/joowani/binarytree.git + ~$ cd binarytree + ~$ flake8 + +You should try to resolve all issues reported. If there is a good reason to +ignore errors from a specific piece of code, visit here_ to see how to exclude +the lines from the check. + +Testing +======= + +To test your changes, run the unit tests that come with **binarytree** on your +local machine. The tests use pytest_. + +To run the unit tests: + +.. code-block:: bash + + ~$ pip install pytest + ~$ git clone https://github.com/joowani/binarytree.git + ~$ cd binarytree + ~$ py.test tests.py --verbose + +To run the unit tests with coverage report: + +.. code-block:: bash + + ~$ pip install coverage pytest pytest-cov + ~$ git clone https://github.com/joowani/binarytree.git + ~$ cd binarytree + ~$ py.test tests.py --verbose --cov-report=html --cov=binarytree + ~$ # Open the generated file htmlcov/index.html in a browser + + +Documentation +============= + +The documentation (including the README) is written in reStructuredText_ and +uses Sphinx_. To build the HTML version of the documentation on your local +machine: + +.. code-block:: bash + + ~$ pip install sphinx sphinx_rtd_theme + ~$ git clone https://github.com/joowani/binarytree.git + ~$ cd binarytree/docs + ~$ sphinx-build . build + ~$ # Open the generated file build/index.html in a browser + + +As always, thanks for your contribution! + +.. _rebased: https://git-scm.com/book/en/v2/Git-Branching-Rebasing +.. _dev: https://github.com/joowani/binarytree/tree/dev +.. _GitHub: https://github.com/joowani/binarytree +.. _properly: https://stackoverflow.com/questions/159720 +.. _PEP8: https://www.python.org/dev/peps/pep-0008/ +.. _coverage: https://coveralls.io/github/joowani/binarytree +.. _this: http://coverage.readthedocs.io/en/latest/excluding.html +.. _TravisCI: https://travis-ci.org/joowani/binarytree +.. _Sphinx: https://github.com/sphinx-doc/sphinx +.. _flake8: http://flake8.pycqa.org +.. _here: http://flake8.pycqa.org/en/latest/user/violations.html#in-line-ignoring-errors +.. _pytest: https://github.com/pytest-dev/pytest +.. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText diff --git a/docs/exceptions.rst b/docs/exceptions.rst new file mode 100644 index 0000000..e02584d --- /dev/null +++ b/docs/exceptions.rst @@ -0,0 +1,7 @@ +Exceptions +---------- + +Below is the list of exceptions raised by **binarytree**: + +.. automodule:: binarytree.exceptions + :members: \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..38600fc --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,51 @@ +Binarytree +---------- + +Welcome to the documentation for **binarytree**! + +**Binarytree** is a Python library which provides a simple API to generate, +visualize, inspect and manipulate binary trees. It allows you to skip the +tedious work of setting up test data, and dive straight into practising your +algorithms! Heaps and BSTs (binary search trees) are also supported. + +Requirements +============ + +- Python 2.7, 3.4, 3.5 or 3.6 +- Pip_ installer + +.. _Pip: https://pip.pypa.io + + +Installation +============ + +To install a stable version from PyPi_: + +.. code-block:: bash + + ~$ pip install binarytree + + +To install the latest version directly from GitHub_: + +.. code-block:: bash + + ~$ pip install -e git+git@github.com:joowani/binarytree.git@master#egg=binarytree + +You may need to use ``sudo`` depending on your environment. + +.. _PyPi: https://pypi.python.org/pypi/binarytree +.. _GitHub: https://github.com/joowani/binarytree + + +Contents +======== + +.. toctree:: + :maxdepth: 1 + + overview + api + exceptions + contributing diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..cd90edd --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=python -msphinx +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=binarytree + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The Sphinx module was not found. Make sure you have Sphinx installed, + echo.then set the SPHINXBUILD environment variable to point to the full + echo.path of the 'sphinx-build' executable. Alternatively you may add the + echo.Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/overview.rst b/docs/overview.rst new file mode 100644 index 0000000..d3a0e68 --- /dev/null +++ b/docs/overview.rst @@ -0,0 +1,276 @@ +Overview +-------- + +By default, **binarytree** uses the following class to represent a node: + +.. code-block:: python + + class Node(object): + + def __init__(self, value, left=None, right=None): + self.value = value # The node value + self.left = left # Left child + self.right = right # Right child + + +Generate and pretty-print various types of binary trees: + +.. code-block:: python + + >>> from binarytree import tree, bst, heap + >>> + >>> # Generate a random binary tree and return its root node + >>> my_tree = tree(height=3, is_perfect=False) + >>> + >>> # Generate a random BST and return its root node + >>> my_bst = bst(height=3, is_perfect=True) + >>> + >>> # Generate a random max heap and return its root node + >>> my_heap = heap(height=3, is_max=True, is_perfect=False) + >>> + >>> # Pretty-print the trees in stdout + >>> print(my_tree) + + _______1_____ + / \ + 4__ ___3 + / \ / \ + 0 9 13 14 + / \ \ + 7 10 2 + + >>> print(my_bst) + + ______7_______ + / \ + __3__ ___11___ + / \ / \ + 1 5 9 _13 + / \ / \ / \ / \ + 0 2 4 6 8 10 12 14 + + >>> print(my_heap) + + _____14__ + / \ + ____13__ 9 + / \ / \ + 12 7 3 8 + / \ / + 0 10 6 + + +Use the :ref:`binarytree.Node ` class to build your +own trees: + +.. code-block:: python + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.right = Node(4) + >>> + >>> print(root) + + __1 + / \ + 2 3 + \ + 4 + + +Inspect tree properties: + +.. code-block:: python + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.left = Node(4) + >>> root.left.right = Node(5) + >>> + >>> print(root) + + __1 + / \ + 2 3 + / \ + 4 5 + + >>> root.height + 2 + >>> root.is_balanced + True + >>> root.is_bst + False + >>> root.is_complete + True + >>> root.is_max_heap + False + >>> root.is_min_heap + True + >>> root.is_perfect + False + >>> root.is_strict + True + >>> root.leaf_count + 3 + >>> root.max_leaf_depth + 2 + >>> root.max_node_value + 5 + >>> root.min_leaf_depth + 1 + >>> root.min_node_value + 1 + >>> root.size + 5 + + >>> root.properties + {'height': 2, + 'is_balanced': True, + 'is_bst': False, + 'is_complete': True, + 'is_max_heap': False, + 'is_min_heap': True, + 'is_perfect': False, + 'is_strict': True, + 'leaf_count': 3, + 'max_leaf_depth': 2, + 'max_node_value': 5, + 'min_leaf_depth': 1, + 'min_node_value': 1, + 'size': 5} + + >>> root.leaves + [Node(3), Node(4), Node(5)] + + >>> root.levels + [[Node(1)], [Node(2), Node(3)], [Node(4), Node(5)]] + +Use `level-order (breath-first)`_ indexes to manipulate nodes: + +.. _level-order (breath-first): + https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search + +.. code-block:: python + + >>> from binarytree import Node + >>> + >>> root = Node(1) # index: 0, value: 1 + >>> root.left = Node(2) # index: 1, value: 2 + >>> root.right = Node(3) # index: 2, value: 3 + >>> root.left.right = Node(4) # index: 4, value: 4 + >>> root.left.right.left = Node(5) # index: 9, value: 5 + >>> + >>> print(root) + + ____1 + / \ + 2__ 3 + \ + 4 + / + 5 + + >>> # Use binarytree.Node.pprint instead of print to display indexes + >>> root.pprint(index=True) + + _________0-1_ + / \ + 1-2_____ 2-3 + \ + _4-4 + / + 9-5 + + >>> # Return the node/subtree at index 9 + >>> root[9] + Node(5) + + >>> # Replace the node/subtree at index 4 + >>> root[4] = Node(6, left=Node(7), right=Node(8)) + >>> root.pprint(index=True) + + ______________0-1_ + / \ + 1-2_____ 2-3 + \ + _4-6_ + / \ + 9-7 10-8 + + >>> # Delete the node/subtree at index 1 + >>> del root[1] + >>> root.pprint(index=True) + + 0-1_ + \ + 2-3 + + +Traverse the trees using different algorithms: + +.. code-block:: python + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.left = Node(4) + >>> root.left.right = Node(5) + >>> + >>> print(root) + + __1 + / \ + 2 3 + / \ + 4 5 + + >>> root.inorder + [Node(4), Node(2), Node(5), Node(1), Node(3)] + + >>> root.preorder + [Node(1), Node(2), Node(4), Node(5), Node(3)] + + >>> root.postorder + [Node(4), Node(5), Node(2), Node(3), Node(1)] + + >>> root.levelorder + [Node(1), Node(2), Node(3), Node(4), Node(5)] + + +`List representations`_ are also supported: + +.. _List representations: + https://en.wikipedia.org/wiki/Binary_tree#Arrays + + +.. code-block:: python + + >>> from binarytree import build + >>> + >>> # Build a tree from list representation + >>> root = build([7, 3, 2, 6, 9, None, 1, 5, 8]) + >>> print(root) + + __7 + / \ + __3 2 + / \ \ + 6 9 1 + / \ + 5 8 + + >>> # Convert the tree back to list representation + >>> list(root) + [7, 3, 2, 6, 9, None, 1, 5, 8] + + +See :ref:`API Specification` for more details. \ No newline at end of file diff --git a/setup.py b/setup.py index c597a92..dd86b84 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,18 @@ from setuptools import setup, find_packages +import binarytree + setup( name='binarytree', description='Python Library for Learning Binary Trees', - version='2.0.1', + version=binarytree.__version__, author='Joohwan Oh', author_email='joohwan.oh@outlook.com', url='https://github.com/joowani/binarytree', packages=find_packages(), include_package_data=True, - tests_require=['pytest'], + tests_require=['pytest', 'flake8'], + license='MIT', classifiers=[ 'Intended Audience :: Developers', 'Intended Audience :: End Users/Desktop', diff --git a/tests.py b/tests.py index 42c868e..754261a 100644 --- a/tests.py +++ b/tests.py @@ -1,1153 +1,832 @@ import sys +import random try: + # noinspection PyCompatibility from StringIO import StringIO except ImportError: from io import StringIO -from random import randint import pytest -from binarytree import ( - Node, - convert, - get_level, - get_levels, - inspect, - tree, - bst, - heap, - subtree, - prune, - leafs, - pprint, - show, - show_ids, - show_all, - stringify, - customize, +from binarytree import Node, build, tree, bst, heap +from binarytree.exceptions import ( + InvalidNodeValueError, + InvalidNodeIndexError, + InvalidNodeTypeError, + OperationForbiddenError, + NodeNotFoundError, + InvalidTreeHeightError, + CyclicNodeReferenceError, ) -repetitions = 100 +REPETITIONS = 20 class CaptureOutput(list): + """Context manager to catch stdout.""" + def __enter__(self): - self._orig_stdout = sys.stdout + self._original_stdout = sys.stdout self._temp_stdout = StringIO() sys.stdout = self._temp_stdout return self def __exit__(self, *args): - self.extend(self._temp_stdout.getvalue().splitlines()) - sys.stdout = self._orig_stdout - - -def attr(instance, attributes): - result = instance - for attribute in attributes.split('.'): - result = getattr(result, attribute) - return result + lines = self._temp_stdout.getvalue().splitlines() + self.extend(line.rstrip() for line in lines) + sys.stdout = self._original_stdout @pytest.mark.order1 -def test_node(): - node = Node(1) - assert attr(node, 'left') is None - assert attr(node, 'right') is None - assert attr(node, 'value') == 1 - assert attr(node, 'parent') is None - assert repr(node) == 'Node(1)' - - node.left = Node(2) - node.right = Node(3) - assert repr(attr(node, 'left')) == 'Node(2)' - assert repr(attr(node, 'right')) == 'Node(3)' - assert attr(node.left, 'parent') == node - assert attr(node.right, 'parent') == node - - node.left.left = Node(4) - node.left.right = Node(5) - node.right.left = Node(6) - node.right.right = Node(7) - assert repr(attr(node, 'left.left')) == 'Node(4)' - assert repr(attr(node, 'left.right')) == 'Node(5)' - assert repr(attr(node, 'right.right')) == 'Node(7)' - assert repr(attr(node, 'right.left')) == 'Node(6)' - assert attr(node.left.left, 'parent') == node.left - assert attr(node.left.right, 'parent') == node.left - assert attr(node.right.left, 'parent') == node.right - assert attr(node.right.right, 'parent') == node.right - - assert node.is_root() is True - assert node.left.is_root() is False - assert node.left.right.is_leaf() is True - assert node.is_leaf() is False - - assert node.left.is_child_of(node) is True - assert node.right.is_child_of(node) is True - assert node.is_child_of(node) is False - assert node.left.is_left_child_of(node) is True - assert node.right.is_left_child_of(node) is False - assert node.right.is_right_child_of(node) is True - assert node.left.is_right_child_of(node) is False - - assert node.level() == 0 - assert node.right.level() == 1 +def test_node_set_attributes(): + root = Node(1) + assert root.left is None + assert root.right is None + assert root.value == 1 + assert repr(root) == 'Node(1)' + + left_child = Node(2) + root.left = left_child + assert root.left is left_child + assert root.right is None + assert root.value == 1 + assert root.left.left is None + assert root.left.right is None + assert root.left.value == 2 + assert repr(left_child) == 'Node(2)' + + right_child = Node(3) + root.right = right_child + assert root.left is left_child + assert root.right is right_child + assert root.value == 1 + assert root.right.left is None + assert root.right.right is None + assert root.right.value == 3 + assert repr(right_child) == 'Node(3)' + + last_node = Node(4) + left_child.right = last_node + assert root.left.right is last_node + assert root.left.right.value == 4 + assert repr(last_node) == 'Node(4)' + + with pytest.raises(InvalidNodeValueError): + # noinspection PyTypeChecker + Node('this_is_not_an_integer') + + with pytest.raises(InvalidNodeTypeError): + # noinspection PyTypeChecker + Node(1, 'this_is_not_a_node') + + with pytest.raises(InvalidNodeTypeError): + # noinspection PyTypeChecker + Node(1, Node(1), 'this_is_not_a_node') + + with pytest.raises(InvalidNodeValueError): + root.value = 'this_is_not_an_integer' + assert root.value == 1 + + with pytest.raises(InvalidNodeTypeError): + root.left = 'this_is_not_a_node' + assert root.left is left_child + + with pytest.raises(InvalidNodeTypeError): + root.right = 'this_is_not_a_node' + assert root.right is right_child @pytest.mark.order2 -def test_tree(): - for invalid_height in ['foo', -1]: - with pytest.raises(ValueError) as err: - tree(height=invalid_height) - assert str(err.value) == 'Height must be a non-negative integer' - - for _ in range(repetitions): - root = tree(height=0) - assert attr(root, 'left') is None - assert attr(root, 'right') is None - assert isinstance(attr(root, 'value'), int) - assert inspect(root)['height'] == 0 - assert inspect(root)['is_height_balanced'] is True - assert inspect(root)['is_weight_balanced'] is True - - for _ in range(repetitions): - height = randint(1, 10) - root = tree(height) - nodes_to_visit = [root] - while nodes_to_visit: - node = nodes_to_visit.pop() - assert isinstance(node, Node) - assert isinstance(attr(node, 'value'), int) - if attr(node, 'left') is not None: - nodes_to_visit.append(attr(node, 'left')) - if attr(node, 'right') is not None: - nodes_to_visit.append(attr(node, 'right')) - assert inspect(root)['height'] == height - - for _ in range(repetitions): - height = randint(1, 10) - root = tree(height, is_balanced=True) - nodes_to_visit = [root] - while nodes_to_visit: - node = nodes_to_visit.pop() - assert isinstance(node, Node) - assert isinstance(attr(node, 'value'), int) - if attr(node, 'left') is not None: - nodes_to_visit.append(attr(node, 'left')) - if attr(node, 'right') is not None: - nodes_to_visit.append(attr(node, 'right')) - assert inspect(root)['height'] == height - assert inspect(root)['is_height_balanced'] is True - assert inspect(root)['is_weight_balanced'] is True - - -def test_bst(): - for invalid_height in ['foo', -1]: - with pytest.raises(ValueError) as err: - bst(height=invalid_height) - assert str(err.value) == 'Height must be a non-negative integer' - - for _ in range(repetitions): - root = bst(height=0) - assert attr(root, 'left') is None - assert attr(root, 'right') is None - assert isinstance(attr(root, 'value'), int) - assert inspect(root)['height'] == 0 - assert inspect(root)['is_bst'] is True - - for _ in range(repetitions): - height = randint(1, 10) - root = bst(height) - nodes_to_visit = [root] - while nodes_to_visit: - node = nodes_to_visit.pop() - assert isinstance(node, Node) - assert isinstance(attr(node, 'value'), int) - if attr(node, 'left') is not None: - nodes_to_visit.append(attr(node, 'left')) - if attr(node, 'right') is not None: - nodes_to_visit.append(attr(node, 'right')) - assert inspect(root)['height'] == height - assert inspect(root)['is_bst'] is True - - -def test_heap(): - for invalid_height in ['foo', -1]: - with pytest.raises(ValueError) as err: - heap(height=invalid_height) - assert str(err.value) == 'Height must be a non-negative integer' - - # Test heap generation with height of 0 - for _ in range(repetitions): - root = heap(height=0) - assert attr(root, 'left') is None - assert attr(root, 'right') is None - assert isinstance(attr(root, 'value'), int) - assert inspect(root)['height'] == 0 - assert inspect(root)['is_min_heap'] is True - assert inspect(root)['is_max_heap'] is True - - for _ in range(repetitions): - height = randint(1, 10) - root = heap(height) - nodes_to_visit = [root] - while nodes_to_visit: - node = nodes_to_visit.pop() - assert isinstance(node, Node) - assert isinstance(attr(node, 'value'), int) - if attr(node, 'left') is not None: - nodes_to_visit.append(attr(node, 'left')) - if attr(node, 'right') is not None: - nodes_to_visit.append(attr(node, 'right')) - assert inspect(root)['height'] == height - assert inspect(root)['is_min_heap'] is True - - for _ in range(repetitions): - height = randint(1, 10) - root = heap(height, is_max=True) - nodes_to_visit = [root] - while nodes_to_visit: - node = nodes_to_visit.pop() - assert isinstance(node, Node) - assert isinstance(attr(node, 'value'), int) - if attr(node, 'left') is not None: - nodes_to_visit.append(attr(node, 'left')) - if attr(node, 'right') is not None: - nodes_to_visit.append(attr(node, 'right')) - assert inspect(root)['height'] == height - assert inspect(root)['is_max_heap'] is True - - -def test_convert(): - for invalid_argument in [1, 'foo', int]: - with pytest.raises(ValueError) as err: - convert(invalid_argument) - assert str(err.value) == 'Expecting a list or a node' - - assert convert(None) == [] - - # Convert trees to lists - for convert_func in [convert, lambda node: node.convert()]: - root = Node(1) - assert convert_func(root) == [1] - - root.right = Node(3) - assert convert_func(root) == [1, None, 3] - - root.left = Node(2) - assert convert_func(root) == [1, 2, 3] - - root.left.right = Node(4) - assert convert_func(root) == [1, 2, 3, None, 4] - - root.right.left = Node(5) - assert convert_func(root) == [1, 2, 3, None, 4, 5] - - root.right.right = Node(6) - assert convert_func(root) == [1, 2, 3, None, 4, 5, 6] - - root.right.right = Node(None) - with pytest.raises(ValueError) as err: - convert_func(root) - assert str(err.value) == 'A node cannot have a null value' - - root.right.right = {} - with pytest.raises(ValueError) as err: - assert convert_func(root) - assert str(err.value) == 'Found an invalid node in the tree' - - # Convert lists to trees - with pytest.raises(ValueError) as err: - convert([None, 2, 3]) - assert str(err.value) == 'Node missing at index 0' - - with pytest.raises(ValueError) as err: - convert([1, 2, None, 3, 4, 5, 6]) - assert str(err.value) == 'Node missing at index 2' - - assert convert([]) is None - - bt = convert([1]) - assert attr(bt, 'value') == 1 - assert attr(bt, 'left') is None - assert attr(bt, 'right') is None - - bt = convert([1, 2]) - assert attr(bt, 'value') == 1 - assert attr(bt, 'left.value') == 2 - assert attr(bt, 'right') is None - assert attr(bt, 'left.left') is None - assert attr(bt, 'left.right') is None - - bt = convert([1, None, 3]) - assert attr(bt, 'value') == 1 - assert attr(bt, 'left') is None - assert attr(bt, 'right.value') == 3 - assert attr(bt, 'right.left') is None - assert attr(bt, 'right.right') is None - - bt = convert([1, 2, 3]) - assert attr(bt, 'value') == 1 - assert attr(bt, 'left.value') == 2 - assert attr(bt, 'right.value') == 3 - assert attr(bt, 'left.left') is None - assert attr(bt, 'left.right') is None - assert attr(bt, 'right.left') is None - assert attr(bt, 'right.right') is None - - -def test_setitem(): - node = Node(1) - node.left = Node(2) - node.right = Node(3) - with pytest.raises(ValueError): - node[2] = 5 - with pytest.raises(ValueError): - node[0] = Node(1) - with pytest.raises(ValueError): - node[-1] = Node(1) - with pytest.raises(ValueError): - node[3.14] = Node(1) - node[1] = Node(11) - node[2] = Node(12) - node[3] = Node(13) - node[4] = Node(15) - with pytest.raises(IndexError): - node[12] = Node(17) - - -def test_getitem(): - node = Node(1) - node.left = Node(2) - node.right = Node(3) - assert node[0] == node - assert node[1] == node.left - assert node[2] == node.right - with pytest.raises(ValueError): - node[-1] - with pytest.raises(IndexError): - node[3] - with pytest.raises(ValueError): - node[3.14] - with pytest.raises(ValueError): - node[None] - - -def test_get_levels(): - for invalid_argument in [None, 1, 'foo']: - with pytest.raises(ValueError) as err: - get_levels(invalid_argument) - assert str(err.value) == 'Expecting a list or a node' - assert str(get_levels(convert([0, 1, 2]))) == '[[Node(0)], [Node(1), Node(2)]]' - assert str(get_levels(convert([0, 1, 2]), show_values=True)) == '[[0], [1, 2]]' - assert str(get_levels(convert([0, 1]), show_values=True, show_nulls=True))\ - == '[[0], [1, None]]' - - -def test_get_level(): - for invalid_argument in [None, 1, 'foo']: - with pytest.raises(ValueError) as err: - get_level(invalid_argument, 0) - assert str(err.value) == 'Expecting a list or a node' - for invalid_argument in [None, -1, 'foo']: - with pytest.raises(ValueError) as err: - get_level(convert([0, 1, 2]), invalid_argument) - assert str(err.value) == 'Requested level must be a non-negative integer.' - with pytest.raises(ValueError) as err: - get_level(convert([0, 1, 2]), 2) - assert str(err.value) == 'Requested level not present in tree.' - - assert str(get_level(convert([0, 1, 2]), 1)) == '[Node(1), Node(2)]' - assert str(get_level(convert([0, 1, 2]), 1, show_values=True)) == '[1, 2]' - assert str(get_level(convert([0, 1, 2, 3]), 2, show_values=True, show_nulls=True))\ - == '[3, None, None, None]' - - -def test_inspect(): - for invalid_argument in [None, 1, 'foo']: - with pytest.raises(ValueError) as err: - inspect(invalid_argument) - assert str(err.value) == 'Expecting a list or a node' - - def convert_inspect(target): - return inspect(convert(target)) - - def self_inspect(target): - return target.inspect() - - for inspect_func in [inspect, convert_inspect, self_inspect]: - root = Node(1) - assert inspect_func(root) == { - 'is_height_balanced': True, - 'is_weight_balanced': True, - 'is_max_heap': True, - 'is_min_heap': True, - 'is_bst': True, - 'is_full': True, - 'height': 0, - 'max_value': 1, - 'min_value': 1, - 'leaf_count': 1, - 'node_count': 1, - 'max_leaf_depth': 0, - 'min_leaf_depth': 0, - } - root.left = Node(2) - assert inspect_func(root) == { - 'is_height_balanced': True, - 'is_weight_balanced': True, - 'is_max_heap': False, - 'is_min_heap': True, - 'is_bst': False, - 'is_full': False, - 'height': 1, - 'max_value': 2, - 'min_value': 1, - 'node_count': 2, - 'leaf_count': 1, - 'max_leaf_depth': 1, - 'min_leaf_depth': 1, - } - root.right = Node(3) - assert inspect_func(root) == { - 'is_height_balanced': True, - 'is_weight_balanced': True, - 'is_max_heap': False, - 'is_min_heap': True, - 'is_bst': False, - 'is_full': True, - 'height': 1, - 'max_value': 3, - 'min_value': 1, - 'leaf_count': 2, - 'node_count': 3, - 'max_leaf_depth': 1, - 'min_leaf_depth': 1, - } - root.value = 2 - root.left.value = 1 - root.right.value = 3 - assert inspect_func(root) == { - 'is_height_balanced': True, - 'is_weight_balanced': True, - 'is_max_heap': False, - 'is_min_heap': False, - 'is_bst': True, - 'is_full': True, - 'height': 1, - 'max_value': 3, - 'min_value': 1, - 'leaf_count': 2, - 'node_count': 3, - 'max_leaf_depth': 1, - 'min_leaf_depth': 1, - } - root.value = 1 - root.left.value = 2 - root.right.value = 3 - root.left.right = Node(4) - assert inspect_func(root) == { - 'is_height_balanced': True, - 'is_weight_balanced': True, - 'is_max_heap': False, - 'is_min_heap': False, - 'is_bst': False, - 'is_full': False, - 'height': 2, - 'max_value': 4, - 'min_value': 1, - 'leaf_count': 2, - 'node_count': 4, - 'max_leaf_depth': 2, - 'min_leaf_depth': 1, - } - root.left.left = Node(5) - assert inspect_func(root) == { - 'is_height_balanced': True, - 'is_weight_balanced': True, - 'is_max_heap': False, - 'is_min_heap': True, - 'is_bst': False, - 'is_full': True, - 'height': 2, - 'max_value': 5, - 'min_value': 1, - 'leaf_count': 3, - 'node_count': 5, - 'max_leaf_depth': 2, - 'min_leaf_depth': 1, - } - root.right.right = Node(6) - assert inspect_func(root) == { - 'is_height_balanced': True, - 'is_weight_balanced': True, - 'is_max_heap': False, - 'is_min_heap': False, - 'is_bst': False, - 'is_full': False, - 'height': 2, - 'max_value': 6, - 'min_value': 1, - 'leaf_count': 3, - 'node_count': 6, - 'max_leaf_depth': 2, - 'min_leaf_depth': 2, - } - - root.right.right = Node(None) - with pytest.raises(ValueError) as err: - assert inspect_func(root) - assert str(err.value) == 'A node cannot have a null value' - - root.right.right = {} - with pytest.raises(ValueError) as err: - assert inspect_func(root) - assert str(err.value) == 'Found an invalid node in the tree' - - -def test_show(): - def convert_show(target): - show(convert(target)) - - def convert_self_show(target): - convert(target).show() - - for invalid_argument in [1, 'foo']: - with pytest.raises(ValueError) as err: - show(invalid_argument) - assert str(err.value) == 'Expecting a list or a node' - - for show_func in [pprint, show, convert_show]: - with CaptureOutput() as output: - show_func([]) - assert output == [''] - - for show_func in [pprint, show, convert_show, convert_self_show]: - with CaptureOutput() as output: - show_func([1, 2]) - assert output == ['', - ' 1', - ' / ', - '2 ', - ' ' - ] - - with CaptureOutput() as output: - show_func([1, None, 3]) - assert output == ['', - '1 ', - ' \\ ', - ' 3', - ' ' - ] - - with CaptureOutput() as output: - show_func([1, 2, 3]) - assert output == ['', - ' 1 ', - ' / \\ ', - '2 3', - ' ' - ] - +def test_tree_build(): + root = build([]) + assert root is None + + root = build([1]) + assert root.value == 1 + assert root.left is None + assert root.right is None + + root = build([1, 2]) + assert root.value == 1 + assert root.left.value == 2 + assert root.right is None + + root = build([1, 2, 3]) + assert root.value == 1 + assert root.left.value == 2 + assert root.right.value == 3 + assert root.left.left is None + assert root.left.right is None + assert root.right.left is None + assert root.right.right is None + + root = build([1, 2, 3, None, 4]) + assert root.value == 1 + assert root.left.value == 2 + assert root.right.value == 3 + assert root.left.left is None + assert root.left.right.value == 4 + assert root.right.left is None + assert root.right.right is None + assert root.left.right.left is None + assert root.left.right.right is None + + with pytest.raises(NodeNotFoundError) as err: + build([None, 1, 2]) + assert str(err.value) == 'Parent node missing at index 0' + + with pytest.raises(NodeNotFoundError) as err: + build([1, None, 2, 3, 4]) + assert str(err.value) == 'Parent node missing at index 1' + + +@pytest.mark.order3 +def test_tree_get_node(): + root = Node(1) + root.left = Node(2) + root.right = Node(3) + root.left.left = Node(4) + root.left.right = Node(5) + root.left.right.left = Node(6) + + assert root[0] is root + assert root[1] is root.left + assert root[2] is root.right + assert root[3] is root.left.left + assert root[4] is root.left.right + assert root[9] is root.left.right.left + + for index in [5, 6, 7, 8, 10]: + with pytest.raises(NodeNotFoundError) as err: + _ = root[index] + assert str(err.value) == 'Node missing at index {}'.format(index) + + with pytest.raises(InvalidNodeIndexError) as err: + _ = root[-1] + assert str(err.value) == 'The node index must be a non-negative integer' + + +@pytest.mark.order4 +def test_tree_set_node(): + root = Node(1) + root.left = Node(2) + root.right = Node(3) + root.left.left = Node(4) + root.left.right = Node(5) + root.left.right.left = Node(6) + + new_node_1 = Node(7) + new_node_2 = Node(8) + + with pytest.raises(OperationForbiddenError) as err: + root[0] = new_node_1 + assert str(err.value) == 'Cannot modify the root node' + + with pytest.raises(InvalidNodeIndexError) as err: + root[-1] = new_node_1 + assert str(err.value) == 'The node index must be a non-negative integer' + + with pytest.raises(NodeNotFoundError) as err: + root[100] = new_node_1 + assert str(err.value) == 'Parent node missing at index 49' + + root[10] = new_node_1 + assert root.value == 1 + assert root.left.value == 2 + assert root.right.value == 3 + assert root.left.left.value == 4 + assert root.left.right.value == 5 + assert root.left.right.left.value == 6 + assert root.left.right.right is new_node_1 + + root[4] = new_node_2 + assert root.value == 1 + assert root.left.value == 2 + assert root.right.value == 3 + assert root.left.left.value == 4 + assert root.left.right.value == 8 + assert root.left.right.left is None + assert root.left.right.right is None + + root[1] = new_node_1 + root[2] = new_node_2 + assert root.left is new_node_1 + assert root.right is new_node_2 + + +@pytest.mark.order5 +def test_tree_del_node(): + root = Node(1) + root.left = Node(2) + root.right = Node(3) + root.left.left = Node(4) + root.left.right = Node(5) + root.left.right.left = Node(6) + + with pytest.raises(OperationForbiddenError) as err: + del root[0] + assert str(err.value) == 'Cannot delete the root node' + + with pytest.raises(InvalidNodeIndexError) as err: + del root[-1] + assert str(err.value) == 'The node index must be a non-negative integer' + + with pytest.raises(NodeNotFoundError) as err: + del root[10] + assert str(err.value) == 'No node to delete at index 10' + + with pytest.raises(NodeNotFoundError) as err: + del root[100] + assert str(err.value) == 'No node to delete at index 100' + + del root[3] + assert root.left.left is None + assert root.left.value == 2 + assert root.left.right.value == 5 + assert root.left.right.right is None + assert root.left.right.left.value == 6 + assert root.left.right.left.left is None + assert root.left.right.left.right is None + assert root.right.value == 3 + assert root.right.left is None + assert root.right.right is None + assert root.size == 5 + + del root[2] + assert root.left.left is None + assert root.left.value == 2 + assert root.left.right.value == 5 + assert root.left.right.right is None + assert root.left.right.left.value == 6 + assert root.left.right.left.left is None + assert root.left.right.left.right is None + assert root.right is None + assert root.size == 4 + + del root[4] + assert root.left.left is None + assert root.left.right is None + assert root.right is None + assert root.size == 2 + + del root[1] + assert root.left is None + assert root.right is None + assert root.size == 1 + + +@pytest.mark.order6 +def test_tree_print_no_index(): + + def class_pprint_method(values): + root = build(values) with CaptureOutput() as output: - show_func([1, 2, 3, None, 5]) - assert output == ['', - ' __1 ', - ' / \\ ', - '2 3', - ' \\ ', - ' 5 ', - ' ' - ] - with CaptureOutput() as output: - show_func([1, 2, 3, None, 5, 6]) - assert output == ['', - ' __1__ ', - ' / \\ ', - '2 3', - ' \\ / ', - ' 5 6 ', - ' ' - ] - with CaptureOutput() as output: - show_func([1, 2, 3, None, 5, 6, 7]) - assert output == ['', - ' __1__ ', - ' / \\ ', - '2 3 ', - ' \\ / \\ ', - ' 5 6 7', - ' ' - ] - with CaptureOutput() as output: - show_func([1, 2, 3, 8, 5, 6, 7]) - assert output == ['', - ' __1__ ', - ' / \\ ', - ' 2 3 ', - ' / \\ / \\ ', - '8 5 6 7', - ' ' - ] - - for _ in range(repetitions): - bt = tree(height=10) - with CaptureOutput() as output: - show(bt) - assert output == stringify(bt).splitlines() - - -def test_show_ids(): - def convert_show_ids(target): - show_ids(convert(target)) - - def convert_self_show_ids(target): - convert(target).show_ids() + root.pprint(index=False, delimiter='-') + assert output[0] == '' and output[-1] == '' + return [line for line in output if line != ''] - for invalid_argument in [1, 'foo']: - with pytest.raises(ValueError) as err: - show_ids(invalid_argument) - assert str(err.value) == 'Expecting a list or a node' - - for show_func in [show_ids, convert_show_ids]: - with CaptureOutput() as output: - show_func([]) - assert output == [''] - - for show_func in [show_ids, convert_show_ids, convert_self_show_ids]: + def builtin_print_function(values): + root = build(values) with CaptureOutput() as output: - show_func([1, 2]) - assert output == ['', - ' 0', - ' / ', - '1 ', - ' ' - ] - + print(root) + assert output[0] == '' and output[-1] == '' + return [line for line in output if line != ''] + + for print_without_index in [builtin_print_function, class_pprint_method]: + lines = print_without_index([1]) + assert lines == ['1'] + lines = print_without_index([1, 2]) + assert lines == [' 1', + ' /', + '2'] + lines = print_without_index([1, None, 3]) + assert lines == ['1', + ' \\', + ' 3'] + lines = print_without_index([1, 2, 3]) + assert lines == [' 1', + ' / \\', + '2 3'] + lines = print_without_index([1, 2, 3, None, 5]) + assert lines == [' __1', + ' / \\', + '2 3', + ' \\', + ' 5'] + lines = print_without_index([1, 2, 3, None, 5, 6]) + assert lines == [' __1__', + ' / \\', + '2 3', + ' \\ /', + ' 5 6'] + lines = print_without_index([1, 2, 3, None, 5, 6, 7]) + assert lines == [' __1__', + ' / \\', + '2 3', + ' \\ / \\', + ' 5 6 7'] + lines = print_without_index([1, 2, 3, 8, 5, 6, 7]) + assert lines == [' __1__', + ' / \\', + ' 2 3', + ' / \\ / \\', + '8 5 6 7'] + + +@pytest.mark.order7 +def test_tree_print_with_index(): + + def print_with_index(values): + root = build(values) with CaptureOutput() as output: - show_func([1, None, 3]) - assert output == ['', - '0 ', - ' \\ ', - ' 2', - ' ' - ] - - with CaptureOutput() as output: - show_func([1, 2, 3]) - assert output == ['', - ' 0 ', - ' / \\ ', - '1 2', - ' ' - ] - - with CaptureOutput() as output: - show_func([1, 2, 3, None, 5]) - - assert output == ['', - ' __0 ', - ' / \\ ', - '1 2', - ' \\ ', - ' 4 ', - ' ' - ] - with CaptureOutput() as output: - show_func([1, 2, 3, None, 5, 6]) - assert output == ['', - ' __0__ ', - ' / \\ ', - '1 2', - ' \\ / ', - ' 4 5 ', - ' ' - ] - with CaptureOutput() as output: - show_func([1, 2, 3, None, 5, 6, 7]) - assert output == ['', - ' __0__ ', - ' / \\ ', - '1 2 ', - ' \\ / \\ ', - ' 4 5 6', - ' ' - ] - with CaptureOutput() as output: - show_func([1, 2, 3, 8, 5, 6, 7]) - assert output == ['', - ' __0__ ', - ' / \\ ', - ' 1 2 ', - ' / \\ / \\ ', - '3 4 5 6', - ' ' - ] - - for _ in range(repetitions): - bt = tree(height=10) - with CaptureOutput() as output: - show_ids(bt) - assert output == stringify(bt, True, False).splitlines() - - -def test_show_all(): - def convert_show_all(target): - show_all(convert(target)) - - def convert_self_show_all(target): - convert(target).show_all() - - for invalid_argument in [1, 'foo']: - with pytest.raises(ValueError) as err: - show_all(invalid_argument) - assert str(err.value) == 'Expecting a list or a node' - - for show_func in [show_all, convert_show_all]: - with CaptureOutput() as output: - show_func([]) - assert output == [''] - - for show_func in [show_all, convert_show_all, convert_self_show_all]: - with CaptureOutput() as output: - show_func([1, 2]) - assert output == ['', - ' _0:1', - ' / ', - '1:2 ', - ' ' - ] - - with CaptureOutput() as output: - show_func([1, None, 3]) - assert output == ['', - '0:1_ ', - ' \\ ', - ' 2:3', - ' ' - ] - - with CaptureOutput() as output: - show_func([1, 2, 3]) - assert output == ['', - ' _0:1_ ', - ' / \\ ', - '1:2 2:3', - ' ' - ] - - with CaptureOutput() as output: - show_func([1, 2, 3, None, 5]) - assert output == ['', - ' _____0:1_ ', - ' / \\ ', - '1:2_ 2:3', - ' \\ ', - ' 4:5 ', - ' ' - ] - - with CaptureOutput() as output: - show_func([1, 2, 3, None, 5, 6]) - assert output == ['', - ' _____0:1_____ ', - ' / \\ ', - '1:2_ _2:3', - ' \\ / ', - ' 4:5 5:6 ', - ' ' - ] - with CaptureOutput() as output: - show_func([1, 2, 3, None, 5, 6, 7]) - assert output == ['', - ' _____0:1_____ ', - ' / \\ ', - '1:2_ _2:3_ ', - ' \\ / \\ ', - ' 4:5 5:6 6:7', - ' ' - ] - with CaptureOutput() as output: - show_func([1, 2, 3, 8, 5, 6, 7]) - assert output == ['', - ' _____0:1_____ ', - ' / \\ ', - ' _1:2_ _2:3_ ', - ' / \\ / \\ ', - '3:8 4:5 5:6 6:7', - ' ' - ] - for _ in range(repetitions): - bt = tree(height=10) - with CaptureOutput() as output: - show_all(bt) - assert output == stringify(bt, True, True).splitlines() - - -def test_subtree(): - - def self_subtree(target, node_id): - return target.subtree(node_id) - - for invalid_tree in ['foo', -1, None]: - with pytest.raises(ValueError) as err: - subtree(invalid_tree, 0) - assert str(err.value) == 'Expecting a list or a node' - - for subtree_func in [subtree, self_subtree]: - root = Node(1) - - for invalid_id in ['foo', None]: - with pytest.raises(ValueError) as err: - subtree_func(root, invalid_id) - assert str(err.value) == 'The node ID must be an integer' - - with pytest.raises(ValueError) as err: - subtree_func(root, -1) - assert str(err.value) == 'The node ID must start from 0' - - assert subtree_func(root, 0) == root - for invalid_id in [1, 2, 3, 4, 5]: - with pytest.raises(ValueError) as err: - subtree_func(root, invalid_id) - assert str(err.value) == \ - 'Cannot find node with ID {}'.format(invalid_id) - - root.left = Node(2) - assert subtree_func(root, 0) == root - assert subtree_func(root, 1) == root.left - for invalid_id in [2, 3, 4, 5]: - with pytest.raises(ValueError) as err: - subtree_func(root, invalid_id) - assert str(err.value) == \ - 'Cannot find node with ID {}'.format(invalid_id) - - root.right = Node(3) - assert subtree_func(root, 0) == root - assert subtree_func(root, 1) == root.left - assert subtree_func(root, 2) == root.right - for invalid_id in [3, 4, 5]: - with pytest.raises(ValueError) as err: - subtree_func(root, invalid_id) - assert str(err.value) == \ - 'Cannot find node with ID {}'.format(invalid_id) - - root.left.right = Node(4) - assert subtree_func(root, 0) == root - assert subtree_func(root, 1) == root.left - assert subtree_func(root, 2) == root.right - assert subtree_func(root, 3) == root.left.right - for invalid_id in [4, 5]: - with pytest.raises(ValueError) as err: - subtree_func(root, invalid_id) - assert str(err.value) == \ - 'Cannot find node with ID {}'.format(invalid_id) - - root.left.left = Node(5) - assert subtree_func(root, 0) == root - assert subtree_func(root, 1) == root.left - assert subtree_func(root, 2) == root.right - assert subtree_func(root, 3) == root.left.left - assert subtree_func(root, 4) == root.left.right - for invalid_id in [5, 6]: - with pytest.raises(ValueError) as err: - subtree_func(root, invalid_id) - assert str(err.value) == \ - 'Cannot find node with ID {}'.format(invalid_id) - - -def test_prune(): - - def self_prune(target, node_id): - return target.prune(node_id) - - for invalid_tree in ['foo', -1, None]: - with pytest.raises(ValueError) as err: - prune(invalid_tree, 0) - assert str(err.value) == 'Expecting a list or a node' - - for prune_func in [prune, self_prune]: - root = Node(1) - - for bad_id in ['foo', None]: - with pytest.raises(ValueError) as err: - prune_func(root, bad_id) - assert str(err.value) == 'The node ID must be an integer' - - with pytest.raises(ValueError) as err: - prune_func(root, -1) - assert str(err.value) == 'The node ID must start from 0' - - with pytest.raises(ValueError) as err: - prune_func(root, 0) - assert str(err.value) == 'Cannot prune the root node' - - with pytest.raises(ValueError) as err: - prune_func(root, 10) - assert str(err.value) == 'Cannot find node with ID 10' - - root.left = Node(2) - assert prune_func(root, 1) == root - assert root.left is None - - root.left = Node(2) - root.right = Node(3) - assert prune_func(root, 1) == root - assert root.left is None - assert attr(root, 'right.value') == 3 - - root.left = Node(2) - root.right = Node(3) - root.left.left = Node(4) - root.left.right = Node(5) - root.left.right.left = Node(6) - root.left.right.right = Node(7) - - assert prune_func(root.left.right, 2) == root.left.right - assert attr(root, 'left.right.right') is None - - assert prune_func(root, 4) == root - assert attr(root, 'left.right') is None - - assert prune_func(root, 1) == root - assert attr(root, 'left') is None - assert attr(root, 'right.value') == 3 - - assert prune_func(root, 1) == root - assert attr(root, 'right') is None - - -def test_leafs(): - - def self_leafs(target, values_only): - return target.leafs(values_only) - - def to_set(nodes): - return set(attr(node, 'value') for node in nodes) - - for invalid_tree in ['foo', -1, None]: - with pytest.raises(ValueError) as err: - leafs(invalid_tree) - assert str(err.value) == 'Expecting a list or a node' - - for leafs_func in [leafs, self_leafs]: - root = Node(1) - assert set(leafs_func(root, True)) == {1} - assert to_set(leafs_func(root, False)) == {1} - - root.left = Node(2) - assert set(leafs_func(root, True)) == {2} - assert to_set(leafs_func(root, False)) == {2} - - root.right = Node(3) - assert set(leafs_func(root, True)) == {2, 3} - assert to_set(leafs_func(root, False)) == {2, 3} - - root.left.left = Node(4) - assert set(leafs_func(root, True)) == {3, 4} - assert to_set(leafs_func(root, False)) == {3, 4} - - root.left.right = Node(5) - assert set(leafs_func(root, True)) == {3, 4, 5} - assert to_set(leafs_func(root, False)) == {3, 4, 5} - - -def test_customize(): - null = -1 - - class GoodNode(Node): - - def __init__(self, val, bar=-1, baz=-1): - self.foo = val - self.bar = bar - self.baz = baz - - class BadNode1(object): - - def __init__(self, val, bar=-1, baz=-1): - self.foo = val - self.bar = bar - self.baz = baz - - class BadNode2(object): - - def __init__(self, val, bar=-2, baz=-2): - self.foo = val - self.bar = bar - self.baz = baz - - customize( - node_init=lambda v: GoodNode(v), - node_class=GoodNode, - null_value=null, - value_attr='foo', - left_attr='bar', - right_attr='baz' - ) - for _ in range(repetitions): - nodes_to_visit = [tree(height=10)] - while nodes_to_visit: - node = nodes_to_visit.pop() - - # Check that the new node class is used - assert isinstance(node, GoodNode) - - # Check that the original attributes do not exist - assert not hasattr(node, 'left') - assert not hasattr(node, 'right') - assert not hasattr(node, 'value') - - # Check that the new attributes are as expected - left = attr(node, 'bar') - right = attr(node, 'baz') - value = attr(node, 'foo') - assert isinstance(value, int) - - if left != null: - assert isinstance(left, GoodNode) - nodes_to_visit.append(left) - if right != null: - assert isinstance(right, GoodNode) - nodes_to_visit.append(right) - - customize( - node_init=lambda v: GoodNode(v), - node_class=GoodNode, - null_value=null, - value_attr='foo', - left_attr='bar', - right_attr='baz' - ) - for _ in range(repetitions): - nodes_to_visit = [tree(height=10)] - while nodes_to_visit: - node = nodes_to_visit.pop() - - # Check that the new node class is used - assert isinstance(node, GoodNode) - - # Check that the original attributes do not exist - assert not hasattr(node, 'left') - assert not hasattr(node, 'right') - assert not hasattr(node, 'value') - - # Check that the new attributes are as expected - left = attr(node, 'bar') - right = attr(node, 'baz') - value = attr(node, 'foo') - assert isinstance(value, int) - - if left != null: - assert isinstance(left, GoodNode) - nodes_to_visit.append(left) - if right != null: - assert isinstance(right, GoodNode) - nodes_to_visit.append(right) - - with pytest.raises(ValueError) as err: - customize( - node_init=lambda v: BadNode1(v), - node_class=None, - null_value=-1, - value_attr='foo', - left_attr='bar', - right_attr='baz', - ) - assert 'Invalid class given' in str(err.value) - - with pytest.raises(ValueError) as err: - customize( - node_init=None, - node_class=BadNode1, - null_value=-1, - value_attr='foo', - left_attr='bar', - right_attr='baz', - ) - assert 'function must be a callable' in str(err.value) - - with pytest.raises(ValueError) as err: - customize( - node_init=lambda v: GoodNode(v), - node_class=BadNode1, - null_value=-1, - value_attr='foo', - left_attr='bar', - right_attr='baz', - ) - assert 'returns an instance of "BadNode1"' in str(err.value) - - with pytest.raises(ValueError) as err: - customize( - node_init=lambda v: GoodNode(v), - node_class=GoodNode, - null_value=-1, - value_attr='value', - left_attr='bar', - right_attr='baz', - ) - assert 'required attributes "value"' in str(err.value) - - with pytest.raises(ValueError) as err: - customize( - node_init=lambda v: GoodNode(v), - node_class=GoodNode, - null_value=-1, - value_attr='foo', - left_attr='left', - right_attr='baz', - ) - assert 'required attributes "left"' in str(err.value) - - with pytest.raises(ValueError) as err: - customize( - node_init=lambda v: GoodNode(v), - node_class=GoodNode, - null_value=-1, - value_attr='foo', - left_attr='bar', - right_attr='right', - ) - assert 'required attributes "right"' in str(err.value) - - with pytest.raises(ValueError) as err: - customize( - node_init=lambda v: GoodNode(v), - node_class=GoodNode, - null_value=-1, - value_attr='foo', - left_attr='bar', - right_attr='right', - ) - assert 'required attributes "right"' in str(err.value) - - with pytest.raises(ValueError) as err: - customize( - node_init=lambda v: BadNode2(v, -2), - node_class=BadNode2, - null_value=-1, - value_attr='foo', - left_attr='bar', - right_attr='baz', - ) - assert ( - 'expected null/sentinel value "-1" for its ' - 'left child node attribute "bar"' - ) in str(err.value) - - with pytest.raises(ValueError) as err: - customize( - node_init=lambda v: BadNode2(v, -1, -2), - node_class=BadNode2, - null_value=-1, - value_attr='foo', - left_attr='bar', - right_attr='baz', - ) - assert ( - 'expected null/sentinel value "-1" for its ' - 'right child node attribute "baz"' - ) in str(err.value) + root.pprint(index=True, delimiter=':') + assert output[0] == '' and output[-1] == '' + return [line for line in output if line != ''] + + lines = print_with_index([1]) + assert lines == ['0:1'] + lines = print_with_index([1, 2]) + assert lines == [' _0:1', + ' /', + '1:2'] + lines = print_with_index([1, None, 3]) + assert lines == ['0:1_', + ' \\', + ' 2:3'] + lines = print_with_index([1, 2, 3]) + assert lines == [' _0:1_', + ' / \\', + '1:2 2:3'] + lines = print_with_index([1, 2, 3, None, 5]) + assert lines == [' _____0:1_', + ' / \\', + '1:2_ 2:3', + ' \\', + ' 4:5'] + lines = print_with_index([1, 2, 3, None, 5, 6]) + assert lines == [' _____0:1_____', + ' / \\', + '1:2_ _2:3', + ' \\ /', + ' 4:5 5:6'] + lines = print_with_index([1, 2, 3, None, 5, 6, 7]) + assert lines == [' _____0:1_____', + ' / \\', + '1:2_ _2:3_', + ' \\ / \\', + ' 4:5 5:6 6:7'] + lines = print_with_index([1, 2, 3, 8, 5, 6, 7]) + assert lines == [' _____0:1_____', + ' / \\', + ' _1:2_ _2:3_', + ' / \\ / \\', + '3:8 4:5 5:6 6:7'] + + +@pytest.mark.order8 +def test_tree_validate(): + + class TestNode(Node): + def __setattr__(self, attr, value): + object.__setattr__(self, attr, value) + + root = Node(1) + root.validate() # Should pass + + root = Node(1) + root.left = Node(2) + root.validate() # Should pass + + root = Node(1) + root.left = Node(2) + root.right = Node(3) + root.validate() # Should pass + + root = Node(1) + root.left = Node(2) + root.right = Node(3) + root.left.left = Node(4) + root.left.right = Node(5) + root.left.right.left = Node(6) + root.validate() # Should pass + + root = TestNode(1) + root.left = 'not_a_node' + with pytest.raises(InvalidNodeTypeError) as err: + root.validate() + assert str(err.value) == 'Invalid node instance at index 1' + + root = TestNode(1) + root.right = TestNode(2) + root.right.value = 'not_an_integer' + with pytest.raises(InvalidNodeValueError) as err: + root.validate() + assert str(err.value) == 'Invalid node value at index 2' + + root = TestNode(1) + root.left = TestNode(2) + root.left.right = root + with pytest.raises(CyclicNodeReferenceError) as err: + root.validate() + assert str(err.value) == 'Cyclic node reference at index 4' + + +@pytest.mark.order9 +def test_tree_properties(): + root = Node(1) + assert root.properties == { + 'height': 0, + 'is_balanced': True, + 'is_bst': True, + 'is_complete': True, + 'is_max_heap': True, + 'is_min_heap': True, + 'is_perfect': True, + 'is_strict': True, + 'leaf_count': 1, + 'max_leaf_depth': 0, + 'max_node_value': 1, + 'min_leaf_depth': 0, + 'min_node_value': 1, + 'size': 1 + } + assert root.height == 0 + assert root.is_balanced is True + assert root.is_bst is True + assert root.is_complete is True + assert root.is_max_heap is True + assert root.is_min_heap is True + assert root.is_perfect is True + assert root.is_strict is True + assert root.leaf_count == 1 + assert root.max_leaf_depth == 0 + assert root.max_node_value == 1 + assert root.min_leaf_depth == 0 + assert root.min_node_value == 1 + assert root.size == len(root) == 1 + + root.left = Node(2) + assert root.properties == { + 'height': 1, + 'is_balanced': True, + 'is_bst': False, + 'is_complete': True, + 'is_max_heap': False, + 'is_min_heap': True, + 'is_perfect': False, + 'is_strict': False, + 'leaf_count': 1, + 'max_leaf_depth': 1, + 'max_node_value': 2, + 'min_leaf_depth': 1, + 'min_node_value': 1, + 'size': 2 + } + assert root.height == 1 + assert root.is_balanced is True + assert root.is_bst is False + assert root.is_complete is True + assert root.is_max_heap is False + assert root.is_min_heap is True + assert root.is_perfect is False + assert root.is_strict is False + assert root.leaf_count == 1 + assert root.max_leaf_depth == 1 + assert root.max_node_value == 2 + assert root.min_leaf_depth == 1 + assert root.min_node_value == 1 + assert root.size == len(root) == 2 + + root.right = Node(3) + assert root.properties == { + 'height': 1, + 'is_balanced': True, + 'is_bst': False, + 'is_complete': True, + 'is_max_heap': False, + 'is_min_heap': True, + 'is_perfect': True, + 'is_strict': True, + 'leaf_count': 2, + 'max_leaf_depth': 1, + 'max_node_value': 3, + 'min_leaf_depth': 1, + 'min_node_value': 1, + 'size': 3 + } + assert root.height == 1 + assert root.is_balanced is True + assert root.is_bst is False + assert root.is_complete is True + assert root.is_max_heap is False + assert root.is_min_heap is True + assert root.is_perfect is True + assert root.is_strict is True + assert root.leaf_count == 2 + assert root.max_leaf_depth == 1 + assert root.max_node_value == 3 + assert root.min_leaf_depth == 1 + assert root.min_node_value == 1 + assert root.size == len(root) == 3 + + root.left.left = Node(4) + assert root.properties == { + 'height': 2, + 'is_balanced': True, + 'is_bst': False, + 'is_complete': True, + 'is_max_heap': False, + 'is_min_heap': True, + 'is_perfect': False, + 'is_strict': False, + 'leaf_count': 2, + 'max_leaf_depth': 2, + 'max_node_value': 4, + 'min_leaf_depth': 1, + 'min_node_value': 1, + 'size': 4 + } + assert root.height == 2 + assert root.is_balanced is True + assert root.is_bst is False + assert root.is_complete is True + assert root.is_max_heap is False + assert root.is_min_heap is True + assert root.is_perfect is False + assert root.is_strict is False + assert root.leaf_count == 2 + assert root.max_leaf_depth == 2 + assert root.max_node_value == 4 + assert root.min_leaf_depth == 1 + assert root.min_node_value == 1 + assert root.size == len(root) == 4 + + root.right.left = Node(5) + assert root.properties == { + 'height': 2, + 'is_balanced': True, + 'is_bst': False, + 'is_complete': False, + 'is_max_heap': False, + 'is_min_heap': False, + 'is_perfect': False, + 'is_strict': False, + 'leaf_count': 2, + 'max_leaf_depth': 2, + 'max_node_value': 5, + 'min_leaf_depth': 2, + 'min_node_value': 1, + 'size': 5 + } + assert root.height == 2 + assert root.is_balanced is True + assert root.is_bst is False + assert root.is_complete is False + assert root.is_max_heap is False + assert root.is_min_heap is False + assert root.is_perfect is False + assert root.is_strict is False + assert root.leaf_count == 2 + assert root.max_leaf_depth == 2 + assert root.max_node_value == 5 + assert root.min_leaf_depth == 2 + assert root.min_node_value == 1 + assert root.size == len(root) == 5 + + root.right.left.left = Node(6) + assert root.properties == { + 'height': 3, + 'is_balanced': False, + 'is_bst': False, + 'is_complete': False, + 'is_max_heap': False, + 'is_min_heap': False, + 'is_perfect': False, + 'is_strict': False, + 'leaf_count': 2, + 'max_leaf_depth': 3, + 'max_node_value': 6, + 'min_leaf_depth': 2, + 'min_node_value': 1, + 'size': 6 + } + assert root.height == 3 + assert root.is_balanced is False + assert root.is_bst is False + assert root.is_complete is False + assert root.is_max_heap is False + assert root.is_min_heap is False + assert root.is_perfect is False + assert root.is_strict is False + assert root.leaf_count == 2 + assert root.max_leaf_depth == 3 + assert root.max_node_value == 6 + assert root.min_leaf_depth == 2 + assert root.min_node_value == 1 + assert root.size == len(root) == 6 + + root.left.left.left = Node(7) + assert root.properties == { + 'height': 3, + 'is_balanced': False, + 'is_bst': False, + 'is_complete': False, + 'is_max_heap': False, + 'is_min_heap': False, + 'is_perfect': False, + 'is_strict': False, + 'leaf_count': 2, + 'max_leaf_depth': 3, + 'max_node_value': 7, + 'min_leaf_depth': 3, + 'min_node_value': 1, + 'size': 7 + } + assert root.height == 3 + assert root.is_balanced is False + assert root.is_bst is False + assert root.is_complete is False + assert root.is_max_heap is False + assert root.is_min_heap is False + assert root.is_perfect is False + assert root.is_strict is False + assert root.leaf_count == 2 + assert root.max_leaf_depth == 3 + assert root.max_node_value == 7 + assert root.min_leaf_depth == 3 + assert root.min_node_value == 1 + assert root.size == len(root) == 7 + + +@pytest.mark.order10 +def test_tree_traversal(): + n1 = Node(1) + assert n1.levels == [[n1]] + assert n1.leaves == [n1] + assert n1.inorder == [n1] + assert n1.preorder == [n1] + assert n1.postorder == [n1] + assert n1.levelorder == [n1] + + n2 = Node(2) + n1.left = n2 + assert n1.levels == [[n1], [n2]] + assert n1.leaves == [n2] + assert n1.inorder == [n2, n1] + assert n1.preorder == [n1, n2] + assert n1.postorder == [n2, n1] + assert n1.levelorder == [n1, n2] + + n3 = Node(3) + n1.right = n3 + assert n1.levels == [[n1], [n2, n3]] + assert n1.leaves == [n2, n3] + assert n1.inorder == [n2, n1, n3] + assert n1.preorder == [n1, n2, n3] + assert n1.postorder == [n2, n3, n1] + assert n1.levelorder == [n1, n2, n3] + + n4 = Node(4) + n5 = Node(5) + n2.left = n4 + n2.right = n5 + + assert n1.levels == [[n1], [n2, n3], [n4, n5]] + assert n1.leaves == [n3, n4, n5] + assert n1.inorder == [n4, n2, n5, n1, n3] + assert n1.preorder == [n1, n2, n4, n5, n3] + assert n1.postorder == [n4, n5, n2, n3, n1] + assert n1.levelorder == [n1, n2, n3, n4, n5] + + +def test_tree_list_representation(): + root = Node(1) + assert list(root) == [1] + + root.right = Node(3) + assert list(root) == [1, None, 3] + + root.left = Node(2) + assert list(root) == [1, 2, 3] + + root.right.left = Node(4) + assert list(root) == [1, 2, 3, None, None, 4] + + root.right.right = Node(5) + assert list(root) == [1, 2, 3, None, None, 4, 5] + + root.left.left = Node(6) + assert list(root) == [1, 2, 3, 6, None, 4, 5] + + root.left.right = Node(7) + assert list(root) == [1, 2, 3, 6, 7, 4, 5] + + +@pytest.mark.order11 +def test_tree_generation(): + for invalid_height in ['foo', -1, None]: + with pytest.raises(InvalidTreeHeightError) as err: + tree(height=invalid_height) + assert str(err.value) == 'The height must be an integer between 0 - 9' + + root = tree(height=0) + root.validate() + assert root.height == 0 + assert root.left is None + assert root.right is None + assert isinstance(root.value, int) + + for _ in range(REPETITIONS): + random_height = random.randint(1, 9) + root = tree(random_height) + root.validate() + assert root.height == random_height + + for _ in range(REPETITIONS): + random_height = random.randint(1, 9) + root = tree(random_height, is_perfect=True) + root.validate() + assert root.height == random_height + assert root.is_perfect is True + assert root.is_balanced is True + assert root.is_strict is True + + +@pytest.mark.order12 +def test_bst_generation(): + for invalid_height in ['foo', -1, None]: + with pytest.raises(InvalidTreeHeightError) as err: + bst(height=invalid_height) + assert str(err.value) == 'The height must be an integer between 0 - 9' + + root = bst(height=0) + root.validate() + assert root.height == 0 + assert root.left is None + assert root.right is None + assert isinstance(root.value, int) + + for _ in range(REPETITIONS): + random_height = random.randint(1, 9) + root = bst(random_height) + root.validate() + assert root.is_bst is True + assert root.height == random_height + + for _ in range(REPETITIONS): + random_height = random.randint(1, 9) + root = bst(random_height, is_perfect=True) + root.validate() + assert root.height == random_height + + if not root.is_bst: + print(root) + raise Exception('boo') + + assert root.is_bst is True + assert root.is_perfect is True + assert root.is_balanced is True + assert root.is_strict is True + + +@pytest.mark.order13 +def test_heap_generation(): + for invalid_height in ['foo', -1, None]: + with pytest.raises(InvalidTreeHeightError) as err: + heap(height=invalid_height) + assert str(err.value) == 'The height must be an integer between 0 - 9' + + root = heap(height=0) + root.validate() + assert root.height == 0 + assert root.left is None + assert root.right is None + assert isinstance(root.value, int) + + for _ in range(REPETITIONS): + random_height = random.randint(1, 9) + root = heap(random_height, is_max=True) + root.validate() + assert root.is_max_heap is True + assert root.is_min_heap is False + assert root.height == random_height + + for _ in range(REPETITIONS): + random_height = random.randint(1, 9) + root = heap(random_height, is_max=False) + root.validate() + assert root.is_max_heap is False + assert root.is_min_heap is True + assert root.height == random_height + + for _ in range(REPETITIONS): + random_height = random.randint(1, 9) + root = heap(random_height, is_perfect=True) + root.validate() + assert root.is_max_heap is True + assert root.is_min_heap is False + assert root.is_perfect is True + assert root.is_balanced is True + assert root.is_strict is True + assert root.height == random_height