diff --git a/README.md b/README.md index 5b31e81..276de5c 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,8 @@ also supported. ![IPython Demo](gifs/demo.gif) -**New in version 6.0.0**: You can now use binarytree with -[Graphviz](https://graphviz.org) and [Jupyter Notebooks](https://jupyter.org) -([documentation](https://binarytree.readthedocs.io/en/main/graphviz.html)): +Binarytree can be used with [Graphviz](https://graphviz.org) and +[Jupyter Notebooks](https://jupyter.org) as well: ![Jupyter Demo](gifs/jupyter.gif) @@ -58,16 +57,16 @@ Generate and pretty-print various types of binary trees: ```python from binarytree import tree, bst, heap -# Generate a random binary tree and return its root node +# 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 +# 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 +# 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 +# Pretty-print the trees in stdout. print(my_tree) # # _______1_____ @@ -154,7 +153,7 @@ assert root.min_leaf_depth == 1 assert root.min_node_value == 1 assert root.size == 5 -# See all properties at once: +# See all properties at once. assert root.properties == { 'height': 2, 'is_balanced': True, @@ -178,6 +177,21 @@ print(root.leaves) print(root.levels) # [[Node(1)], [Node(2), Node(3)], [Node(4), Node(5)]] ``` + +Compare and clone trees: +```python +from binarytree import tree + +original = tree() + +# Clone the binary tree. +clone = original.clone() + +# Check if the trees are equal. +original.equals(clone) +``` + + Use [level-order (breadth-first)](https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search) indexes to manipulate nodes: @@ -213,7 +227,7 @@ root.pprint(index=True) print(root[9]) # Node(5) -# Replace the node/subtree at index 4 +# Replace the node/subtree at index 4. root[4] = Node(6, left=Node(7), right=Node(8)) root.pprint(index=True) # @@ -226,7 +240,7 @@ root.pprint(index=True) # 9-7 10-8 # -# Delete the node/subtree at index 1 +# Delete the node/subtree at index 1. del root[1] root.pprint(index=True) # @@ -294,4 +308,41 @@ print(root.values) # [7, 3, 2, 6, 9, None, 1, 5, 8] ``` +Binarytree supports another representation which is more compact but without +the [indexing properties](https://en.wikipedia.org/wiki/Binary_tree#Arrays): + +```python +from binarytree import build, build2, Node + +# First let's create an example tree. +root = Node(1) +root.left = Node(2) +root.left.left = Node(3) +root.left.left.left = Node(4) +root.left.left.right = Node(5) +print(root) +# +# 1 +# / +# __2 +# / +# 3 +# / \ +# 4 5 + +# First representation is was already shown above. +# All "null" nodes in each level are present. +print(root.values) +# [1, 2, None, 3, None, None, None, 4, 5] + +# Second representation is more compact but without the indexing properties. +print(root.values2) +# [1, 2, None, 3, None, 4, 5] + +# Build trees from the list representations +tree1 = build(root.values) +tree2 = build2(root.values2) +assert tree1.equals(tree2) is True +``` + Check out the [documentation](http://binarytree.readthedocs.io) for more details. diff --git a/binarytree/__init__.py b/binarytree/__init__.py index 0695713..537f947 100644 --- a/binarytree/__init__.py +++ b/binarytree/__init__.py @@ -1,4 +1,13 @@ -__all__ = ["Node", "tree", "bst", "heap", "build", "get_parent", "__version__"] +__all__ = [ + "Node", + "tree", + "bst", + "heap", + "build", + "build2", + "get_parent", + "__version__", +] import heapq import random @@ -567,6 +576,7 @@ def graphviz(self, *args: Any, **kwargs: Any) -> Digraph: # pragma: no cover :raise binarytree.exceptions.GraphvizImportError: If graphviz is not installed .. code-block:: python + >>> from binarytree import tree >>> >>> t = tree() @@ -716,6 +726,64 @@ def validate(self) -> None: current_nodes = next_nodes + def equals(self, other: "Node") -> bool: + """Check if this binary tree is equal to other binary tree. + + :param other: Root of the other binary tree. + :type other: binarytree.Node + :return: True if the binary trees a equal, False otherwise. + :rtype: bool + """ + stack1: List[Optional[Node]] = [self] + stack2: List[Optional[Node]] = [other] + + while stack1 or stack2: + node1 = stack1.pop() + node2 = stack2.pop() + + if node1 is None and node2 is None: + continue + elif node1 is None or node2 is None: + return False + elif not isinstance(node2, Node): + return False + else: + if node1.val != node2.val: + return False + stack1.append(node1.right) + stack1.append(node1.left) + stack2.append(node2.right) + stack2.append(node2.left) + + return True + + def clone(self) -> "Node": + """Return a clone of this binary tree. + + :return: Root of the clone. + :rtype: binarytree.Node + """ + other = Node(self.val) + + stack1 = [self] + stack2 = [other] + + while stack1 or stack2: + node1 = stack1.pop() + node2 = stack2.pop() + + if node1.left is not None: + node2.left = Node(node1.left.val) + stack1.append(node1.left) + stack2.append(node2.left) + + if node1.right is not None: + node2.right = Node(node1.right.val) + stack1.append(node1.right) + stack2.append(node2.right) + + return other + @property def values(self) -> List[Optional[NodeValue]]: """Return the `list representation`_ of the binary tree. @@ -723,12 +791,11 @@ def values(self) -> List[Optional[NodeValue]]: .. _list representation: https://en.wikipedia.org/wiki/Binary_tree#Arrays - :return: List representation of the binary tree, which is a list of - node values in breadth-first order starting from the root (current - node). If a node is at index i, its left child is always at 2i + 1, - right child at 2i + 2, and parent at index floor((i - 1) / 2). None - indicates absence of a node at that index. See example below for an - illustration. + :return: List representation of the binary tree, which is a list of node values + in breadth-first order starting from the root. If a node is at index i, its + left child is always at 2i + 1, right child at 2i + 2, and parent at index + floor((i - 1) / 2). None indicates absence of a node at that index. See + example below for an illustration. :rtype: [float | int | None] **Example**: @@ -739,11 +806,12 @@ def values(self) -> List[Optional[NodeValue]]: >>> >>> root = Node(1) >>> root.left = Node(2) - >>> root.right = Node(3) - >>> root.left.right = Node(4) + >>> root.left.left = Node(3) + >>> root.left.left.left = Node(4) + >>> root.left.left.right = Node(5) >>> >>> root.values - [1, 2, 3, None, 4] + [1, 2, None, 3, None, None, None, 4, 5] """ current_nodes: List[Optional[Node]] = [self] has_more_nodes = True @@ -774,6 +842,62 @@ def values(self) -> List[Optional[NodeValue]]: return node_values + @property + def values2(self) -> List[Optional[NodeValue]]: + """Return the list representation (version 2) of the binary tree. + + :return: List of node values like those from :func:`binarytree.Node.values`, + but with a slightly different representation which associates two adjacent + child values with the first parent value that has not been associated yet. + This representation does not provide the same indexing properties where if + a node is at index i, its left child is always at 2i + 1, right child at + 2i + 2, and parent at floor((i - 1) / 2), but it allows for more compact + lists as it does not hold "None"s between nodes in each level. See example + below for an illustration. + :rtype: [float | int | None] + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.left.left = Node(3) + >>> root.left.left.left = Node(4) + >>> root.left.left.right = Node(5) + >>> + >>> root.values + [1, 2, None, 3, None, None, None, 4, 5] + >>> root.values2 + [1, 2, None, 3, None, 4, 5] + """ + current_nodes: List[Node] = [self] + has_more_nodes = True + node_values: List[Optional[NodeValue]] = [self.value] + + while has_more_nodes: + has_more_nodes = False + next_nodes: List[Node] = [] + + for node in current_nodes: + for child in node.left, node.right: + if child is None: + node_values.append(None) + else: + has_more_nodes = True + node_values.append(child.value) + next_nodes.append(child) + + current_nodes = next_nodes + + # Get rid of trailing None values + while node_values and node_values[-1] is None: + node_values.pop() + + return node_values + @property def leaves(self) -> List["Node"]: """Return the leaf nodes of the binary tree. @@ -1717,7 +1841,7 @@ def _generate_random_leaf_count(height: int) -> int: return roll_1 + roll_2 or half_leaf_count -def _generate_random_node_values(height: int) -> List[int]: +def _generate_random_node_values(height: int) -> List[NodeValue]: """Return random node values for building binary trees. :param height: Height of the binary tree. @@ -1726,7 +1850,7 @@ def _generate_random_node_values(height: int) -> List[int]: :rtype: [int] """ max_node_count = 2 ** (height + 1) - 1 - node_values = list(range(max_node_count)) + node_values: List[NodeValue] = list(range(max_node_count)) random.shuffle(node_values) return node_values @@ -1954,7 +2078,7 @@ def get_parent(root: Node, child: Node) -> Optional[Node]: return None -def build(values: List[int]) -> Optional[Node]: +def build(values: List[NodeValue]) -> Optional[Node]: """Build a tree from `list representation`_ and return its root node. .. _list representation: @@ -1963,7 +2087,7 @@ def build(values: List[int]) -> Optional[Node]: :param values: List representation of the binary tree, which is a list of node values in breadth-first order starting from the root (current node). If a node is at index i, its left child is always at 2i + 1, - right child at 2i + 2, and parent at floor((i - 1) / 2). None indicates + right child at 2i + 2, and parent at floor((i - 1) / 2). "None" indicates absence of a node at that index. See example below for an illustration. :type values: [int | float | None] :return: Root node of the binary tree. @@ -2013,6 +2137,75 @@ def build(values: List[int]) -> Optional[Node]: return nodes[0] if nodes else None +def build2(values: List[NodeValue]) -> Optional[Node]: + """Build a tree from a list of values and return its root node. + + :param values: List of node values like those for :func:`binarytree.build`, but + with a slightly different representation which associates two adjacent child + values with the first parent value that has not been associated yet. This + representation does not provide the same indexing properties where if a node + is at index i, its left child is always at 2i + 1, right child at 2i + 2, and + parent at floor((i - 1) / 2), but it allows for more compact lists as it + does not hold "None"s between nodes in each level. See example below for an + illustration. + :type values: [int | float | None] + :return: Root node of the binary tree. + :rtype: binarytree.Node | None + :raise binarytree.exceptions.NodeNotFoundError: If the list representation + is malformed (e.g. a parent node is missing). + + **Example**: + + .. doctest:: + + >>> from binarytree import build2 + >>> + >>> root = build2([2, 5, None, 3, None, 1, 4]) + >>> + >>> print(root) + + 2 + / + __5 + / + 3 + / \\ + 1 4 + + + .. doctest:: + + >>> from binarytree import build2 + >>> + >>> root = build2([None, 1, 2]) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + NodeValueError: node value must be a float or int + """ + queue: Deque[Node] = deque() + root: Optional[Node] = None + + if values: + root = Node(values[0]) + queue.append(root) + + index = 1 + while index < len(values): + node = queue.popleft() + + if values[index] is not None: + node.left = Node(values[index]) + queue.append(node.left) + index += 1 + + if index < len(values) and values[index] is not None: + node.right = Node(values[index]) + queue.append(node.right) + index += 1 + + return root + + def tree(height: int = 3, is_perfect: bool = False) -> Optional[Node]: """Generate a random binary tree and return its root node. diff --git a/docs/overview.rst b/docs/overview.rst index 1195642..066622c 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -18,16 +18,16 @@ Generate and pretty-print various types of binary trees: >>> from binarytree import tree, bst, heap >>> - >>> # Generate a random binary tree and return its root node + >>> # 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 + >>> # 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 + >>> # 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 + >>> # Pretty-print the trees in stdout. >>> print(my_tree) _______1_____ @@ -128,7 +128,7 @@ Inspect tree properties: >>> root.size 5 - >>> properties = root.properties # Get all properties at once + >>> properties = root.properties # Get all properties at once. >>> properties['height'] 2 >>> properties['is_balanced'] @@ -142,6 +142,16 @@ Inspect tree properties: >>> root.levels [[Node(1)], [Node(2), Node(3)], [Node(4), Node(5)]] +Compare and clone trees: + +.. doctest:: + + >>> from binarytree import tree + >>> original = tree() + >>> clone = original.clone() + >>> original.equals(clone) + True + Use `level-order (breadth-first)`_ indexes to manipulate nodes: .. _level-order (breadth-first): @@ -167,7 +177,7 @@ Use `level-order (breadth-first)`_ indexes to manipulate nodes: / 5 - >>> # Use binarytree.Node.pprint instead of print to display indexes + >>> # Use binarytree.Node.pprint instead of print to display indexes. >>> root.pprint(index=True) _________0-1_ @@ -178,11 +188,11 @@ Use `level-order (breadth-first)`_ indexes to manipulate nodes: / 9-5 - >>> # Return the node/subtree at index 9 + >>> # Return the node/subtree at index 9. >>> root[9] Node(5) - >>> # Replace the node/subtree at index 4 + >>> # Replace the node/subtree at index 4. >>> root[4] = Node(6, left=Node(7), right=Node(8)) >>> root.pprint(index=True) @@ -194,7 +204,7 @@ Use `level-order (breadth-first)`_ indexes to manipulate nodes: / \ 9-7 10-8 - >>> # Delete the node/subtree at index 1 + >>> # Delete the node/subtree at index 1. >>> del root[1] >>> root.pprint(index=True) @@ -235,10 +245,10 @@ Traverse trees using different algorithms: >>> root.levelorder [Node(1), Node(2), Node(3), Node(4), Node(5)] - >>> list(root) # Equivalent to root.levelorder + >>> list(root) # Equivalent to root.levelorder. [Node(1), Node(2), Node(3), Node(4), Node(5)] -`List representations`_ are also supported: +Convert to `List representations`_: .. _List representations: https://en.wikipedia.org/wiki/Binary_tree#Arrays @@ -247,7 +257,7 @@ Traverse trees using different algorithms: >>> from binarytree import build >>> - >>> # Build a tree from list representation + >>> # Build a tree from list representation. >>> values = [7, 3, 2, 6, 9, None, 1, 5, 8] >>> root = build(values) >>> print(root) @@ -260,8 +270,50 @@ Traverse trees using different algorithms: / \ 5 8 - >>> # Convert the tree back to list representation + >>> # Convert the tree back to list representation. >>> root.values [7, 3, 2, 6, 9, None, 1, 5, 8] +Binarytree supports another representation which is more compact but without +the `indexing properties`_: + +.. _indexing properties: + https://en.wikipedia.org/wiki/Binary_tree#Arrays + +.. doctest:: + + >>> from binarytree import build, build2, Node + >>> + >>> # First let's create an example tree. + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.left.left = Node(3) + >>> root.left.left.left = Node(4) + >>> root.left.left.right = Node(5) + >>> print(root) + + 1 + / + __2 + / + 3 + / \ + 4 5 + + + >>> # First representation is was already shown above. + >>> # All "null" nodes in each level are present. + >>> root.values + [1, 2, None, 3, None, None, None, 4, 5] + + >>> # Second representation is more compact but without the indexing properties. + >>> root.values2 + [1, 2, None, 3, None, 4, 5] + + >>> # Build trees from both list representations. + >>> tree1 = build(root.values) + >>> tree2 = build2(root.values2) + >>> tree1.equals(tree2) + True + See :doc:`specs` for more details. diff --git a/docs/specs.rst b/docs/specs.rst index 8235bb0..cc0a4bd 100644 --- a/docs/specs.rst +++ b/docs/specs.rst @@ -6,6 +6,7 @@ functions: * :class:`binarytree.Node` * :func:`binarytree.build` +* :func:`binarytree.build2` * :func:`binarytree.tree` * :func:`binarytree.bst` * :func:`binarytree.heap` @@ -25,6 +26,11 @@ Function: binarytree.build .. autofunction:: binarytree.build +Function: binarytree.build2 +=========================== + +.. autofunction:: binarytree.build2 + Function: binarytree.tree ========================= diff --git a/tests/test_tree.py b/tests/test_tree.py index 119bda6..c3820da 100644 --- a/tests/test_tree.py +++ b/tests/test_tree.py @@ -5,7 +5,7 @@ import pytest -from binarytree import Node, bst, build, get_parent, heap, tree +from binarytree import Node, bst, build, build2, get_parent, heap, tree from binarytree.exceptions import ( NodeIndexError, NodeModifyError, @@ -154,8 +154,52 @@ def test_node_set_attributes(): assert str(err.value) == "right child must be a Node instance" +# noinspection PyTypeChecker +def test_tree_equals(): + root1 = Node(1) + root2 = Node(1) + assert root1.equals(None) is False + assert root1.equals(1) is False + assert root1.equals(Node(2)) is False + assert root1.equals(root2) is True + assert root2.equals(root1) is True + + root1.left = Node(2) + assert root1.equals(root2) is False + assert root2.equals(root1) is False + + root2.left = Node(2) + assert root1.equals(root2) is True + assert root2.equals(root1) is True + + root1.right = Node(3) + assert root1.equals(root2) is False + assert root2.equals(root1) is False + + root2.right = Node(3) + assert root1.equals(root2) is True + assert root2.equals(root1) is True + + root1.right.left = Node(4) + assert root1.equals(root2) is False + assert root2.equals(root1) is False + + root2.right.left = Node(4) + assert root1.equals(root2) is True + assert root2.equals(root1) is True + + +def test_tree_clone(): + for _ in range(REPETITIONS): + root = tree() + clone = root.clone() + assert root.values == clone.values + assert root.equals(clone) + assert clone.equals(root) + + # noinspection PyUnresolvedReferences -def test_tree_build(): +def test_list_representation(): root = build([]) assert root is None @@ -197,6 +241,120 @@ def test_tree_build(): build([1, None, 2, 3, 4]) assert str(err.value) == "parent node missing at index 1" + root = Node(1) + assert root.values == [1] + + root.right = Node(3) + assert root.values == [1, None, 3] + + root.left = Node(2) + assert root.values == [1, 2, 3] + + root.right.left = Node(4) + assert root.values == [1, 2, 3, None, None, 4] + + root.right.right = Node(5) + assert root.values == [1, 2, 3, None, None, 4, 5] + + root.left.left = Node(6) + assert root.values == [1, 2, 3, 6, None, 4, 5] + + root.left.right = Node(7) + assert root.values == [1, 2, 3, 6, 7, 4, 5] + + for _ in range(REPETITIONS): + t1 = tree() + t2 = build(t1.values) + assert t1.values == t2.values + + +# noinspection PyUnresolvedReferences +def test_list_representation2(): + root = build2([]) + assert root is None + + root = build2([1]) + assert root.val == 1 + assert root.left is None + assert root.right is None + + root = build2([1, 2]) + assert root.val == 1 + assert root.left.val == 2 + assert root.right is None + + root = build2([1, 2, 3]) + assert root.val == 1 + assert root.left.val == 2 + assert root.right.val == 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 = build2([1, 2, 3, None, 4]) + assert root.val == 1 + assert root.left.val == 2 + assert root.right.val == 3 + assert root.left.left is None + assert root.left.right.val == 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 + + root = build2([1, None, 2, 3, 4]) + assert root.val == 1 + assert root.left is None + assert root.right.val == 2 + assert root.right.left.val == 3 + assert root.right.right.val == 4 + + root = build2([2, 5, None, 3, None, 1, 4]) + assert root.val == 2 + assert root.left.val == 5 + assert root.right is None + assert root.left.left.val == 3 + assert root.left.left.left.val == 1 + assert root.left.left.right.val == 4 + + with pytest.raises(NodeValueError): + build2([None, 1, 2]) + + root = Node(1) + assert root.values2 == [1] + root.right = Node(3) + assert root.values2 == [1, None, 3] + root.left = Node(2) + assert root.values2 == [1, 2, 3] + root.right.left = Node(4) + assert root.values2 == [1, 2, 3, None, None, 4] + root.right.right = Node(5) + assert root.values2 == [1, 2, 3, None, None, 4, 5] + root.left.left = Node(6) + assert root.values2 == [1, 2, 3, 6, None, 4, 5] + root.left.right = Node(7) + assert root.values2 == [1, 2, 3, 6, 7, 4, 5] + + root = Node(1) + assert root.values2 == [1] + root.left = Node(2) + assert root.values2 == [1, 2] + root.right = Node(3) + assert root.values2 == [1, 2, 3] + root.right = None + root.left.left = Node(3) + assert root.values2 == [1, 2, None, 3] + root.left.left.left = Node(4) + assert root.values2 == [1, 2, None, 3, None, 4] + root.left.left.right = Node(5) + assert root.values2 == [1, 2, None, 3, None, 4, 5] + + for _ in range(REPETITIONS): + t1 = tree() + t2 = build2(t1.values2) + assert t1.values2 == t2.values2 + def test_tree_get_node(): root = Node(1) @@ -732,29 +890,6 @@ def test_tree_traversal(): assert n1.levelorder == [n1, n2, n3, n4, n5] -def test_tree_list_representation(): - root = Node(1) - assert root.values == [1] - - root.right = Node(3) - assert root.values == [1, None, 3] - - root.left = Node(2) - assert root.values == [1, 2, 3] - - root.right.left = Node(4) - assert root.values == [1, 2, 3, None, None, 4] - - root.right.right = Node(5) - assert root.values == [1, 2, 3, None, None, 4, 5] - - root.left.left = Node(6) - assert root.values == [1, 2, 3, 6, None, 4, 5] - - root.left.right = Node(7) - assert root.values == [1, 2, 3, 6, 7, 4, 5] - - def test_tree_generation(): for invalid_height in ["foo", -1, None]: with pytest.raises(TreeHeightError) as err: