Source code for binarytree

__all__ = [
    "Node",
    "tree",
    "bst",
    "heap",
    "build",
    "build2",
    "get_index",
    "get_parent",
    "number_to_letters",
    "__version__",
    "NodeValue",
    "NodeValueList",
]

import heapq
import random
from collections import deque
from dataclasses import dataclass
from subprocess import SubprocessError
from typing import Any, Deque, Dict, Iterator, List, Optional, Tuple, Union

from graphviz import Digraph, nohtml

try:  # pragma: no cover
    from graphviz.exceptions import ExecutableNotFound
except ImportError:  # pragma: no cover
    # noinspection PyProtectedMember
    from graphviz import ExecutableNotFound
from pkg_resources import get_distribution

from binarytree.exceptions import (
    NodeIndexError,
    NodeModifyError,
    NodeNotFoundError,
    NodeReferenceError,
    NodeTypeError,
    NodeValueError,
    TreeHeightError,
)

__version__ = get_distribution("binarytree").version

_ATTR_LEFT = "left"
_ATTR_RIGHT = "right"
_ATTR_VAL = "val"
_ATTR_VALUE = "value"
_SVG_XML_TEMPLATE = """
<svg width="{width}" height="{height}" xmlns="http://www.w3.org/2000/svg">
<style>
    .value {{
        font: 300 16px sans-serif;
        text-align: center;
        dominant-baseline: middle;
        text-anchor: middle;
    }}
    .node {{
        fill: lightgray;
        stroke-width: 1;
    }}
</style>
<g stroke="#000000">
{body}
</g>
</svg>
"""
_NODE_VAL_TYPES = (float, int, str)
NodeValue = Any  # Union[float, int, str]
NodeValueList = Union[
    List[Optional[float]],
    List[Optional[int]],
    List[Optional[str]],
    List[float],
    List[int],
    List[str],
]


@dataclass
class NodeProperties:
    height: int
    size: int
    is_max_heap: bool
    is_min_heap: bool
    is_perfect: bool
    is_strict: bool
    is_complete: bool
    leaf_count: int
    min_node_value: NodeValue
    max_node_value: NodeValue
    min_leaf_depth: int
    max_leaf_depth: int


[docs]class Node: """Represents a binary tree node. This class provides methods and properties for managing the current node, and the binary tree in which the node is the root. When a docstring in this class mentions "binary tree", it is referring to the current node and its descendants. :param value: Node value (must be a float/int/str). :type value: float | int | str :param left: Left child node (default: None). :type left: binarytree.Node | None :param right: Right child node (default: None). :type right: binarytree.Node | None :raise binarytree.exceptions.NodeTypeError: If left or right child node is not an instance of :class:`binarytree.Node`. :raise binarytree.exceptions.NodeValueError: If node value is invalid. """ def __init__( self, value: NodeValue, left: Optional["Node"] = None, right: Optional["Node"] = None, ) -> None: self.value = self.val = value self.left = left self.right = right def __repr__(self) -> str: """Return the string representation of the current node. :return: String representation. :rtype: str **Example**: .. doctest:: >>> from binarytree import Node >>> >>> Node(1) Node(1) """ return "Node({})".format(self.val)
[docs] def __str__(self) -> str: """Return the pretty-print string for the binary tree. :return: Pretty-print string. :rtype: str **Example**: .. doctest:: >>> from binarytree import Node >>> >>> root = Node(1) >>> root.left = Node(2) >>> root.right = Node(3) >>> root.left.right = Node(4) >>> >>> print(root) <BLANKLINE> __1 / \\ 2 3 \\ 4 <BLANKLINE> .. note:: To include level-order_ indexes in the output string, use :func:`binarytree.Node.pprint` instead. .. _level-order: 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, attr: str, obj: Any) -> None: """Modified version of ``__setattr__`` with extra sanity checking. Class attributes **left**, **right** and **value** are validated. :param attr: Name of the class attribute. :type attr: str :param obj: Object to set. :type obj: object :raise binarytree.exceptions.NodeTypeError: If left or right child is not an instance of :class:`binarytree.Node`. :raise binarytree.exceptions.NodeValueError: If node value is invalid. **Example**: .. doctest:: >>> from binarytree import Node >>> >>> node = Node(1) >>> node.left = [] # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... binarytree.exceptions.NodeTypeError: Left child must be a Node instance .. doctest:: >>> from binarytree import Node >>> >>> node = Node(1) >>> node.val = [] # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... binarytree.exceptions.NodeValueError: node value must be a float/int/str """ if attr == _ATTR_LEFT: if obj is not None and not isinstance(obj, Node): raise NodeTypeError("left child must be a Node instance") elif attr == _ATTR_RIGHT: if obj is not None and not isinstance(obj, Node): raise NodeTypeError("right child must be a Node instance") elif attr == _ATTR_VALUE: if not isinstance(obj, _NODE_VAL_TYPES): raise NodeValueError("node value must be a float/int/str") object.__setattr__(self, _ATTR_VAL, obj) elif attr == _ATTR_VAL: if not isinstance(obj, _NODE_VAL_TYPES): raise NodeValueError("node value must be a float/int/str") object.__setattr__(self, _ATTR_VALUE, obj) object.__setattr__(self, attr, obj)
[docs] def __iter__(self) -> Iterator["Node"]: """Iterate through the nodes in the binary tree in level-order_. .. _level-order: https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search :return: Node iterator. :rtype: Iterator[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) <BLANKLINE> __1 / \\ 2 3 / \\ 4 5 <BLANKLINE> >>> list(root) [Node(1), Node(2), Node(3), Node(4), Node(5)] """ current_nodes = [self] while len(current_nodes) > 0: next_nodes = [] for node in current_nodes: yield 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
[docs] def __len__(self) -> int: """Return the total number of nodes in the binary tree. :return: 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 sum(1 for _ in iter(self))
[docs] def __getitem__(self, index: int) -> "Node": """Return the node (or subtree) at the given level-order_ index. .. _level-order: https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search :param index: Level-order index of the node. :type index: int :return: Node (or subtree) at the given index. :rtype: binarytree.Node :raise binarytree.exceptions.NodeIndexError: If node index is invalid. :raise binarytree.exceptions.NodeNotFoundError: If the node is missing. **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): ... binarytree.exceptions.NodeNotFoundError: node missing at index 3 """ if not isinstance(index, int) or index < 0: raise NodeIndexError("node index must be a non-negative int") current_nodes: List[Optional[Node]] = [self] current_index = 0 has_more_nodes = True while has_more_nodes: has_more_nodes = False next_nodes: List[Optional[Node]] = [] 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.append(None) next_nodes.append(None) continue next_nodes.append(node.left) next_nodes.append(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))
[docs] def __setitem__(self, index: int, node: "Node") -> None: """Insert a node (or subtree) at the given level-order_ index. * An exception is raised if the parent node is missing. * Any existing node or subtree is overwritten. * Root node (current node) cannot be replaced. .. _level-order: https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search :param index: Level-order index of the node. :type index: int :param node: Node to insert. :type node: binarytree.Node :raise binarytree.exceptions.NodeTypeError: If new node is not an instance of :class:`binarytree.Node`. :raise binarytree.exceptions.NodeNotFoundError: If parent is missing. :raise binarytree.exceptions.NodeModifyError: If user attempts to overwrite the root node (current node). **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): ... binarytree.exceptions.NodeModifyError: 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): ... binarytree.exceptions.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 NodeModifyError("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, _ATTR_LEFT if index % 2 else _ATTR_RIGHT, node)
[docs] def __delitem__(self, index: int) -> None: """Remove the node (or subtree) at the given level-order_ index. * An exception is raised if the target node is missing. * The descendants of the target node (if any) are also removed. * Root node (current node) cannot be deleted. .. _level-order: https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search :param index: Level-order index of the node. :type index: int :raise binarytree.exceptions.NodeNotFoundError: If the target node or its parent is missing. :raise binarytree.exceptions.NodeModifyError: If user attempts to delete the root node (current node). **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): ... binarytree.exceptions.NodeModifyError: 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): ... binarytree.exceptions.NodeNotFoundError: node missing at index 2 """ if index == 0: raise NodeModifyError("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 = _ATTR_LEFT if index % 2 == 1 else _ATTR_RIGHT if getattr(parent, child_attr) is None: raise NodeNotFoundError("no node to delete at index {}".format(index)) setattr(parent, child_attr, None)
[docs] def _repr_svg_(self) -> str: # pragma: no cover """Display the binary tree using Graphviz (used for `Jupyter notebooks`_). .. _Jupyter notebooks: https://jupyter.org """ try: try: # noinspection PyProtectedMember return str(self.graphviz()._repr_svg_()) except AttributeError: # noinspection PyProtectedMember return str(self.graphviz()._repr_image_svg_xml()) except (SubprocessError, ExecutableNotFound, FileNotFoundError): return self.svg()
[docs] def svg(self, node_radius: int = 16) -> str: """Generate SVG XML. :param node_radius: Node radius in pixels (default: 16). :type node_radius: int :return: Raw SVG XML. :rtype: str """ tree_height = self.height scale = node_radius * 3 xml: Deque[str] = deque() def scale_x(x: int, y: int) -> float: diff = tree_height - y x = 2 ** (diff + 1) * x + 2**diff - 1 return 1 + node_radius + scale * x / 2 def scale_y(y: int) -> float: return scale * (1 + y) def add_edge(parent_x: int, parent_y: int, node_x: int, node_y: int) -> None: xml.appendleft( '<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}"/>'.format( x1=scale_x(parent_x, parent_y), y1=scale_y(parent_y), x2=scale_x(node_x, node_y), y2=scale_y(node_y), ) ) def add_node(node_x: int, node_y: int, node_value: NodeValue) -> None: x, y = scale_x(node_x, node_y), scale_y(node_y) xml.append(f'<circle class="node" cx="{x}" cy="{y}" r="{node_radius}"/>') xml.append(f'<text class="value" x="{x}" y="{y}">{node_value}</text>') current_nodes = [self.left, self.right] has_more_nodes = True y = 1 add_node(0, 0, self.value) while has_more_nodes: has_more_nodes = False next_nodes: List[Optional[Node]] = [] for x, node in enumerate(current_nodes): if node is None: next_nodes.append(None) next_nodes.append(None) else: if node.left is not None or node.right is not None: has_more_nodes = True add_edge(x // 2, y - 1, x, y) add_node(x, y, node.value) next_nodes.append(node.left) next_nodes.append(node.right) current_nodes = next_nodes y += 1 return _SVG_XML_TEMPLATE.format( width=scale * (2**tree_height), height=scale * (2 + tree_height), body="\n".join(xml), )
[docs] def graphviz(self, *args: Any, **kwargs: Any) -> Digraph: # pragma: no cover """Return a graphviz.Digraph_ object representing the binary tree. This method's positional and keyword arguments are passed directly into the Digraph's **__init__** method. :return: graphviz.Digraph_ object representing the binary tree. :raise binarytree.exceptions.GraphvizImportError: If graphviz is not installed .. code-block:: python >>> from binarytree import tree >>> >>> t = tree() >>> >>> graph = t.graphviz() # Generate a graphviz object >>> graph.body # Get the DOT body >>> graph.render() # Render the graph .. _graphviz.Digraph: https://graphviz.readthedocs.io/en/stable/api.html#digraph """ if "node_attr" not in kwargs: kwargs["node_attr"] = { "shape": "record", "style": "filled, rounded", "color": "lightgray", "fillcolor": "lightgray", "fontcolor": "black", } digraph = Digraph(*args, **kwargs) for node in self: node_id = str(id(node)) digraph.node(node_id, nohtml(f"<l>|<v> {node.value}|<r>")) if node.left is not None: digraph.edge(f"{node_id}:l", f"{id(node.left)}:v") if node.right is not None: digraph.edge(f"{node_id}:r", f"{id(node.right)}:v") return digraph
[docs] def pprint(self, index: bool = False, delimiter: str = "-") -> None: """Pretty-print the binary tree. :param index: If set to True (default: False), display level-order_ indexes using the format: ``{index}{delimiter}{value}``. :type index: bool :param delimiter: Delimiter character between the node index and the node value (default: '-'). :type delimiter: str **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() <BLANKLINE> __1 / \\ 2 3 \\ 4 <BLANKLINE> >>> root.pprint(index=True) # Format: {index}-{value} <BLANKLINE> _____0-1_ / \\ 1-2_ 2-3 \\ 4-4 <BLANKLINE> .. note:: If you do not need level-order_ indexes in the output string, use :func:`binarytree.Node.__str__` instead. .. _level-order: https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search """ lines = _build_tree_string(self, 0, index, delimiter)[0] print("\n" + "\n".join((line.rstrip() for line in lines)))
[docs] def validate(self) -> None: """Check if the binary tree is malformed. :raise binarytree.exceptions.NodeReferenceError: If there is a cyclic reference to a node in the binary tree. :raise binarytree.exceptions.NodeTypeError: If a node is not an instance of :class:`binarytree.Node`. :raise binarytree.exceptions.NodeValueError: If node value is invalid. **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): ... binarytree.exceptions.NodeReferenceError: cyclic node reference at index 0 """ has_more_nodes = True nodes_seen = set() current_nodes: List[Optional[Node]] = [self] node_index = 0 # level-order index while has_more_nodes: has_more_nodes = False next_nodes: List[Optional[Node]] = [] for node in current_nodes: if node is None: next_nodes.append(None) next_nodes.append(None) else: if node in nodes_seen: raise NodeReferenceError( f"cyclic reference at Node({node.val}) " + f"(level-order index {node_index})" ) if not isinstance(node, Node): raise NodeTypeError( "invalid node instance at index {}".format(node_index) ) if not isinstance(node.val, _NODE_VAL_TYPES): raise NodeValueError( "invalid node value at index {}".format(node_index) ) if not isinstance(node.value, _NODE_VAL_TYPES): # pragma: no cover raise NodeValueError( "invalid node value at index {}".format(node_index) ) if node.left is not None or node.right is not None: has_more_nodes = True nodes_seen.add(node) next_nodes.append(node.left) next_nodes.append(node.right) node_index += 1 current_nodes = next_nodes
[docs] 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 are 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
[docs] 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. .. _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. 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**: .. 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] """ current_nodes: List[Optional[Node]] = [self] has_more_nodes = True node_values: List[Optional[NodeValue]] = [] while has_more_nodes: has_more_nodes = False next_nodes: List[Optional[Node]] = [] for node in current_nodes: if node is None: node_values.append(None) next_nodes.append(None) next_nodes.append(None) else: if node.left is not None or node.right is not None: has_more_nodes = True node_values.append(node.val) next_nodes.append(node.left) next_nodes.append(node.right) 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 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. A leaf node is any node that does not have child nodes. :return: 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) <BLANKLINE> __1 / \\ 2 3 \\ 4 <BLANKLINE> >>> 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) -> List[List["Node"]]: """Return the nodes in the binary tree level by level. :return: Lists of nodes level by level. :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) <BLANKLINE> __1 / \\ 2 3 \\ 4 <BLANKLINE> >>> >>> 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) -> int: """Return the height of the binary tree. Height of a binary tree is the number of edges on the longest path between the root node and a leaf node. Binary tree with just a single node has a height of 0. :return: 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) <BLANKLINE> 1 / 2 / 3 <BLANKLINE> >>> 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) -> int: """Return the total number of nodes in the binary tree. :return: 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 self.__len__() @property def leaf_count(self) -> int: """Return the total number of leaf nodes in the binary tree. A leaf node is a node with no child nodes. :return: Total number of leaf 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.leaf_count 2 """ return _get_tree_properties(self).leaf_count @property def is_balanced(self) -> bool: """Check if the binary tree is height-balanced. A binary tree is height-balanced if it meets the following criteria: * Left subtree is height-balanced. * Right subtree is height-balanced. * The difference between heights of left and right subtrees is no more than 1. * An empty binary tree is always height-balanced. :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) <BLANKLINE> 1 / 2 / 3 <BLANKLINE> >>> root.is_balanced False """ return _is_balanced(self) >= 0 @property def is_bst(self) -> bool: """Check if the binary tree is a BST_ (binary search tree). :return: True if the binary tree is a BST_, False otherwise. :rtype: bool .. _BST: https://en.wikipedia.org/wiki/Binary_search_tree **Example**: .. doctest:: >>> from binarytree import Node >>> >>> root = Node(2) >>> root.left = Node(1) >>> root.right = Node(3) >>> >>> print(root) <BLANKLINE> 2 / \\ 1 3 <BLANKLINE> >>> root.is_bst True """ return _is_bst(self) @property def is_symmetric(self) -> bool: """Check if the binary tree is symmetric. A binary tree is symmetric if it meets the following criteria: * Left subtree is a mirror of the right subtree about the root node. :return: True if the binary tree is a symmetric, False otherwise. :rtype: bool **Example**: .. doctest:: >>> from binarytree import Node >>> >>> root = Node(1) >>> root.left = Node(2) >>> root.right = Node(2) >>> root.left.left = Node(3) >>> root.left.right = Node(4) >>> root.right.left = Node(4) >>> root.right.right = Node(3) >>> >>> print(root) <BLANKLINE> __1__ / \\ 2 2 / \\ / \\ 3 4 4 3 <BLANKLINE> >>> root.is_symmetric True """ return _is_symmetric(self) @property def is_max_heap(self) -> bool: """Check if the binary tree is a `max heap`_. :return: True if the binary tree is a `max heap`_, False otherwise. :rtype: bool .. _max heap: https://en.wikipedia.org/wiki/Min-max_heap **Example**: .. doctest:: >>> from binarytree import Node >>> >>> root = Node(3) >>> root.left = Node(1) >>> root.right = Node(2) >>> >>> print(root) <BLANKLINE> 3 / \\ 1 2 <BLANKLINE> >>> root.is_max_heap True """ return _get_tree_properties(self).is_max_heap @property def is_min_heap(self) -> bool: """Check if the binary tree is a `min heap`_. :return: True if the binary tree is a `min heap`_, False otherwise. :rtype: bool .. _min heap: https://en.wikipedia.org/wiki/Min-max_heap **Example**: .. doctest:: >>> from binarytree import Node >>> >>> root = Node(1) >>> root.left = Node(2) >>> root.right = Node(3) >>> >>> print(root) <BLANKLINE> 1 / \\ 2 3 <BLANKLINE> >>> root.is_min_heap True """ return _get_tree_properties(self).is_min_heap @property def is_perfect(self) -> bool: """Check if the binary tree is perfect. A binary tree is perfect if all its levels are completely filled. See example below for an illustration. :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) <BLANKLINE> __1__ / \\ 2 3 / \\ / \\ 4 5 6 7 <BLANKLINE> >>> root.is_perfect True """ return _get_tree_properties(self).is_perfect @property def is_strict(self) -> bool: """Check if the binary tree is strict. A binary tree is strict if all its non-leaf nodes have both the left and right child nodes. :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) <BLANKLINE> __1 / \\ 2 3 / \\ 4 5 <BLANKLINE> >>> root.is_strict True """ return _get_tree_properties(self).is_strict @property def is_complete(self) -> bool: """Check if the binary tree is complete. A binary tree is complete if it meets the following criteria: * All levels except possibly the last are completely filled. * Last level is left-justified. :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) <BLANKLINE> __1 / \\ 2 3 / \\ 4 5 <BLANKLINE> >>> root.is_complete True """ return _get_tree_properties(self).is_complete @property def min_node_value(self) -> NodeValue: """Return the minimum node value of the binary tree. :return: Minimum node value. :rtype: float | 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) -> NodeValue: """Return the maximum node value of the binary tree. :return: Maximum node value. :rtype: float | 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) -> int: """Return the maximum leaf node depth of the binary tree. :return: 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) <BLANKLINE> 1____ / \\ 2 3 / 4 / 5 <BLANKLINE> >>> root.max_leaf_depth 3 """ return _get_tree_properties(self).max_leaf_depth @property def min_leaf_depth(self) -> int: """Return the minimum leaf node depth of the binary tree. :return: 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) <BLANKLINE> 1____ / \\ 2 3 / 4 / 5 <BLANKLINE> >>> root.min_leaf_depth 1 """ return _get_tree_properties(self).min_leaf_depth @property def properties(self) -> Dict[str, Any]: """Return various properties of the binary tree. :return: Binary tree properties. :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_symmetric'] # equivalent to root.is_symmetric False >>> 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).__dict__.copy() properties["is_balanced"] = _is_balanced(self) >= 0 properties["is_bst"] = _is_bst(self) properties["is_symmetric"] = _is_symmetric(self) return properties @property def inorder(self) -> List["Node"]: """Return the nodes in the binary tree using in-order_ traversal. An in-order_ traversal visits left subtree, root, then right subtree. .. _in-order: https://en.wikipedia.org/wiki/Tree_traversal :return: 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) <BLANKLINE> __1 / \\ 2 3 / \\ 4 5 <BLANKLINE> >>> root.inorder [Node(4), Node(2), Node(5), Node(1), Node(3)] """ result: List[Node] = [] stack: List[Node] = [] node: Optional[Node] = self while node or stack: while node: stack.append(node) node = node.left if stack: node = stack.pop() result.append(node) node = node.right return result @property def preorder(self) -> List["Node"]: """Return the nodes in the binary tree using pre-order_ traversal. A pre-order_ traversal visits root, left subtree, then right subtree. .. _pre-order: https://en.wikipedia.org/wiki/Tree_traversal :return: 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) <BLANKLINE> __1 / \\ 2 3 / \\ 4 5 <BLANKLINE> >>> root.preorder [Node(1), Node(2), Node(4), Node(5), Node(3)] """ result: List[Node] = [] stack: List[Optional[Node]] = [self] while stack: node = stack.pop() if node: result.append(node) stack.append(node.right) stack.append(node.left) return result @property def postorder(self) -> List["Node"]: """Return the nodes in the binary tree using post-order_ traversal. A post-order_ traversal visits left subtree, right subtree, then root. .. _post-order: https://en.wikipedia.org/wiki/Tree_traversal :return: 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) <BLANKLINE> __1 / \\ 2 3 / \\ 4 5 <BLANKLINE> >>> root.postorder [Node(4), Node(5), Node(2), Node(3), Node(1)] """ result: List[Node] = [] stack: List[Optional[Node]] = [self] while stack: node = stack.pop() if node: result.append(node) stack.append(node.left) stack.append(node.right) return result[::-1] @property def levelorder(self) -> List["Node"]: """Return the nodes in the binary tree using level-order_ traversal. A level-order_ traversal visits nodes left to right, level by level. .. _level-order: https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search :return: 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) <BLANKLINE> __1 / \\ 2 3 / \\ 4 5 <BLANKLINE> >>> root.levelorder [Node(1), Node(2), Node(3), Node(4), Node(5)] """ current_nodes = [self] result = [] while len(current_nodes) > 0: next_nodes = [] for node in current_nodes: result.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 result
def _is_balanced(root: Optional[Node]) -> int: """Return the tree height + 1 if balanced, -1 otherwise. :param root: Root node of the binary tree. :type root: binarytree.Node | None :return: Height if the binary tree is balanced, -1 otherwise. :rtype: int """ if root is None: return 0 left = _is_balanced(root.left) if left < 0: return -1 right = _is_balanced(root.right) if right < 0: return -1 return -1 if abs(left - right) > 1 else max(left, right) + 1 def _is_bst(root: Optional[Node]) -> bool: """Check if the binary tree is a BST (binary search tree). :param root: Root node of the binary tree. :type root: binarytree.Node | None :return: True if the binary tree is a BST, False otherwise. :rtype: bool """ stack: List[Node] = [] cur = root pre = None while stack or cur is not None: if cur is not None: stack.append(cur) cur = cur.left else: node = stack.pop() if pre is not None and node.val <= pre.val: return False pre = node cur = node.right return True def _is_symmetric(root: Optional[Node]) -> bool: """Check if the binary tree is symmetric. :param root: Root node of the binary tree. :type root: binarytree.Node | None :return: True if the binary tree is symmetric, False otherwise. :rtype: bool """ def symmetric_helper(left: Optional[Node], right: Optional[Node]) -> bool: if left is None and right is None: return True if left is None or right is None: return False return ( left.val == right.val and symmetric_helper(left.left, right.right) and symmetric_helper(left.right, right.left) ) return symmetric_helper(root, root) def _validate_tree_height(height: int) -> None: """Check if the height of the binary tree is valid. :param height: Height of the binary tree (must be 0 - 9 inclusive). :type height: int :raise binarytree.exceptions.TreeHeightError: If height is invalid. """ if not (type(height) == int and 0 <= height <= 9): raise TreeHeightError("height must be an int between 0 - 9") def _generate_perfect_bst(height: int) -> Optional[Node]: """Generate a perfect BST (binary search tree) and return its root. :param height: Height of the BST. :type height: int :return: Root node of the BST. :rtype: binarytree.Node | None """ max_node_count = 2 ** (height + 1) - 1 node_values = list(range(max_node_count)) return _build_bst_from_sorted_values(node_values) def _build_bst_from_sorted_values(sorted_values: List[int]) -> Optional[Node]: """Recursively build a perfect BST from odd number of sorted values. :param sorted_values: Odd number of sorted values. :type sorted_values: [float | int | str] :return: Root node of the BST. :rtype: binarytree.Node | None """ 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 def _generate_random_leaf_count(height: int) -> int: """Return a random leaf count for building binary trees. :param height: Height of the binary tree. :type height: int :return: Random leaf count. :rtype: int """ max_leaf_count = 2**height half_leaf_count = max_leaf_count // 2 # 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 def number_to_letters(number: int) -> str: """Convert a positive integer to a string of uppercase letters. :param number: A positive integer. :type number: int :return: String of uppercase letters. :rtype: str """ assert number >= 0, "number must be a positive integer" quotient, remainder = divmod(number, 26) return quotient * "Z" + chr(65 + remainder) def _generate_random_numbers(height: int) -> List[int]: """Return random numbers for building binary trees. :param height: Height of the binary tree. :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_tree_string( root: Optional[Node], curr_index: int, include_index: bool = False, delimiter: str = "-", ) -> Tuple[List[str], int, int, int]: """Recursively walk down the binary tree and build a pretty-print string. In each recursive call, a "box" of characters visually representing the 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 node value repr string (required for drawing branches) are sent up to the parent call. The parent call then combines its left and right sub-boxes to build a larger box etc. :param root: Root node of the binary tree. :type root: binarytree.Node | None :param curr_index: Level-order_ index of the current node (root node is 0). :type curr_index: int :param include_index: If set to True, include the level-order_ node indexes using the following format: ``{index}{delimiter}{value}`` (default: False). :type include_index: bool :param delimiter: Delimiter character between the node index and the node value (default: '-'). :type delimiter: :return: Box of characters visually representing the current subtree, width of the box, and start-end positions of the repr string of the new root node value. :rtype: ([str], int, int, int) .. _Level-order: https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search """ if root is None: return [], 0, 0, 0 line1 = [] line2 = [] if include_index: node_repr = "{}{}{}".format(curr_index, delimiter, root.val) else: node_repr = str(root.val) new_root_width = gap_size = len(node_repr) # 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_tree_string( root.left, 2 * curr_index + 1, include_index, delimiter ) r_box, r_box_width, r_root_start, r_root_end = _build_tree_string( root.right, 2 * curr_index + 2, include_index, delimiter ) # Draw the branch connecting the current root node to the left sub-box # Pad the line with whitespaces where necessary if l_box_width > 0: 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 + "/") line2.append(" " * (l_box_width - l_root)) new_root_start = l_box_width + 1 gap_size += 1 else: new_root_start = 0 # Draw the representation of the current root node line1.append(node_repr) line2.append(" " * new_root_width) # Draw the branch connecting the current root node to the right sub-box # Pad the line with whitespaces where necessary if r_box_width > 0: 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 + "\\") line2.append(" " * (r_box_width - r_root)) gap_size += 1 new_root_end = new_root_start + new_root_width - 1 # Combine the left and right sub-boxes with the branches drawn above gap = " " * gap_size new_box = ["".join(line1), "".join(line2)] for i in range(max(len(l_box), len(r_box))): l_line = l_box[i] if i < len(l_box) else " " * l_box_width r_line = r_box[i] if i < len(r_box) else " " * r_box_width new_box.append(l_line + gap + r_line) # Return the new box, its width and its root repr positions return new_box, len(new_box[0]), new_root_start, new_root_end def _get_tree_properties(root: Node) -> NodeProperties: """Inspect the binary tree and return its properties (e.g. height). :param root: Root node of the binary tree. :type root: binarytree.Node :return: Binary tree properties. :rtype: binarytree.NodeProperties """ is_descending = True is_ascending = True min_node_value = root.val max_node_value = root.val size = 0 leaf_count = 0 min_leaf_depth = 0 max_leaf_depth = -1 is_strict = True is_complete = True current_nodes = [root] non_full_node_seen = False while len(current_nodes) > 0: max_leaf_depth += 1 next_nodes = [] for node in current_nodes: size += 1 val = node.val min_node_value = min(val, min_node_value) max_node_value = max(val, max_node_value) # Node is a leaf. if node.left is None and node.right is None: if min_leaf_depth == 0: min_leaf_depth = max_leaf_depth leaf_count += 1 if node.left is not None: if node.left.val > val: is_descending = False elif node.left.val < val: is_ascending = False next_nodes.append(node.left) is_complete = not non_full_node_seen else: non_full_node_seen = True if node.right is not None: if node.right.val > val: is_descending = False elif node.right.val < val: is_ascending = False next_nodes.append(node.right) is_complete = not non_full_node_seen else: non_full_node_seen = True # If we see a node with only one child, it is not strict is_strict &= (node.left is None) == (node.right is None) current_nodes = next_nodes return NodeProperties( 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, min_node_value=min_node_value, max_node_value=max_node_value, min_leaf_depth=min_leaf_depth, max_leaf_depth=max_leaf_depth, )
[docs]def get_index(root: Node, descendent: Node) -> int: """Return the level-order_ index given the root and a possible descendent. .. _level-order: https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search :return: Level-order index of the descendent relative to the root node. :rtype: int :raise binarytree.exceptions.NodeTypeError: If root or descendent is not an instance of :class:`binarytree.Node`. :raise binarytree.exceptions.NodeReferenceError: If given a node that is not a root/descendent. **Example**: .. doctest:: >>> from binarytree import Node, get_index >>> >>> root = Node(1) >>> root.left = Node(2) >>> root.right = Node(3) >>> root.left.right = Node(4) >>> >>> get_index(root, root.left) 1 >>> get_index(root, root.right) 2 >>> get_index(root, root.left.right) 4 >>> get_index(root.left, root.right) Traceback (most recent call last): ... binarytree.exceptions.NodeReferenceError: given nodes are not in the same tree """ if root is None: raise NodeTypeError("root must be a Node instance") if descendent is None: raise NodeTypeError("descendent must be a Node instance") current_nodes: List[Optional[Node]] = [root] current_index = 0 has_more_nodes = True while has_more_nodes: has_more_nodes = False next_nodes: List[Optional[Node]] = [] for node in current_nodes: if node is not None and node is descendent: return current_index if node is None: next_nodes.append(None) next_nodes.append(None) else: next_nodes.append(node.left) next_nodes.append(node.right) if node.left is not None or node.right is not None: has_more_nodes = True current_index += 1 current_nodes = next_nodes raise NodeReferenceError("given nodes are not in the same tree")
[docs]def get_parent(root: Optional[Node], child: Optional[Node]) -> Optional[Node]: """Search the binary tree and return the parent of given child. :param root: Root node of the binary tree. :type: binarytree.Node | None :param child: Child node. :rtype: binarytree.Node | None :return: Parent node, or None if missing. :rtype: binarytree.Node | None **Example**: .. doctest:: >>> from binarytree import Node, get_parent >>> >>> root = Node(1) >>> root.left = Node(2) >>> root.right = Node(3) >>> root.left.right = Node(4) >>> >>> print(root) <BLANKLINE> __1 / \\ 2 3 \\ 4 <BLANKLINE> >>> print(get_parent(root, root.left.right)) <BLANKLINE> 2 \\ 4 <BLANKLINE> """ if child is None: return None stack: List[Optional[Node]] = [root] while stack: node = stack.pop() if node: if node.left is child or node.right is child: return node else: stack.append(node.left) stack.append(node.right) return None
[docs]def build(values: NodeValueList) -> Optional[Node]: """Build a tree from `list representation`_ and return its root node. .. _list representation: https://en.wikipedia.org/wiki/Binary_tree#Arrays :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 absence of a node at that index. See example below for an illustration. :type values: [float | int | str | 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 build >>> >>> root = build([1, 2, 3, None, 4]) >>> >>> print(root) <BLANKLINE> __1 / \\ 2 3 \\ 4 <BLANKLINE> .. doctest:: >>> from binarytree import build >>> >>> root = build([None, 2, 3]) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... binarytree.exceptions.NodeNotFoundError: parent node missing at index 0 """ 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, _ATTR_LEFT if index % 2 else _ATTR_RIGHT, node) return nodes[0] if nodes else None
[docs]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: [float | int | str | 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) <BLANKLINE> 2 / __5 / 3 / \\ 1 4 <BLANKLINE> .. doctest:: >>> from binarytree import build2 >>> >>> root = build2([None, 1, 2]) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... binarytree.exceptions.NodeValueError: node value must be a float/int/str """ 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
[docs]def tree( height: int = 3, is_perfect: bool = False, letters: bool = False, ) -> Optional[Node]: """Generate a random binary tree and return its root node. :param height: 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. If set to False, a perfect binary tree may still be generated by chance. :type is_perfect: bool :param letters: If set to True (default: False), uppercase alphabet letters are used for node values instead of numbers. :type letters: bool :return: Root node of the binary tree. :rtype: binarytree.Node :raise binarytree.exceptions.TreeHeightError: If height is invalid. **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): ... binarytree.exceptions.TreeHeightError: height must be an int between 0 - 9 """ _validate_tree_height(height) numbers = _generate_random_numbers(height) values: NodeValueList = ( list(map(number_to_letters, numbers)) if letters else numbers ) if is_perfect: return build(values) leaf_count = _generate_random_leaf_count(height) root_node = Node(values.pop(0)) leaves = set() for value in values: node = root_node depth = 0 inserted = False while depth < height and not inserted: attr = random.choice((_ATTR_LEFT, _ATTR_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_node
[docs]def bst( height: int = 3, is_perfect: bool = False, letters: bool = False, ) -> Optional[Node]: """Generate a random BST (binary search tree) and return its root node. :param height: 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. If set to False, a perfect BST may still be generated by chance. :type is_perfect: bool :param letters: If set to True (default: False), uppercase alphabet letters are used for node values instead of numbers. :type letters: bool :return: Root node of the BST. :rtype: binarytree.Node :raise binarytree.exceptions.TreeHeightError: If height is invalid. **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): ... binarytree.exceptions.TreeHeightError: height must be an int between 0 - 9 """ _validate_tree_height(height) if is_perfect: return _generate_perfect_bst(height) numbers = _generate_random_numbers(height) values: NodeValueList = ( list(map(number_to_letters, numbers)) if letters else numbers ) leaf_count = _generate_random_leaf_count(height) root_node = Node(values.pop(0)) leaves = set() for value in values: node = root_node depth = 0 inserted = False while depth < height and not inserted: attr = _ATTR_LEFT if node.val > value else _ATTR_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_node
[docs]def heap( height: int = 3, is_max: bool = True, is_perfect: bool = False, letters: bool = False, ) -> Optional[Node]: """Generate a random heap and return its root node. :param height: 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. If set to False, generate a min heap. A binary tree with only the root node is considered 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. If set to False, a perfect heap may still be generated by chance. :type is_perfect: bool :param letters: If set to True (default: False), uppercase alphabet letters are used for node values instead of numbers. :type letters: bool :return: Root node of the heap. :rtype: binarytree.Node :raise binarytree.exceptions.TreeHeightError: If height is invalid. **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): ... binarytree.exceptions.TreeHeightError: height must be an int between 0 - 9 """ _validate_tree_height(height) values = _generate_random_numbers(height) if not is_perfect: # Randomly cut some leaf nodes away random_cut = random.randint(2**height, len(values)) values = values[:random_cut] if is_max: negated = [-v for v in values] heapq.heapify(negated) values = [-v for v in negated] else: heapq.heapify(values) return build(list(map(number_to_letters, values)) if letters else values)