├── .coveragerc ├── .github └── workflows │ ├── build.yaml │ ├── codeql.yaml │ └── pypi.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── binarytree ├── __init__.py └── exceptions.py ├── docs ├── Makefile ├── conf.py ├── exceptions.rst ├── graphviz.rst ├── index.rst ├── make.bat ├── overview.rst └── specs.rst ├── gifs ├── demo.gif └── jupyter.gif ├── pyproject.toml ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── test_tree.py └── utils.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source=binarytree 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | push: 6 | branches: [ main, dev ] 7 | pull_request: 8 | branches: [ main, dev ] 9 | jobs: 10 | build: 11 | name: Test 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ ubuntu-latest, macos-latest, windows-latest ] 16 | python-version: [ "3.7", "3.8", "3.9", "3.10" ] 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Fetch complete history for all tags and branches 20 | run: git fetch --prune --unshallow 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Setup pip 26 | run: python -m pip install --upgrade pip setuptools wheel 27 | - name: Install package 28 | run: pip install .[dev] 29 | - name: Check black version 30 | run: black --version 31 | - name: Run black 32 | run: black -v --check . 33 | - name: Run flake8 34 | run: flake8 . 35 | - name: Run isort 36 | run: isort --check --profile=black . 37 | - name: Run mypy 38 | run: mypy binarytree 39 | - name: Run pytest 40 | run: py.test --cov=binarytree --cov-report=xml 41 | - name: Run Sphinx doctest 42 | run: python -m sphinx -b doctest docs docs/_build 43 | - name: Run Sphinx HTML 44 | run: python -m sphinx -b html -W docs docs/_build 45 | - name: Upload coverge to Codecov 46 | uses: codecov/codecov-action@v2 47 | if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.8' 48 | with: 49 | fail_ci_if_error: true 50 | token: ${{ secrets.CODECOV_TOKEN }} 51 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | on: 3 | push: 4 | branches: [ main, dev ] 5 | pull_request: 6 | branches: [ main, dev ] 7 | schedule: 8 | - cron: '21 2 * * 3' 9 | 10 | jobs: 11 | analyze: 12 | name: Analyze 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | - name: Initialize CodeQL 18 | uses: github/codeql-action/init@v1 19 | with: 20 | languages: 'python' 21 | - name: Perform CodeQL Analysis 22 | uses: github/codeql-action/analyze@v1 23 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yaml: -------------------------------------------------------------------------------- 1 | name: Upload to PyPI 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | upload: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Fetch complete history for all tags and branches 11 | run: git fetch --prune --unshallow 12 | - name: Set up Python 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: '3.x' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install setuptools wheel twine setuptools_scm[toml] 20 | - name: Build distribution 21 | run: python setup.py sdist bdist_wheel 22 | - name: Publish to PyPI Test 23 | env: 24 | TWINE_USERNAME: __token__ 25 | TWINE_PASSWORD: ${{ secrets.PYPI_TEST_TOKEN }} 26 | run: twine upload --repository testpypi dist/* 27 | - name: Publish to PyPI 28 | env: 29 | TWINE_USERNAME: __token__ 30 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 31 | run: twine upload --repository pypi dist/* 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # PyCharm 92 | .idea/ 93 | 94 | # setuptools-scm 95 | binarytree/version.py 96 | 97 | # Jupyter Notebook 98 | *.ipynb 99 | 100 | # direnv 101 | .envrc 102 | .direnv/ 103 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.1.0 4 | hooks: 5 | - id: check-case-conflict 6 | - id: check-executables-have-shebangs 7 | - id: check-json 8 | - id: check-merge-conflict 9 | - id: check-symlinks 10 | - id: check-toml 11 | - id: check-yaml 12 | - id: end-of-file-fixer 13 | - id: mixed-line-ending 14 | - repo: https://github.com/psf/black 15 | rev: 22.3.0 16 | hooks: 17 | - id: black 18 | - repo: https://github.com/timothycrosley/isort 19 | rev: 5.10.1 20 | hooks: 21 | - id: isort 22 | args: [ --profile, black ] 23 | - repo: https://github.com/pre-commit/mirrors-mypy 24 | rev: v0.931 25 | hooks: 26 | - id: mypy 27 | files: ^binarytree/ 28 | additional_dependencies: [ types-all ] 29 | args: [ --strict ] 30 | - repo: https://gitlab.com/pycqa/flake8 31 | rev: 4.0.1 32 | hooks: 33 | - id: flake8 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Set up dev environment: 4 | ```shell 5 | cd ~/your/repository/fork # Activate venv if you have one (recommended) 6 | pip install -e .[dev] # Install dev dependencies (e.g. black, mypy, pre-commit) 7 | pre-commit install # Install git pre-commit hooks 8 | ``` 9 | 10 | Run unit tests with coverage: 11 | 12 | ```shell 13 | py.test --cov=binarytree --cov-report=html # Open htmlcov/index.html in your browser 14 | ``` 15 | 16 | Build and test documentation: 17 | 18 | ```shell 19 | python -m sphinx docs docs/_build # Open docs/_build/index.html in your browser 20 | ``` 21 | 22 | Thank you for your contribution! 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2021 Joohwan Oh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE 2 | prune tests 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Binarytree: Python Library for Studying Binary Trees 2 | 3 | ![Build](https://github.com/joowani/binarytree/workflows/Build/badge.svg) 4 | ![CodeQL](https://github.com/joowani/binarytree/workflows/CodeQL/badge.svg) 5 | [![codecov](https://codecov.io/gh/joowani/binarytree/branch/main/graph/badge.svg?token=C2X2OMPL65)](https://codecov.io/gh/joowani/binarytree) 6 | [![PyPI version](https://badge.fury.io/py/binarytree.svg)](https://badge.fury.io/py/binarytree) 7 | [![GitHub license](https://img.shields.io/github/license/joowani/binarytree?color=brightgreen)](https://github.com/joowani/binarytree/blob/main/LICENSE) 8 | ![Python version](https://img.shields.io/badge/python-3.7%2B-blue) 9 | 10 | Are you studying binary trees for your next exam, assignment or technical interview? 11 | 12 | **Binarytree** is a Python library which lets you generate, visualize, inspect and 13 | manipulate [binary trees](https://en.wikipedia.org/wiki/Binary_tree). Skip the tedious 14 | work of setting up test data, and dive straight into practising your algorithms. 15 | [Heaps](https://en.wikipedia.org/wiki/Heap_(data_structure)) and 16 | [binary search trees](https://en.wikipedia.org/wiki/Binary_search_tree) are also supported. 17 | Self-balancing search trees like [red-black](https://en.wikipedia.org/wiki/Red%E2%80%93black_tree) 18 | or [AVL](https://en.wikipedia.org/wiki/AVL_tree) will be added in the future. 19 | 20 | Check out the [documentation](http://binarytree.readthedocs.io) for more details. 21 | 22 | ![IPython Demo](gifs/demo.gif) 23 | 24 | Binarytree can be used with [Graphviz](https://graphviz.org) and 25 | [Jupyter Notebooks](https://jupyter.org) as well: 26 | 27 | ![Jupyter Demo](gifs/jupyter.gif) 28 | 29 | ## Requirements 30 | 31 | Python 3.7+ 32 | 33 | ## Installation 34 | 35 | Install via [pip](https://pip.pypa.io): 36 | 37 | ```shell 38 | pip install binarytree --upgrade 39 | ``` 40 | 41 | For [conda](https://docs.conda.io) users: 42 | 43 | ```shell 44 | conda install binarytree -c conda-forge 45 | ``` 46 | 47 | ## Getting Started 48 | 49 | Binarytree uses the following class to represent a node: 50 | 51 | ```python 52 | class Node: 53 | 54 | def __init__(self, value, left=None, right=None): 55 | self.value = value # The node value (float/int/str) 56 | self.left = left # Left child 57 | self.right = right # Right child 58 | ``` 59 | 60 | Generate and pretty-print various types of binary trees: 61 | 62 | ```python 63 | from binarytree import tree, bst, heap 64 | 65 | # Generate a random binary tree and return its root node. 66 | my_tree = tree(height=3, is_perfect=False) 67 | 68 | # Generate a random BST and return its root node. 69 | my_bst = bst(height=3, is_perfect=True) 70 | 71 | # Generate a random max heap and return its root node. 72 | my_heap = heap(height=3, is_max=True, is_perfect=False) 73 | 74 | # Pretty-print the trees in stdout. 75 | print(my_tree) 76 | # 77 | # _______1_____ 78 | # / \ 79 | # 4__ ___3 80 | # / \ / \ 81 | # 0 9 13 14 82 | # / \ \ 83 | # 7 10 2 84 | # 85 | print(my_bst) 86 | # 87 | # ______7_______ 88 | # / \ 89 | # __3__ ___11___ 90 | # / \ / \ 91 | # 1 5 9 _13 92 | # / \ / \ / \ / \ 93 | # 0 2 4 6 8 10 12 14 94 | # 95 | print(my_heap) 96 | # 97 | # _____14__ 98 | # / \ 99 | # ____13__ 9 100 | # / \ / \ 101 | # 12 7 3 8 102 | # / \ / 103 | # 0 10 6 104 | # 105 | ``` 106 | Generate trees with letter values instead of numbers: 107 | 108 | ```python 109 | from binarytree import tree 110 | 111 | my_tree = tree(height=3, is_perfect=False, letters=True) 112 | 113 | print(my_tree) 114 | # 115 | # ____H____ 116 | # / \ 117 | # __E__ F__ 118 | # / \ / \ 119 | # M G J B 120 | # \ / / / \ 121 | # O L D I A 122 | # 123 | ``` 124 | 125 | 126 | Build your own trees: 127 | 128 | ```python 129 | from binarytree import Node 130 | 131 | root = Node(1) 132 | root.left = Node(2) 133 | root.right = Node(3) 134 | root.left.right = Node(4) 135 | 136 | print(root) 137 | # 138 | # __1 139 | # / \ 140 | # 2 3 141 | # \ 142 | # 4 143 | # 144 | ``` 145 | 146 | Inspect tree properties: 147 | 148 | ```python 149 | from binarytree import Node 150 | 151 | root = Node(1) 152 | root.left = Node(2) 153 | root.right = Node(3) 154 | root.left.left = Node(4) 155 | root.left.right = Node(5) 156 | 157 | print(root) 158 | # 159 | # __1 160 | # / \ 161 | # 2 3 162 | # / \ 163 | # 4 5 164 | # 165 | assert root.height == 2 166 | assert root.is_balanced is True 167 | assert root.is_bst is False 168 | assert root.is_complete is True 169 | assert root.is_max_heap is False 170 | assert root.is_min_heap is True 171 | assert root.is_perfect is False 172 | assert root.is_strict is True 173 | assert root.leaf_count == 3 174 | assert root.max_leaf_depth == 2 175 | assert root.max_node_value == 5 176 | assert root.min_leaf_depth == 1 177 | assert root.min_node_value == 1 178 | assert root.size == 5 179 | 180 | # See all properties at once. 181 | assert root.properties == { 182 | 'height': 2, 183 | 'is_balanced': True, 184 | 'is_bst': False, 185 | 'is_complete': True, 186 | 'is_max_heap': False, 187 | 'is_min_heap': True, 188 | 'is_perfect': False, 189 | 'is_strict': True, 190 | 'leaf_count': 3, 191 | 'max_leaf_depth': 2, 192 | 'max_node_value': 5, 193 | 'min_leaf_depth': 1, 194 | 'min_node_value': 1, 195 | 'size': 5 196 | } 197 | 198 | print(root.leaves) 199 | # [Node(3), Node(4), Node(5)] 200 | 201 | print(root.levels) 202 | # [[Node(1)], [Node(2), Node(3)], [Node(4), Node(5)]] 203 | ``` 204 | 205 | Compare and clone trees: 206 | ```python 207 | from binarytree import tree 208 | 209 | original = tree() 210 | 211 | # Clone the binary tree. 212 | clone = original.clone() 213 | 214 | # Check if the trees are equal. 215 | original.equals(clone) 216 | ``` 217 | 218 | 219 | Use [level-order (breadth-first)](https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search) 220 | indexes to manipulate nodes: 221 | 222 | ```python 223 | from binarytree import Node 224 | 225 | root = Node(1) # index: 0, value: 1 226 | root.left = Node(2) # index: 1, value: 2 227 | root.right = Node(3) # index: 2, value: 3 228 | root.left.right = Node(4) # index: 4, value: 4 229 | root.left.right.left = Node(5) # index: 9, value: 5 230 | 231 | print(root) 232 | # 233 | # ____1 234 | # / \ 235 | # 2__ 3 236 | # \ 237 | # 4 238 | # / 239 | # 5 240 | # 241 | root.pprint(index=True) 242 | # 243 | # _________0-1_ 244 | # / \ 245 | # 1-2_____ 2-3 246 | # \ 247 | # _4-4 248 | # / 249 | # 9-5 250 | # 251 | print(root[9]) 252 | # Node(5) 253 | 254 | # Replace the node/subtree at index 4. 255 | root[4] = Node(6, left=Node(7), right=Node(8)) 256 | root.pprint(index=True) 257 | # 258 | # ______________0-1_ 259 | # / \ 260 | # 1-2_____ 2-3 261 | # \ 262 | # _4-6_ 263 | # / \ 264 | # 9-7 10-8 265 | # 266 | 267 | # Delete the node/subtree at index 1. 268 | del root[1] 269 | root.pprint(index=True) 270 | # 271 | # 0-1_ 272 | # \ 273 | # 2-3 274 | ``` 275 | 276 | Traverse trees using different algorithms: 277 | 278 | ```python 279 | from binarytree import Node 280 | 281 | root = Node(1) 282 | root.left = Node(2) 283 | root.right = Node(3) 284 | root.left.left = Node(4) 285 | root.left.right = Node(5) 286 | 287 | print(root) 288 | # 289 | # __1 290 | # / \ 291 | # 2 3 292 | # / \ 293 | # 4 5 294 | # 295 | print(root.inorder) 296 | # [Node(4), Node(2), Node(5), Node(1), Node(3)] 297 | 298 | print(root.preorder) 299 | # [Node(1), Node(2), Node(4), Node(5), Node(3)] 300 | 301 | print(root.postorder) 302 | # [Node(4), Node(5), Node(2), Node(3), Node(1)] 303 | 304 | print(root.levelorder) 305 | # [Node(1), Node(2), Node(3), Node(4), Node(5)] 306 | 307 | print(list(root)) # Equivalent to root.levelorder 308 | # [Node(1), Node(2), Node(3), Node(4), Node(5)] 309 | ``` 310 | 311 | Convert to [list representations](https://en.wikipedia.org/wiki/Binary_tree#Arrays): 312 | 313 | ```python 314 | from binarytree import build 315 | 316 | # Build a tree from list representation 317 | values = [7, 3, 2, 6, 9, None, 1, 5, 8] 318 | root = build(values) 319 | print(root) 320 | # 321 | # __7 322 | # / \ 323 | # __3 2 324 | # / \ \ 325 | # 6 9 1 326 | # / \ 327 | # 5 8 328 | # 329 | 330 | # Go back to list representation 331 | print(root.values) 332 | # [7, 3, 2, 6, 9, None, 1, 5, 8] 333 | ``` 334 | 335 | Binarytree supports another representation which is more compact but without 336 | the [indexing properties](https://en.wikipedia.org/wiki/Binary_tree#Arrays) 337 | (this method is often used in [Leetcode](https://leetcode.com/)): 338 | 339 | ```python 340 | from binarytree import build, build2, Node 341 | 342 | # First let's create an example tree. 343 | root = Node(1) 344 | root.left = Node(2) 345 | root.left.left = Node(3) 346 | root.left.left.left = Node(4) 347 | root.left.left.right = Node(5) 348 | print(root) 349 | # 350 | # 1 351 | # / 352 | # __2 353 | # / 354 | # 3 355 | # / \ 356 | # 4 5 357 | 358 | # First representation is already shown above. 359 | # All "null" nodes in each level are present. 360 | print(root.values) 361 | # [1, 2, None, 3, None, None, None, 4, 5] 362 | 363 | # Second representation is more compact but without the indexing properties. 364 | print(root.values2) 365 | # [1, 2, None, 3, None, 4, 5] 366 | 367 | # Build trees from the list representations 368 | tree1 = build(root.values) 369 | tree2 = build2(root.values2) 370 | assert tree1.equals(tree2) is True 371 | ``` 372 | 373 | Check out the [documentation](http://binarytree.readthedocs.io) for more details. 374 | -------------------------------------------------------------------------------- /binarytree/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "Node", 3 | "tree", 4 | "bst", 5 | "heap", 6 | "build", 7 | "build2", 8 | "get_index", 9 | "get_parent", 10 | "number_to_letters", 11 | "__version__", 12 | "NodeValue", 13 | "NodeValueList", 14 | ] 15 | 16 | import heapq 17 | import random 18 | from collections import deque 19 | from dataclasses import dataclass 20 | from subprocess import SubprocessError 21 | from typing import Any, Deque, Dict, Iterator, List, Optional, Tuple, Union 22 | 23 | from graphviz import Digraph, nohtml 24 | 25 | try: # pragma: no cover 26 | from graphviz.exceptions import ExecutableNotFound 27 | except ImportError: # pragma: no cover 28 | # noinspection PyProtectedMember 29 | from graphviz import ExecutableNotFound 30 | from pkg_resources import get_distribution 31 | 32 | from binarytree.exceptions import ( 33 | NodeIndexError, 34 | NodeModifyError, 35 | NodeNotFoundError, 36 | NodeReferenceError, 37 | NodeTypeError, 38 | NodeValueError, 39 | TreeHeightError, 40 | ) 41 | 42 | __version__ = get_distribution("binarytree").version 43 | 44 | _ATTR_LEFT = "left" 45 | _ATTR_RIGHT = "right" 46 | _ATTR_VAL = "val" 47 | _ATTR_VALUE = "value" 48 | _SVG_XML_TEMPLATE = """ 49 | 50 | 62 | 63 | {body} 64 | 65 | 66 | """ 67 | _NODE_VAL_TYPES = (float, int, str) 68 | NodeValue = Any # Union[float, int, str] 69 | NodeValueList = Union[ 70 | List[Optional[float]], 71 | List[Optional[int]], 72 | List[Optional[str]], 73 | List[float], 74 | List[int], 75 | List[str], 76 | ] 77 | 78 | 79 | @dataclass 80 | class NodeProperties: 81 | height: int 82 | size: int 83 | is_max_heap: bool 84 | is_min_heap: bool 85 | is_perfect: bool 86 | is_strict: bool 87 | is_complete: bool 88 | leaf_count: int 89 | min_node_value: NodeValue 90 | max_node_value: NodeValue 91 | min_leaf_depth: int 92 | max_leaf_depth: int 93 | 94 | 95 | class Node: 96 | """Represents a binary tree node. 97 | 98 | This class provides methods and properties for managing the current node, 99 | and the binary tree in which the node is the root. When a docstring in 100 | this class mentions "binary tree", it is referring to the current node and 101 | its descendants. 102 | 103 | :param value: Node value (must be a float/int/str). 104 | :type value: float | int | str 105 | :param left: Left child node (default: None). 106 | :type left: binarytree.Node | None 107 | :param right: Right child node (default: None). 108 | :type right: binarytree.Node | None 109 | :raise binarytree.exceptions.NodeTypeError: If left or right child node is 110 | not an instance of :class:`binarytree.Node`. 111 | :raise binarytree.exceptions.NodeValueError: If node value is invalid. 112 | """ 113 | 114 | def __init__( 115 | self, 116 | value: NodeValue, 117 | left: Optional["Node"] = None, 118 | right: Optional["Node"] = None, 119 | ) -> None: 120 | self.value = self.val = value 121 | self.left = left 122 | self.right = right 123 | 124 | def __repr__(self) -> str: 125 | """Return the string representation of the current node. 126 | 127 | :return: String representation. 128 | :rtype: str 129 | 130 | **Example**: 131 | 132 | .. doctest:: 133 | 134 | >>> from binarytree import Node 135 | >>> 136 | >>> Node(1) 137 | Node(1) 138 | """ 139 | return "Node({})".format(self.val) 140 | 141 | def __str__(self) -> str: 142 | """Return the pretty-print string for the binary tree. 143 | 144 | :return: Pretty-print string. 145 | :rtype: str 146 | 147 | **Example**: 148 | 149 | .. doctest:: 150 | 151 | >>> from binarytree import Node 152 | >>> 153 | >>> root = Node(1) 154 | >>> root.left = Node(2) 155 | >>> root.right = Node(3) 156 | >>> root.left.right = Node(4) 157 | >>> 158 | >>> print(root) 159 | 160 | __1 161 | / \\ 162 | 2 3 163 | \\ 164 | 4 165 | 166 | 167 | .. note:: 168 | To include level-order_ indexes in the output string, use 169 | :func:`binarytree.Node.pprint` instead. 170 | 171 | .. _level-order: 172 | https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search 173 | """ 174 | lines = _build_tree_string(self, 0, False, "-")[0] 175 | return "\n" + "\n".join((line.rstrip() for line in lines)) 176 | 177 | def __setattr__(self, attr: str, obj: Any) -> None: 178 | """Modified version of ``__setattr__`` with extra sanity checking. 179 | 180 | Class attributes **left**, **right** and **value** are validated. 181 | 182 | :param attr: Name of the class attribute. 183 | :type attr: str 184 | :param obj: Object to set. 185 | :type obj: object 186 | :raise binarytree.exceptions.NodeTypeError: If left or right child is 187 | not an instance of :class:`binarytree.Node`. 188 | :raise binarytree.exceptions.NodeValueError: If node value is invalid. 189 | 190 | **Example**: 191 | 192 | .. doctest:: 193 | 194 | >>> from binarytree import Node 195 | >>> 196 | >>> node = Node(1) 197 | >>> node.left = [] # doctest: +IGNORE_EXCEPTION_DETAIL 198 | Traceback (most recent call last): 199 | ... 200 | binarytree.exceptions.NodeTypeError: Left child must be a Node instance 201 | 202 | .. doctest:: 203 | 204 | >>> from binarytree import Node 205 | >>> 206 | >>> node = Node(1) 207 | >>> node.val = [] # doctest: +IGNORE_EXCEPTION_DETAIL 208 | Traceback (most recent call last): 209 | ... 210 | binarytree.exceptions.NodeValueError: node value must be a float/int/str 211 | """ 212 | if attr == _ATTR_LEFT: 213 | if obj is not None and not isinstance(obj, Node): 214 | raise NodeTypeError("left child must be a Node instance") 215 | 216 | elif attr == _ATTR_RIGHT: 217 | if obj is not None and not isinstance(obj, Node): 218 | raise NodeTypeError("right child must be a Node instance") 219 | 220 | elif attr == _ATTR_VALUE: 221 | if not isinstance(obj, _NODE_VAL_TYPES): 222 | raise NodeValueError("node value must be a float/int/str") 223 | object.__setattr__(self, _ATTR_VAL, obj) 224 | 225 | elif attr == _ATTR_VAL: 226 | if not isinstance(obj, _NODE_VAL_TYPES): 227 | raise NodeValueError("node value must be a float/int/str") 228 | object.__setattr__(self, _ATTR_VALUE, obj) 229 | 230 | object.__setattr__(self, attr, obj) 231 | 232 | def __iter__(self) -> Iterator["Node"]: 233 | """Iterate through the nodes in the binary tree in level-order_. 234 | 235 | .. _level-order: 236 | https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search 237 | 238 | :return: Node iterator. 239 | :rtype: Iterator[binarytree.Node] 240 | 241 | **Example**: 242 | 243 | .. doctest:: 244 | 245 | >>> from binarytree import Node 246 | >>> 247 | >>> root = Node(1) 248 | >>> root.left = Node(2) 249 | >>> root.right = Node(3) 250 | >>> root.left.left = Node(4) 251 | >>> root.left.right = Node(5) 252 | >>> 253 | >>> print(root) 254 | 255 | __1 256 | / \\ 257 | 2 3 258 | / \\ 259 | 4 5 260 | 261 | >>> list(root) 262 | [Node(1), Node(2), Node(3), Node(4), Node(5)] 263 | """ 264 | current_nodes = [self] 265 | 266 | while len(current_nodes) > 0: 267 | next_nodes = [] 268 | 269 | for node in current_nodes: 270 | yield node 271 | 272 | if node.left is not None: 273 | next_nodes.append(node.left) 274 | if node.right is not None: 275 | next_nodes.append(node.right) 276 | 277 | current_nodes = next_nodes 278 | 279 | def __len__(self) -> int: 280 | """Return the total number of nodes in the binary tree. 281 | 282 | :return: Total number of nodes. 283 | :rtype: int 284 | 285 | **Example**: 286 | 287 | .. doctest:: 288 | 289 | >>> from binarytree import Node 290 | >>> 291 | >>> root = Node(1) 292 | >>> root.left = Node(2) 293 | >>> root.right = Node(3) 294 | >>> 295 | >>> len(root) 296 | 3 297 | 298 | .. note:: 299 | This method is equivalent to :attr:`binarytree.Node.size`. 300 | """ 301 | return sum(1 for _ in iter(self)) 302 | 303 | def __getitem__(self, index: int) -> "Node": 304 | """Return the node (or subtree) at the given level-order_ index. 305 | 306 | .. _level-order: 307 | https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search 308 | 309 | :param index: Level-order index of the node. 310 | :type index: int 311 | :return: Node (or subtree) at the given index. 312 | :rtype: binarytree.Node 313 | :raise binarytree.exceptions.NodeIndexError: If node index is invalid. 314 | :raise binarytree.exceptions.NodeNotFoundError: If the node is missing. 315 | 316 | **Example**: 317 | 318 | .. doctest:: 319 | 320 | >>> from binarytree import Node 321 | >>> 322 | >>> root = Node(1) # index: 0, value: 1 323 | >>> root.left = Node(2) # index: 1, value: 2 324 | >>> root.right = Node(3) # index: 2, value: 3 325 | >>> 326 | >>> root[0] 327 | Node(1) 328 | >>> root[1] 329 | Node(2) 330 | >>> root[2] 331 | Node(3) 332 | >>> root[3] # doctest: +IGNORE_EXCEPTION_DETAIL 333 | Traceback (most recent call last): 334 | ... 335 | binarytree.exceptions.NodeNotFoundError: node missing at index 3 336 | """ 337 | if not isinstance(index, int) or index < 0: 338 | raise NodeIndexError("node index must be a non-negative int") 339 | 340 | current_nodes: List[Optional[Node]] = [self] 341 | current_index = 0 342 | has_more_nodes = True 343 | 344 | while has_more_nodes: 345 | has_more_nodes = False 346 | next_nodes: List[Optional[Node]] = [] 347 | 348 | for node in current_nodes: 349 | if current_index == index: 350 | if node is None: 351 | break 352 | else: 353 | return node 354 | current_index += 1 355 | 356 | if node is None: 357 | next_nodes.append(None) 358 | next_nodes.append(None) 359 | continue 360 | next_nodes.append(node.left) 361 | next_nodes.append(node.right) 362 | if node.left is not None or node.right is not None: 363 | has_more_nodes = True 364 | 365 | current_nodes = next_nodes 366 | 367 | raise NodeNotFoundError("node missing at index {}".format(index)) 368 | 369 | def __setitem__(self, index: int, node: "Node") -> None: 370 | """Insert a node (or subtree) at the given level-order_ index. 371 | 372 | * An exception is raised if the parent node is missing. 373 | * Any existing node or subtree is overwritten. 374 | * Root node (current node) cannot be replaced. 375 | 376 | .. _level-order: 377 | https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search 378 | 379 | :param index: Level-order index of the node. 380 | :type index: int 381 | :param node: Node to insert. 382 | :type node: binarytree.Node 383 | :raise binarytree.exceptions.NodeTypeError: If new node is not an 384 | instance of :class:`binarytree.Node`. 385 | :raise binarytree.exceptions.NodeNotFoundError: If parent is missing. 386 | :raise binarytree.exceptions.NodeModifyError: If user attempts to 387 | overwrite the root node (current node). 388 | 389 | **Example**: 390 | 391 | .. doctest:: 392 | 393 | >>> from binarytree import Node 394 | >>> 395 | >>> root = Node(1) # index: 0, value: 1 396 | >>> root.left = Node(2) # index: 1, value: 2 397 | >>> root.right = Node(3) # index: 2, value: 3 398 | >>> 399 | >>> root[0] = Node(4) # doctest: +IGNORE_EXCEPTION_DETAIL 400 | Traceback (most recent call last): 401 | ... 402 | binarytree.exceptions.NodeModifyError: cannot modify the root node 403 | 404 | .. doctest:: 405 | 406 | >>> from binarytree import Node 407 | >>> 408 | >>> root = Node(1) # index: 0, value: 1 409 | >>> root.left = Node(2) # index: 1, value: 2 410 | >>> root.right = Node(3) # index: 2, value: 3 411 | >>> 412 | >>> root[11] = Node(4) # doctest: +IGNORE_EXCEPTION_DETAIL 413 | Traceback (most recent call last): 414 | ... 415 | binarytree.exceptions.NodeNotFoundError: parent node missing at index 5 416 | 417 | .. doctest:: 418 | 419 | >>> from binarytree import Node 420 | >>> 421 | >>> root = Node(1) # index: 0, value: 1 422 | >>> root.left = Node(2) # index: 1, value: 2 423 | >>> root.right = Node(3) # index: 2, value: 3 424 | >>> 425 | >>> root[1] = Node(4) 426 | >>> 427 | >>> root.left 428 | Node(4) 429 | """ 430 | if index == 0: 431 | raise NodeModifyError("cannot modify the root node") 432 | 433 | parent_index = (index - 1) // 2 434 | try: 435 | parent = self.__getitem__(parent_index) 436 | except NodeNotFoundError: 437 | raise NodeNotFoundError( 438 | "parent node missing at index {}".format(parent_index) 439 | ) 440 | 441 | setattr(parent, _ATTR_LEFT if index % 2 else _ATTR_RIGHT, node) 442 | 443 | def __delitem__(self, index: int) -> None: 444 | """Remove the node (or subtree) at the given level-order_ index. 445 | 446 | * An exception is raised if the target node is missing. 447 | * The descendants of the target node (if any) are also removed. 448 | * Root node (current node) cannot be deleted. 449 | 450 | .. _level-order: 451 | https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search 452 | 453 | :param index: Level-order index of the node. 454 | :type index: int 455 | :raise binarytree.exceptions.NodeNotFoundError: If the target node or 456 | its parent is missing. 457 | :raise binarytree.exceptions.NodeModifyError: If user attempts to 458 | delete the root node (current node). 459 | 460 | **Example**: 461 | 462 | .. doctest:: 463 | 464 | >>> from binarytree import Node 465 | >>> 466 | >>> root = Node(1) # index: 0, value: 1 467 | >>> root.left = Node(2) # index: 1, value: 2 468 | >>> root.right = Node(3) # index: 2, value: 3 469 | >>> 470 | >>> del root[0] # doctest: +IGNORE_EXCEPTION_DETAIL 471 | Traceback (most recent call last): 472 | ... 473 | binarytree.exceptions.NodeModifyError: cannot delete the root node 474 | 475 | .. doctest:: 476 | 477 | >>> from binarytree import Node 478 | >>> 479 | >>> root = Node(1) # index: 0, value: 1 480 | >>> root.left = Node(2) # index: 1, value: 2 481 | >>> root.right = Node(3) # index: 2, value: 3 482 | >>> 483 | >>> del root[2] 484 | >>> 485 | >>> root[2] # doctest: +IGNORE_EXCEPTION_DETAIL 486 | Traceback (most recent call last): 487 | ... 488 | binarytree.exceptions.NodeNotFoundError: node missing at index 2 489 | """ 490 | if index == 0: 491 | raise NodeModifyError("cannot delete the root node") 492 | 493 | parent_index = (index - 1) // 2 494 | try: 495 | parent = self.__getitem__(parent_index) 496 | except NodeNotFoundError: 497 | raise NodeNotFoundError("no node to delete at index {}".format(index)) 498 | 499 | child_attr = _ATTR_LEFT if index % 2 == 1 else _ATTR_RIGHT 500 | if getattr(parent, child_attr) is None: 501 | raise NodeNotFoundError("no node to delete at index {}".format(index)) 502 | 503 | setattr(parent, child_attr, None) 504 | 505 | def _repr_svg_(self) -> str: # pragma: no cover 506 | """Display the binary tree using Graphviz (used for `Jupyter notebooks`_). 507 | 508 | .. _Jupyter notebooks: https://jupyter.org 509 | """ 510 | try: 511 | try: 512 | # noinspection PyProtectedMember 513 | return str(self.graphviz()._repr_svg_()) 514 | except AttributeError: 515 | # noinspection PyProtectedMember 516 | return str(self.graphviz()._repr_image_svg_xml()) 517 | 518 | except (SubprocessError, ExecutableNotFound, FileNotFoundError): 519 | return self.svg() 520 | 521 | def svg(self, node_radius: int = 16) -> str: 522 | """Generate SVG XML. 523 | 524 | :param node_radius: Node radius in pixels (default: 16). 525 | :type node_radius: int 526 | :return: Raw SVG XML. 527 | :rtype: str 528 | """ 529 | tree_height = self.height 530 | scale = node_radius * 3 531 | xml: Deque[str] = deque() 532 | 533 | def scale_x(x: int, y: int) -> float: 534 | diff = tree_height - y 535 | x = 2 ** (diff + 1) * x + 2**diff - 1 536 | return 1 + node_radius + scale * x / 2 537 | 538 | def scale_y(y: int) -> float: 539 | return scale * (1 + y) 540 | 541 | def add_edge(parent_x: int, parent_y: int, node_x: int, node_y: int) -> None: 542 | xml.appendleft( 543 | ''.format( 544 | x1=scale_x(parent_x, parent_y), 545 | y1=scale_y(parent_y), 546 | x2=scale_x(node_x, node_y), 547 | y2=scale_y(node_y), 548 | ) 549 | ) 550 | 551 | def add_node(node_x: int, node_y: int, node_value: NodeValue) -> None: 552 | x, y = scale_x(node_x, node_y), scale_y(node_y) 553 | xml.append(f'') 554 | xml.append(f'{node_value}') 555 | 556 | current_nodes = [self.left, self.right] 557 | has_more_nodes = True 558 | y = 1 559 | 560 | add_node(0, 0, self.value) 561 | 562 | while has_more_nodes: 563 | 564 | has_more_nodes = False 565 | next_nodes: List[Optional[Node]] = [] 566 | 567 | for x, node in enumerate(current_nodes): 568 | if node is None: 569 | next_nodes.append(None) 570 | next_nodes.append(None) 571 | else: 572 | if node.left is not None or node.right is not None: 573 | has_more_nodes = True 574 | 575 | add_edge(x // 2, y - 1, x, y) 576 | add_node(x, y, node.value) 577 | 578 | next_nodes.append(node.left) 579 | next_nodes.append(node.right) 580 | 581 | current_nodes = next_nodes 582 | y += 1 583 | 584 | return _SVG_XML_TEMPLATE.format( 585 | width=scale * (2**tree_height), 586 | height=scale * (2 + tree_height), 587 | body="\n".join(xml), 588 | ) 589 | 590 | def graphviz(self, *args: Any, **kwargs: Any) -> Digraph: # pragma: no cover 591 | """Return a graphviz.Digraph_ object representing the binary tree. 592 | 593 | This method's positional and keyword arguments are passed directly into the 594 | Digraph's **__init__** method. 595 | 596 | :return: graphviz.Digraph_ object representing the binary tree. 597 | :raise binarytree.exceptions.GraphvizImportError: If graphviz is not installed 598 | 599 | .. code-block:: python 600 | 601 | >>> from binarytree import tree 602 | >>> 603 | >>> t = tree() 604 | >>> 605 | >>> graph = t.graphviz() # Generate a graphviz object 606 | >>> graph.body # Get the DOT body 607 | >>> graph.render() # Render the graph 608 | 609 | .. _graphviz.Digraph: https://graphviz.readthedocs.io/en/stable/api.html#digraph 610 | """ 611 | if "node_attr" not in kwargs: 612 | kwargs["node_attr"] = { 613 | "shape": "record", 614 | "style": "filled, rounded", 615 | "color": "lightgray", 616 | "fillcolor": "lightgray", 617 | "fontcolor": "black", 618 | } 619 | digraph = Digraph(*args, **kwargs) 620 | 621 | for node in self: 622 | node_id = str(id(node)) 623 | 624 | digraph.node(node_id, nohtml(f"| {node.value}|")) 625 | 626 | if node.left is not None: 627 | digraph.edge(f"{node_id}:l", f"{id(node.left)}:v") 628 | 629 | if node.right is not None: 630 | digraph.edge(f"{node_id}:r", f"{id(node.right)}:v") 631 | 632 | return digraph 633 | 634 | def pprint(self, index: bool = False, delimiter: str = "-") -> None: 635 | """Pretty-print the binary tree. 636 | 637 | :param index: If set to True (default: False), display level-order_ 638 | indexes using the format: ``{index}{delimiter}{value}``. 639 | :type index: bool 640 | :param delimiter: Delimiter character between the node index and 641 | the node value (default: '-'). 642 | :type delimiter: str 643 | 644 | **Example**: 645 | 646 | .. doctest:: 647 | 648 | >>> from binarytree import Node 649 | >>> 650 | >>> root = Node(1) # index: 0, value: 1 651 | >>> root.left = Node(2) # index: 1, value: 2 652 | >>> root.right = Node(3) # index: 2, value: 3 653 | >>> root.left.right = Node(4) # index: 4, value: 4 654 | >>> 655 | >>> root.pprint() 656 | 657 | __1 658 | / \\ 659 | 2 3 660 | \\ 661 | 4 662 | 663 | >>> root.pprint(index=True) # Format: {index}-{value} 664 | 665 | _____0-1_ 666 | / \\ 667 | 1-2_ 2-3 668 | \\ 669 | 4-4 670 | 671 | 672 | .. note:: 673 | If you do not need level-order_ indexes in the output string, use 674 | :func:`binarytree.Node.__str__` instead. 675 | 676 | .. _level-order: 677 | https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search 678 | """ 679 | lines = _build_tree_string(self, 0, index, delimiter)[0] 680 | print("\n" + "\n".join((line.rstrip() for line in lines))) 681 | 682 | def validate(self) -> None: 683 | """Check if the binary tree is malformed. 684 | 685 | :raise binarytree.exceptions.NodeReferenceError: If there is a 686 | cyclic reference to a node in the binary tree. 687 | :raise binarytree.exceptions.NodeTypeError: If a node is not an 688 | instance of :class:`binarytree.Node`. 689 | :raise binarytree.exceptions.NodeValueError: If node value is invalid. 690 | 691 | **Example**: 692 | 693 | .. doctest:: 694 | 695 | >>> from binarytree import Node 696 | >>> 697 | >>> root = Node(1) 698 | >>> root.left = Node(2) 699 | >>> root.right = root # Cyclic reference to root 700 | >>> 701 | >>> root.validate() # doctest: +IGNORE_EXCEPTION_DETAIL 702 | Traceback (most recent call last): 703 | ... 704 | binarytree.exceptions.NodeReferenceError: cyclic node reference at index 0 705 | """ 706 | has_more_nodes = True 707 | nodes_seen = set() 708 | current_nodes: List[Optional[Node]] = [self] 709 | node_index = 0 # level-order index 710 | 711 | while has_more_nodes: 712 | 713 | has_more_nodes = False 714 | next_nodes: List[Optional[Node]] = [] 715 | 716 | for node in current_nodes: 717 | if node is None: 718 | next_nodes.append(None) 719 | next_nodes.append(None) 720 | else: 721 | if node in nodes_seen: 722 | raise NodeReferenceError( 723 | f"cyclic reference at Node({node.val}) " 724 | + f"(level-order index {node_index})" 725 | ) 726 | if not isinstance(node, Node): 727 | raise NodeTypeError( 728 | "invalid node instance at index {}".format(node_index) 729 | ) 730 | if not isinstance(node.val, _NODE_VAL_TYPES): 731 | raise NodeValueError( 732 | "invalid node value at index {}".format(node_index) 733 | ) 734 | if not isinstance(node.value, _NODE_VAL_TYPES): # pragma: no cover 735 | raise NodeValueError( 736 | "invalid node value at index {}".format(node_index) 737 | ) 738 | if node.left is not None or node.right is not None: 739 | has_more_nodes = True 740 | 741 | nodes_seen.add(node) 742 | next_nodes.append(node.left) 743 | next_nodes.append(node.right) 744 | 745 | node_index += 1 746 | 747 | current_nodes = next_nodes 748 | 749 | def equals(self, other: "Node") -> bool: 750 | """Check if this binary tree is equal to other binary tree. 751 | 752 | :param other: Root of the other binary tree. 753 | :type other: binarytree.Node 754 | :return: True if the binary trees are equal, False otherwise. 755 | :rtype: bool 756 | """ 757 | stack1: List[Optional[Node]] = [self] 758 | stack2: List[Optional[Node]] = [other] 759 | 760 | while stack1 or stack2: 761 | node1 = stack1.pop() 762 | node2 = stack2.pop() 763 | 764 | if node1 is None and node2 is None: 765 | continue 766 | elif node1 is None or node2 is None: 767 | return False 768 | elif not isinstance(node2, Node): 769 | return False 770 | else: 771 | if node1.val != node2.val: 772 | return False 773 | stack1.append(node1.right) 774 | stack1.append(node1.left) 775 | stack2.append(node2.right) 776 | stack2.append(node2.left) 777 | 778 | return True 779 | 780 | def clone(self) -> "Node": 781 | """Return a clone of this binary tree. 782 | 783 | :return: Root of the clone. 784 | :rtype: binarytree.Node 785 | """ 786 | other = Node(self.val) 787 | 788 | stack1 = [self] 789 | stack2 = [other] 790 | 791 | while stack1 or stack2: 792 | node1 = stack1.pop() 793 | node2 = stack2.pop() 794 | 795 | if node1.left is not None: 796 | node2.left = Node(node1.left.val) 797 | stack1.append(node1.left) 798 | stack2.append(node2.left) 799 | 800 | if node1.right is not None: 801 | node2.right = Node(node1.right.val) 802 | stack1.append(node1.right) 803 | stack2.append(node2.right) 804 | 805 | return other 806 | 807 | @property 808 | def values(self) -> List[Optional[NodeValue]]: 809 | """Return the `list representation`_ of the binary tree. 810 | 811 | .. _list representation: 812 | https://en.wikipedia.org/wiki/Binary_tree#Arrays 813 | 814 | :return: List representation of the binary tree, which is a list of node values 815 | in breadth-first order starting from the root. If a node is at index i, its 816 | left child is always at 2i + 1, right child at 2i + 2, and parent at index 817 | floor((i - 1) / 2). None indicates absence of a node at that index. See 818 | example below for an illustration. 819 | :rtype: [float | int | None] 820 | 821 | **Example**: 822 | 823 | .. doctest:: 824 | 825 | >>> from binarytree import Node 826 | >>> 827 | >>> root = Node(1) 828 | >>> root.left = Node(2) 829 | >>> root.left.left = Node(3) 830 | >>> root.left.left.left = Node(4) 831 | >>> root.left.left.right = Node(5) 832 | >>> 833 | >>> root.values 834 | [1, 2, None, 3, None, None, None, 4, 5] 835 | """ 836 | current_nodes: List[Optional[Node]] = [self] 837 | has_more_nodes = True 838 | node_values: List[Optional[NodeValue]] = [] 839 | 840 | while has_more_nodes: 841 | has_more_nodes = False 842 | next_nodes: List[Optional[Node]] = [] 843 | 844 | for node in current_nodes: 845 | if node is None: 846 | node_values.append(None) 847 | next_nodes.append(None) 848 | next_nodes.append(None) 849 | else: 850 | if node.left is not None or node.right is not None: 851 | has_more_nodes = True 852 | 853 | node_values.append(node.val) 854 | next_nodes.append(node.left) 855 | next_nodes.append(node.right) 856 | 857 | current_nodes = next_nodes 858 | 859 | # Get rid of trailing None values 860 | while node_values and node_values[-1] is None: 861 | node_values.pop() 862 | 863 | return node_values 864 | 865 | @property 866 | def values2(self) -> List[Optional[NodeValue]]: 867 | """Return the list representation (version 2) of the binary tree. 868 | 869 | :return: List of node values like those from :func:`binarytree.Node.values`, 870 | but with a slightly different representation which associates two adjacent 871 | child values with the first parent value that has not been associated yet. 872 | This representation does not provide the same indexing properties where if 873 | a node is at index i, its left child is always at 2i + 1, right child at 874 | 2i + 2, and parent at floor((i - 1) / 2), but it allows for more compact 875 | lists as it does not hold "None"s between nodes in each level. See example 876 | below for an illustration. 877 | :rtype: [float | int | None] 878 | 879 | **Example**: 880 | 881 | .. doctest:: 882 | 883 | >>> from binarytree import Node 884 | >>> 885 | >>> root = Node(1) 886 | >>> root.left = Node(2) 887 | >>> root.left.left = Node(3) 888 | >>> root.left.left.left = Node(4) 889 | >>> root.left.left.right = Node(5) 890 | >>> 891 | >>> root.values 892 | [1, 2, None, 3, None, None, None, 4, 5] 893 | >>> root.values2 894 | [1, 2, None, 3, None, 4, 5] 895 | """ 896 | current_nodes: List[Node] = [self] 897 | has_more_nodes = True 898 | node_values: List[Optional[NodeValue]] = [self.value] 899 | 900 | while has_more_nodes: 901 | has_more_nodes = False 902 | next_nodes: List[Node] = [] 903 | 904 | for node in current_nodes: 905 | for child in node.left, node.right: 906 | if child is None: 907 | node_values.append(None) 908 | else: 909 | has_more_nodes = True 910 | node_values.append(child.value) 911 | next_nodes.append(child) 912 | 913 | current_nodes = next_nodes 914 | 915 | # Get rid of trailing None values 916 | while node_values and node_values[-1] is None: 917 | node_values.pop() 918 | 919 | return node_values 920 | 921 | @property 922 | def leaves(self) -> List["Node"]: 923 | """Return the leaf nodes of the binary tree. 924 | 925 | A leaf node is any node that does not have child nodes. 926 | 927 | :return: List of leaf nodes. 928 | :rtype: [binarytree.Node] 929 | 930 | **Example**: 931 | 932 | .. doctest:: 933 | 934 | >>> from binarytree import Node 935 | >>> 936 | >>> root = Node(1) 937 | >>> root.left = Node(2) 938 | >>> root.right = Node(3) 939 | >>> root.left.right = Node(4) 940 | >>> 941 | >>> print(root) 942 | 943 | __1 944 | / \\ 945 | 2 3 946 | \\ 947 | 4 948 | 949 | >>> root.leaves 950 | [Node(3), Node(4)] 951 | """ 952 | current_nodes = [self] 953 | leaves = [] 954 | 955 | while len(current_nodes) > 0: 956 | next_nodes = [] 957 | for node in current_nodes: 958 | if node.left is None and node.right is None: 959 | leaves.append(node) 960 | continue 961 | if node.left is not None: 962 | next_nodes.append(node.left) 963 | if node.right is not None: 964 | next_nodes.append(node.right) 965 | current_nodes = next_nodes 966 | return leaves 967 | 968 | @property 969 | def levels(self) -> List[List["Node"]]: 970 | """Return the nodes in the binary tree level by level. 971 | 972 | :return: Lists of nodes level by level. 973 | :rtype: [[binarytree.Node]] 974 | 975 | **Example**: 976 | 977 | .. doctest:: 978 | 979 | >>> from binarytree import Node 980 | >>> 981 | >>> root = Node(1) 982 | >>> root.left = Node(2) 983 | >>> root.right = Node(3) 984 | >>> root.left.right = Node(4) 985 | >>> 986 | >>> print(root) 987 | 988 | __1 989 | / \\ 990 | 2 3 991 | \\ 992 | 4 993 | 994 | >>> 995 | >>> root.levels 996 | [[Node(1)], [Node(2), Node(3)], [Node(4)]] 997 | """ 998 | current_nodes = [self] 999 | levels = [] 1000 | 1001 | while len(current_nodes) > 0: 1002 | next_nodes = [] 1003 | 1004 | for node in current_nodes: 1005 | if node.left is not None: 1006 | next_nodes.append(node.left) 1007 | if node.right is not None: 1008 | next_nodes.append(node.right) 1009 | 1010 | levels.append(current_nodes) 1011 | current_nodes = next_nodes 1012 | 1013 | return levels 1014 | 1015 | @property 1016 | def height(self) -> int: 1017 | """Return the height of the binary tree. 1018 | 1019 | Height of a binary tree is the number of edges on the longest path 1020 | between the root node and a leaf node. Binary tree with just a single 1021 | node has a height of 0. 1022 | 1023 | :return: Height of the binary tree. 1024 | :rtype: int 1025 | 1026 | **Example**: 1027 | 1028 | .. doctest:: 1029 | 1030 | >>> from binarytree import Node 1031 | >>> 1032 | >>> root = Node(1) 1033 | >>> root.left = Node(2) 1034 | >>> root.left.left = Node(3) 1035 | >>> 1036 | >>> print(root) 1037 | 1038 | 1 1039 | / 1040 | 2 1041 | / 1042 | 3 1043 | 1044 | >>> root.height 1045 | 2 1046 | 1047 | .. note:: 1048 | A binary tree with only a root node has a height of 0. 1049 | """ 1050 | return _get_tree_properties(self).height 1051 | 1052 | @property 1053 | def size(self) -> int: 1054 | """Return the total number of nodes in the binary tree. 1055 | 1056 | :return: Total number of nodes. 1057 | :rtype: int 1058 | 1059 | **Example**: 1060 | 1061 | .. doctest:: 1062 | 1063 | >>> from binarytree import Node 1064 | >>> 1065 | >>> root = Node(1) 1066 | >>> root.left = Node(2) 1067 | >>> root.right = Node(3) 1068 | >>> root.left.right = Node(4) 1069 | >>> 1070 | >>> root.size 1071 | 4 1072 | 1073 | .. note:: 1074 | This method is equivalent to :func:`binarytree.Node.__len__`. 1075 | """ 1076 | return self.__len__() 1077 | 1078 | @property 1079 | def leaf_count(self) -> int: 1080 | """Return the total number of leaf nodes in the binary tree. 1081 | 1082 | A leaf node is a node with no child nodes. 1083 | 1084 | :return: Total number of leaf nodes. 1085 | :rtype: int 1086 | 1087 | **Example**: 1088 | 1089 | .. doctest:: 1090 | 1091 | >>> from binarytree import Node 1092 | >>> 1093 | >>> root = Node(1) 1094 | >>> root.left = Node(2) 1095 | >>> root.right = Node(3) 1096 | >>> root.left.right = Node(4) 1097 | >>> 1098 | >>> root.leaf_count 1099 | 2 1100 | """ 1101 | return _get_tree_properties(self).leaf_count 1102 | 1103 | @property 1104 | def is_balanced(self) -> bool: 1105 | """Check if the binary tree is height-balanced. 1106 | 1107 | A binary tree is height-balanced if it meets the following criteria: 1108 | 1109 | * Left subtree is height-balanced. 1110 | * Right subtree is height-balanced. 1111 | * The difference between heights of left and right subtrees is no more 1112 | than 1. 1113 | * An empty binary tree is always height-balanced. 1114 | 1115 | :return: True if the binary tree is balanced, False otherwise. 1116 | :rtype: bool 1117 | 1118 | **Example**: 1119 | 1120 | .. doctest:: 1121 | 1122 | >>> from binarytree import Node 1123 | >>> 1124 | >>> root = Node(1) 1125 | >>> root.left = Node(2) 1126 | >>> root.left.left = Node(3) 1127 | >>> 1128 | >>> print(root) 1129 | 1130 | 1 1131 | / 1132 | 2 1133 | / 1134 | 3 1135 | 1136 | >>> root.is_balanced 1137 | False 1138 | """ 1139 | return _is_balanced(self) >= 0 1140 | 1141 | @property 1142 | def is_bst(self) -> bool: 1143 | """Check if the binary tree is a BST_ (binary search tree). 1144 | 1145 | :return: True if the binary tree is a BST_, False otherwise. 1146 | :rtype: bool 1147 | 1148 | .. _BST: https://en.wikipedia.org/wiki/Binary_search_tree 1149 | 1150 | **Example**: 1151 | 1152 | .. doctest:: 1153 | 1154 | >>> from binarytree import Node 1155 | >>> 1156 | >>> root = Node(2) 1157 | >>> root.left = Node(1) 1158 | >>> root.right = Node(3) 1159 | >>> 1160 | >>> print(root) 1161 | 1162 | 2 1163 | / \\ 1164 | 1 3 1165 | 1166 | >>> root.is_bst 1167 | True 1168 | """ 1169 | return _is_bst(self) 1170 | 1171 | @property 1172 | def is_symmetric(self) -> bool: 1173 | """Check if the binary tree is symmetric. 1174 | 1175 | A binary tree is symmetric if it meets the following criteria: 1176 | 1177 | * Left subtree is a mirror of the right subtree about the root node. 1178 | 1179 | :return: True if the binary tree is a symmetric, False otherwise. 1180 | :rtype: bool 1181 | 1182 | **Example**: 1183 | 1184 | .. doctest:: 1185 | 1186 | >>> from binarytree import Node 1187 | >>> 1188 | >>> root = Node(1) 1189 | >>> root.left = Node(2) 1190 | >>> root.right = Node(2) 1191 | >>> root.left.left = Node(3) 1192 | >>> root.left.right = Node(4) 1193 | >>> root.right.left = Node(4) 1194 | >>> root.right.right = Node(3) 1195 | >>> 1196 | >>> print(root) 1197 | 1198 | __1__ 1199 | / \\ 1200 | 2 2 1201 | / \\ / \\ 1202 | 3 4 4 3 1203 | 1204 | >>> root.is_symmetric 1205 | True 1206 | """ 1207 | return _is_symmetric(self) 1208 | 1209 | @property 1210 | def is_max_heap(self) -> bool: 1211 | """Check if the binary tree is a `max heap`_. 1212 | 1213 | :return: True if the binary tree is a `max heap`_, False otherwise. 1214 | :rtype: bool 1215 | 1216 | .. _max heap: https://en.wikipedia.org/wiki/Min-max_heap 1217 | 1218 | **Example**: 1219 | 1220 | .. doctest:: 1221 | 1222 | >>> from binarytree import Node 1223 | >>> 1224 | >>> root = Node(3) 1225 | >>> root.left = Node(1) 1226 | >>> root.right = Node(2) 1227 | >>> 1228 | >>> print(root) 1229 | 1230 | 3 1231 | / \\ 1232 | 1 2 1233 | 1234 | >>> root.is_max_heap 1235 | True 1236 | """ 1237 | return _get_tree_properties(self).is_max_heap 1238 | 1239 | @property 1240 | def is_min_heap(self) -> bool: 1241 | """Check if the binary tree is a `min heap`_. 1242 | 1243 | :return: True if the binary tree is a `min heap`_, False otherwise. 1244 | :rtype: bool 1245 | 1246 | .. _min heap: https://en.wikipedia.org/wiki/Min-max_heap 1247 | 1248 | **Example**: 1249 | 1250 | .. doctest:: 1251 | 1252 | >>> from binarytree import Node 1253 | >>> 1254 | >>> root = Node(1) 1255 | >>> root.left = Node(2) 1256 | >>> root.right = Node(3) 1257 | >>> 1258 | >>> print(root) 1259 | 1260 | 1 1261 | / \\ 1262 | 2 3 1263 | 1264 | >>> root.is_min_heap 1265 | True 1266 | """ 1267 | return _get_tree_properties(self).is_min_heap 1268 | 1269 | @property 1270 | def is_perfect(self) -> bool: 1271 | """Check if the binary tree is perfect. 1272 | 1273 | A binary tree is perfect if all its levels are completely filled. See 1274 | example below for an illustration. 1275 | 1276 | :return: True if the binary tree is perfect, False otherwise. 1277 | :rtype: bool 1278 | 1279 | **Example**: 1280 | 1281 | .. doctest:: 1282 | 1283 | >>> from binarytree import Node 1284 | >>> 1285 | >>> root = Node(1) 1286 | >>> root.left = Node(2) 1287 | >>> root.right = Node(3) 1288 | >>> root.left.left = Node(4) 1289 | >>> root.left.right = Node(5) 1290 | >>> root.right.left = Node(6) 1291 | >>> root.right.right = Node(7) 1292 | >>> 1293 | >>> print(root) 1294 | 1295 | __1__ 1296 | / \\ 1297 | 2 3 1298 | / \\ / \\ 1299 | 4 5 6 7 1300 | 1301 | >>> root.is_perfect 1302 | True 1303 | """ 1304 | return _get_tree_properties(self).is_perfect 1305 | 1306 | @property 1307 | def is_strict(self) -> bool: 1308 | """Check if the binary tree is strict. 1309 | 1310 | A binary tree is strict if all its non-leaf nodes have both the left 1311 | and right child nodes. 1312 | 1313 | :return: True if the binary tree is strict, False otherwise. 1314 | :rtype: bool 1315 | 1316 | **Example**: 1317 | 1318 | .. doctest:: 1319 | 1320 | >>> from binarytree import Node 1321 | >>> 1322 | >>> root = Node(1) 1323 | >>> root.left = Node(2) 1324 | >>> root.right = Node(3) 1325 | >>> root.left.left = Node(4) 1326 | >>> root.left.right = Node(5) 1327 | >>> 1328 | >>> print(root) 1329 | 1330 | __1 1331 | / \\ 1332 | 2 3 1333 | / \\ 1334 | 4 5 1335 | 1336 | >>> root.is_strict 1337 | True 1338 | """ 1339 | return _get_tree_properties(self).is_strict 1340 | 1341 | @property 1342 | def is_complete(self) -> bool: 1343 | """Check if the binary tree is complete. 1344 | 1345 | A binary tree is complete if it meets the following criteria: 1346 | 1347 | * All levels except possibly the last are completely filled. 1348 | * Last level is left-justified. 1349 | 1350 | :return: True if the binary tree is complete, False otherwise. 1351 | :rtype: bool 1352 | 1353 | **Example**: 1354 | 1355 | .. doctest:: 1356 | 1357 | >>> from binarytree import Node 1358 | >>> 1359 | >>> root = Node(1) 1360 | >>> root.left = Node(2) 1361 | >>> root.right = Node(3) 1362 | >>> root.left.left = Node(4) 1363 | >>> root.left.right = Node(5) 1364 | >>> 1365 | >>> print(root) 1366 | 1367 | __1 1368 | / \\ 1369 | 2 3 1370 | / \\ 1371 | 4 5 1372 | 1373 | >>> root.is_complete 1374 | True 1375 | """ 1376 | return _get_tree_properties(self).is_complete 1377 | 1378 | @property 1379 | def min_node_value(self) -> NodeValue: 1380 | """Return the minimum node value of the binary tree. 1381 | 1382 | :return: Minimum node value. 1383 | :rtype: float | int 1384 | 1385 | **Example**: 1386 | 1387 | .. doctest:: 1388 | 1389 | >>> from binarytree import Node 1390 | >>> 1391 | >>> root = Node(1) 1392 | >>> root.left = Node(2) 1393 | >>> root.right = Node(3) 1394 | >>> 1395 | >>> root.min_node_value 1396 | 1 1397 | """ 1398 | return _get_tree_properties(self).min_node_value 1399 | 1400 | @property 1401 | def max_node_value(self) -> NodeValue: 1402 | """Return the maximum node value of the binary tree. 1403 | 1404 | :return: Maximum node value. 1405 | :rtype: float | int 1406 | 1407 | **Example**: 1408 | 1409 | .. doctest:: 1410 | 1411 | >>> from binarytree import Node 1412 | >>> 1413 | >>> root = Node(1) 1414 | >>> root.left = Node(2) 1415 | >>> root.right = Node(3) 1416 | >>> 1417 | >>> root.max_node_value 1418 | 3 1419 | """ 1420 | return _get_tree_properties(self).max_node_value 1421 | 1422 | @property 1423 | def max_leaf_depth(self) -> int: 1424 | """Return the maximum leaf node depth of the binary tree. 1425 | 1426 | :return: Maximum leaf node depth. 1427 | :rtype: int 1428 | 1429 | **Example**: 1430 | 1431 | .. doctest:: 1432 | 1433 | >>> from binarytree import Node 1434 | >>> 1435 | >>> root = Node(1) 1436 | >>> root.left = Node(2) 1437 | >>> root.right = Node(3) 1438 | >>> root.right.left = Node(4) 1439 | >>> root.right.left.left = Node(5) 1440 | >>> 1441 | >>> print(root) 1442 | 1443 | 1____ 1444 | / \\ 1445 | 2 3 1446 | / 1447 | 4 1448 | / 1449 | 5 1450 | 1451 | >>> root.max_leaf_depth 1452 | 3 1453 | """ 1454 | return _get_tree_properties(self).max_leaf_depth 1455 | 1456 | @property 1457 | def min_leaf_depth(self) -> int: 1458 | """Return the minimum leaf node depth of the binary tree. 1459 | 1460 | :return: Minimum leaf node depth. 1461 | :rtype: int 1462 | 1463 | **Example**: 1464 | 1465 | .. doctest:: 1466 | 1467 | >>> from binarytree import Node 1468 | >>> 1469 | >>> root = Node(1) 1470 | >>> root.left = Node(2) 1471 | >>> root.right = Node(3) 1472 | >>> root.right.left = Node(4) 1473 | >>> root.right.left.left = Node(5) 1474 | >>> 1475 | >>> print(root) 1476 | 1477 | 1____ 1478 | / \\ 1479 | 2 3 1480 | / 1481 | 4 1482 | / 1483 | 5 1484 | 1485 | >>> root.min_leaf_depth 1486 | 1 1487 | """ 1488 | return _get_tree_properties(self).min_leaf_depth 1489 | 1490 | @property 1491 | def properties(self) -> Dict[str, Any]: 1492 | """Return various properties of the binary tree. 1493 | 1494 | :return: Binary tree properties. 1495 | :rtype: dict 1496 | 1497 | **Example**: 1498 | 1499 | .. doctest:: 1500 | 1501 | >>> from binarytree import Node 1502 | >>> 1503 | >>> root = Node(1) 1504 | >>> root.left = Node(2) 1505 | >>> root.right = Node(3) 1506 | >>> root.left.left = Node(4) 1507 | >>> root.left.right = Node(5) 1508 | >>> props = root.properties 1509 | >>> 1510 | >>> props['height'] # equivalent to root.height 1511 | 2 1512 | >>> props['size'] # equivalent to root.size 1513 | 5 1514 | >>> props['max_leaf_depth'] # equivalent to root.max_leaf_depth 1515 | 2 1516 | >>> props['min_leaf_depth'] # equivalent to root.min_leaf_depth 1517 | 1 1518 | >>> props['max_node_value'] # equivalent to root.max_node_value 1519 | 5 1520 | >>> props['min_node_value'] # equivalent to root.min_node_value 1521 | 1 1522 | >>> props['leaf_count'] # equivalent to root.leaf_count 1523 | 3 1524 | >>> props['is_balanced'] # equivalent to root.is_balanced 1525 | True 1526 | >>> props['is_bst'] # equivalent to root.is_bst 1527 | False 1528 | >>> props['is_complete'] # equivalent to root.is_complete 1529 | True 1530 | >>> props['is_symmetric'] # equivalent to root.is_symmetric 1531 | False 1532 | >>> props['is_max_heap'] # equivalent to root.is_max_heap 1533 | False 1534 | >>> props['is_min_heap'] # equivalent to root.is_min_heap 1535 | True 1536 | >>> props['is_perfect'] # equivalent to root.is_perfect 1537 | False 1538 | >>> props['is_strict'] # equivalent to root.is_strict 1539 | True 1540 | """ 1541 | properties = _get_tree_properties(self).__dict__.copy() 1542 | properties["is_balanced"] = _is_balanced(self) >= 0 1543 | properties["is_bst"] = _is_bst(self) 1544 | properties["is_symmetric"] = _is_symmetric(self) 1545 | return properties 1546 | 1547 | @property 1548 | def inorder(self) -> List["Node"]: 1549 | """Return the nodes in the binary tree using in-order_ traversal. 1550 | 1551 | An in-order_ traversal visits left subtree, root, then right subtree. 1552 | 1553 | .. _in-order: https://en.wikipedia.org/wiki/Tree_traversal 1554 | 1555 | :return: List of nodes. 1556 | :rtype: [binarytree.Node] 1557 | 1558 | **Example**: 1559 | 1560 | .. doctest:: 1561 | 1562 | >>> from binarytree import Node 1563 | >>> 1564 | >>> root = Node(1) 1565 | >>> root.left = Node(2) 1566 | >>> root.right = Node(3) 1567 | >>> root.left.left = Node(4) 1568 | >>> root.left.right = Node(5) 1569 | >>> 1570 | >>> print(root) 1571 | 1572 | __1 1573 | / \\ 1574 | 2 3 1575 | / \\ 1576 | 4 5 1577 | 1578 | >>> root.inorder 1579 | [Node(4), Node(2), Node(5), Node(1), Node(3)] 1580 | """ 1581 | result: List[Node] = [] 1582 | stack: List[Node] = [] 1583 | node: Optional[Node] = self 1584 | 1585 | while node or stack: 1586 | while node: 1587 | stack.append(node) 1588 | node = node.left 1589 | if stack: 1590 | node = stack.pop() 1591 | result.append(node) 1592 | node = node.right 1593 | 1594 | return result 1595 | 1596 | @property 1597 | def preorder(self) -> List["Node"]: 1598 | """Return the nodes in the binary tree using pre-order_ traversal. 1599 | 1600 | A pre-order_ traversal visits root, left subtree, then right subtree. 1601 | 1602 | .. _pre-order: https://en.wikipedia.org/wiki/Tree_traversal 1603 | 1604 | :return: List of nodes. 1605 | :rtype: [binarytree.Node] 1606 | 1607 | **Example**: 1608 | 1609 | .. doctest:: 1610 | 1611 | >>> from binarytree import Node 1612 | >>> 1613 | >>> root = Node(1) 1614 | >>> root.left = Node(2) 1615 | >>> root.right = Node(3) 1616 | >>> root.left.left = Node(4) 1617 | >>> root.left.right = Node(5) 1618 | >>> 1619 | >>> print(root) 1620 | 1621 | __1 1622 | / \\ 1623 | 2 3 1624 | / \\ 1625 | 4 5 1626 | 1627 | >>> root.preorder 1628 | [Node(1), Node(2), Node(4), Node(5), Node(3)] 1629 | """ 1630 | result: List[Node] = [] 1631 | stack: List[Optional[Node]] = [self] 1632 | 1633 | while stack: 1634 | node = stack.pop() 1635 | if node: 1636 | result.append(node) 1637 | stack.append(node.right) 1638 | stack.append(node.left) 1639 | 1640 | return result 1641 | 1642 | @property 1643 | def postorder(self) -> List["Node"]: 1644 | """Return the nodes in the binary tree using post-order_ traversal. 1645 | 1646 | A post-order_ traversal visits left subtree, right subtree, then root. 1647 | 1648 | .. _post-order: https://en.wikipedia.org/wiki/Tree_traversal 1649 | 1650 | :return: List of nodes. 1651 | :rtype: [binarytree.Node] 1652 | 1653 | **Example**: 1654 | 1655 | .. doctest:: 1656 | 1657 | >>> from binarytree import Node 1658 | >>> 1659 | >>> root = Node(1) 1660 | >>> root.left = Node(2) 1661 | >>> root.right = Node(3) 1662 | >>> root.left.left = Node(4) 1663 | >>> root.left.right = Node(5) 1664 | >>> 1665 | >>> print(root) 1666 | 1667 | __1 1668 | / \\ 1669 | 2 3 1670 | / \\ 1671 | 4 5 1672 | 1673 | >>> root.postorder 1674 | [Node(4), Node(5), Node(2), Node(3), Node(1)] 1675 | """ 1676 | result: List[Node] = [] 1677 | stack: List[Optional[Node]] = [self] 1678 | 1679 | while stack: 1680 | node = stack.pop() 1681 | if node: 1682 | result.append(node) 1683 | stack.append(node.left) 1684 | stack.append(node.right) 1685 | 1686 | return result[::-1] 1687 | 1688 | @property 1689 | def levelorder(self) -> List["Node"]: 1690 | """Return the nodes in the binary tree using level-order_ traversal. 1691 | 1692 | A level-order_ traversal visits nodes left to right, level by level. 1693 | 1694 | .. _level-order: 1695 | https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search 1696 | 1697 | :return: List of nodes. 1698 | :rtype: [binarytree.Node] 1699 | 1700 | **Example**: 1701 | 1702 | .. doctest:: 1703 | 1704 | >>> from binarytree import Node 1705 | >>> 1706 | >>> root = Node(1) 1707 | >>> root.left = Node(2) 1708 | >>> root.right = Node(3) 1709 | >>> root.left.left = Node(4) 1710 | >>> root.left.right = Node(5) 1711 | >>> 1712 | >>> print(root) 1713 | 1714 | __1 1715 | / \\ 1716 | 2 3 1717 | / \\ 1718 | 4 5 1719 | 1720 | >>> root.levelorder 1721 | [Node(1), Node(2), Node(3), Node(4), Node(5)] 1722 | """ 1723 | current_nodes = [self] 1724 | result = [] 1725 | 1726 | while len(current_nodes) > 0: 1727 | next_nodes = [] 1728 | for node in current_nodes: 1729 | result.append(node) 1730 | if node.left is not None: 1731 | next_nodes.append(node.left) 1732 | if node.right is not None: 1733 | next_nodes.append(node.right) 1734 | current_nodes = next_nodes 1735 | 1736 | return result 1737 | 1738 | 1739 | def _is_balanced(root: Optional[Node]) -> int: 1740 | """Return the tree height + 1 if balanced, -1 otherwise. 1741 | 1742 | :param root: Root node of the binary tree. 1743 | :type root: binarytree.Node | None 1744 | :return: Height if the binary tree is balanced, -1 otherwise. 1745 | :rtype: int 1746 | """ 1747 | if root is None: 1748 | return 0 1749 | left = _is_balanced(root.left) 1750 | if left < 0: 1751 | return -1 1752 | right = _is_balanced(root.right) 1753 | if right < 0: 1754 | return -1 1755 | return -1 if abs(left - right) > 1 else max(left, right) + 1 1756 | 1757 | 1758 | def _is_bst(root: Optional[Node]) -> bool: 1759 | """Check if the binary tree is a BST (binary search tree). 1760 | 1761 | :param root: Root node of the binary tree. 1762 | :type root: binarytree.Node | None 1763 | :return: True if the binary tree is a BST, False otherwise. 1764 | :rtype: bool 1765 | """ 1766 | stack: List[Node] = [] 1767 | cur = root 1768 | pre = None 1769 | 1770 | while stack or cur is not None: 1771 | if cur is not None: 1772 | stack.append(cur) 1773 | cur = cur.left 1774 | else: 1775 | node = stack.pop() 1776 | if pre is not None and node.val <= pre.val: 1777 | return False 1778 | pre = node 1779 | cur = node.right 1780 | return True 1781 | 1782 | 1783 | def _is_symmetric(root: Optional[Node]) -> bool: 1784 | """Check if the binary tree is symmetric. 1785 | 1786 | :param root: Root node of the binary tree. 1787 | :type root: binarytree.Node | None 1788 | :return: True if the binary tree is symmetric, False otherwise. 1789 | :rtype: bool 1790 | """ 1791 | 1792 | def symmetric_helper(left: Optional[Node], right: Optional[Node]) -> bool: 1793 | if left is None and right is None: 1794 | return True 1795 | if left is None or right is None: 1796 | return False 1797 | return ( 1798 | left.val == right.val 1799 | and symmetric_helper(left.left, right.right) 1800 | and symmetric_helper(left.right, right.left) 1801 | ) 1802 | 1803 | return symmetric_helper(root, root) 1804 | 1805 | 1806 | def _validate_tree_height(height: int) -> None: 1807 | """Check if the height of the binary tree is valid. 1808 | 1809 | :param height: Height of the binary tree (must be 0 - 9 inclusive). 1810 | :type height: int 1811 | :raise binarytree.exceptions.TreeHeightError: If height is invalid. 1812 | """ 1813 | if not (type(height) == int and 0 <= height <= 9): 1814 | raise TreeHeightError("height must be an int between 0 - 9") 1815 | 1816 | 1817 | def _generate_perfect_bst(height: int) -> Optional[Node]: 1818 | """Generate a perfect BST (binary search tree) and return its root. 1819 | 1820 | :param height: Height of the BST. 1821 | :type height: int 1822 | :return: Root node of the BST. 1823 | :rtype: binarytree.Node | None 1824 | """ 1825 | max_node_count = 2 ** (height + 1) - 1 1826 | node_values = list(range(max_node_count)) 1827 | return _build_bst_from_sorted_values(node_values) 1828 | 1829 | 1830 | def _build_bst_from_sorted_values(sorted_values: List[int]) -> Optional[Node]: 1831 | """Recursively build a perfect BST from odd number of sorted values. 1832 | 1833 | :param sorted_values: Odd number of sorted values. 1834 | :type sorted_values: [float | int | str] 1835 | :return: Root node of the BST. 1836 | :rtype: binarytree.Node | None 1837 | """ 1838 | if len(sorted_values) == 0: 1839 | return None 1840 | mid_index = len(sorted_values) // 2 1841 | root = Node(sorted_values[mid_index]) 1842 | root.left = _build_bst_from_sorted_values(sorted_values[:mid_index]) 1843 | root.right = _build_bst_from_sorted_values(sorted_values[mid_index + 1 :]) 1844 | return root 1845 | 1846 | 1847 | def _generate_random_leaf_count(height: int) -> int: 1848 | """Return a random leaf count for building binary trees. 1849 | 1850 | :param height: Height of the binary tree. 1851 | :type height: int 1852 | :return: Random leaf count. 1853 | :rtype: int 1854 | """ 1855 | max_leaf_count = 2**height 1856 | half_leaf_count = max_leaf_count // 2 1857 | 1858 | # A very naive way of mimicking normal distribution 1859 | roll_1 = random.randint(0, half_leaf_count) 1860 | roll_2 = random.randint(0, max_leaf_count - half_leaf_count) 1861 | return roll_1 + roll_2 or half_leaf_count 1862 | 1863 | 1864 | def number_to_letters(number: int) -> str: 1865 | """Convert a positive integer to a string of uppercase letters. 1866 | 1867 | :param number: A positive integer. 1868 | :type number: int 1869 | :return: String of uppercase letters. 1870 | :rtype: str 1871 | """ 1872 | assert number >= 0, "number must be a positive integer" 1873 | quotient, remainder = divmod(number, 26) 1874 | return quotient * "Z" + chr(65 + remainder) 1875 | 1876 | 1877 | def _generate_random_numbers(height: int) -> List[int]: 1878 | """Return random numbers for building binary trees. 1879 | 1880 | :param height: Height of the binary tree. 1881 | :type height: int 1882 | :return: Randomly generated node values. 1883 | :rtype: [int] 1884 | """ 1885 | max_node_count = 2 ** (height + 1) - 1 1886 | node_values = list(range(max_node_count)) 1887 | random.shuffle(node_values) 1888 | return node_values 1889 | 1890 | 1891 | def _build_tree_string( 1892 | root: Optional[Node], 1893 | curr_index: int, 1894 | include_index: bool = False, 1895 | delimiter: str = "-", 1896 | ) -> Tuple[List[str], int, int, int]: 1897 | """Recursively walk down the binary tree and build a pretty-print string. 1898 | 1899 | In each recursive call, a "box" of characters visually representing the 1900 | current (sub)tree is constructed line by line. Each line is padded with 1901 | whitespaces to ensure all lines in the box have the same length. Then the 1902 | box, its width, and start-end positions of its root node value repr string 1903 | (required for drawing branches) are sent up to the parent call. The parent 1904 | call then combines its left and right sub-boxes to build a larger box etc. 1905 | 1906 | :param root: Root node of the binary tree. 1907 | :type root: binarytree.Node | None 1908 | :param curr_index: Level-order_ index of the current node (root node is 0). 1909 | :type curr_index: int 1910 | :param include_index: If set to True, include the level-order_ node indexes using 1911 | the following format: ``{index}{delimiter}{value}`` (default: False). 1912 | :type include_index: bool 1913 | :param delimiter: Delimiter character between the node index and the node 1914 | value (default: '-'). 1915 | :type delimiter: 1916 | :return: Box of characters visually representing the current subtree, width 1917 | of the box, and start-end positions of the repr string of the new root 1918 | node value. 1919 | :rtype: ([str], int, int, int) 1920 | 1921 | .. _Level-order: 1922 | https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search 1923 | """ 1924 | if root is None: 1925 | return [], 0, 0, 0 1926 | 1927 | line1 = [] 1928 | line2 = [] 1929 | if include_index: 1930 | node_repr = "{}{}{}".format(curr_index, delimiter, root.val) 1931 | else: 1932 | node_repr = str(root.val) 1933 | 1934 | new_root_width = gap_size = len(node_repr) 1935 | 1936 | # Get the left and right sub-boxes, their widths, and root repr positions 1937 | l_box, l_box_width, l_root_start, l_root_end = _build_tree_string( 1938 | root.left, 2 * curr_index + 1, include_index, delimiter 1939 | ) 1940 | r_box, r_box_width, r_root_start, r_root_end = _build_tree_string( 1941 | root.right, 2 * curr_index + 2, include_index, delimiter 1942 | ) 1943 | 1944 | # Draw the branch connecting the current root node to the left sub-box 1945 | # Pad the line with whitespaces where necessary 1946 | if l_box_width > 0: 1947 | l_root = (l_root_start + l_root_end) // 2 + 1 1948 | line1.append(" " * (l_root + 1)) 1949 | line1.append("_" * (l_box_width - l_root)) 1950 | line2.append(" " * l_root + "/") 1951 | line2.append(" " * (l_box_width - l_root)) 1952 | new_root_start = l_box_width + 1 1953 | gap_size += 1 1954 | else: 1955 | new_root_start = 0 1956 | 1957 | # Draw the representation of the current root node 1958 | line1.append(node_repr) 1959 | line2.append(" " * new_root_width) 1960 | 1961 | # Draw the branch connecting the current root node to the right sub-box 1962 | # Pad the line with whitespaces where necessary 1963 | if r_box_width > 0: 1964 | r_root = (r_root_start + r_root_end) // 2 1965 | line1.append("_" * r_root) 1966 | line1.append(" " * (r_box_width - r_root + 1)) 1967 | line2.append(" " * r_root + "\\") 1968 | line2.append(" " * (r_box_width - r_root)) 1969 | gap_size += 1 1970 | new_root_end = new_root_start + new_root_width - 1 1971 | 1972 | # Combine the left and right sub-boxes with the branches drawn above 1973 | gap = " " * gap_size 1974 | new_box = ["".join(line1), "".join(line2)] 1975 | for i in range(max(len(l_box), len(r_box))): 1976 | l_line = l_box[i] if i < len(l_box) else " " * l_box_width 1977 | r_line = r_box[i] if i < len(r_box) else " " * r_box_width 1978 | new_box.append(l_line + gap + r_line) 1979 | 1980 | # Return the new box, its width and its root repr positions 1981 | return new_box, len(new_box[0]), new_root_start, new_root_end 1982 | 1983 | 1984 | def _get_tree_properties(root: Node) -> NodeProperties: 1985 | """Inspect the binary tree and return its properties (e.g. height). 1986 | 1987 | :param root: Root node of the binary tree. 1988 | :type root: binarytree.Node 1989 | :return: Binary tree properties. 1990 | :rtype: binarytree.NodeProperties 1991 | """ 1992 | is_descending = True 1993 | is_ascending = True 1994 | min_node_value = root.val 1995 | max_node_value = root.val 1996 | size = 0 1997 | leaf_count = 0 1998 | min_leaf_depth = 0 1999 | max_leaf_depth = -1 2000 | is_strict = True 2001 | is_complete = True 2002 | current_nodes = [root] 2003 | non_full_node_seen = False 2004 | 2005 | while len(current_nodes) > 0: 2006 | max_leaf_depth += 1 2007 | next_nodes = [] 2008 | 2009 | for node in current_nodes: 2010 | size += 1 2011 | val = node.val 2012 | min_node_value = min(val, min_node_value) 2013 | max_node_value = max(val, max_node_value) 2014 | 2015 | # Node is a leaf. 2016 | if node.left is None and node.right is None: 2017 | if min_leaf_depth == 0: 2018 | min_leaf_depth = max_leaf_depth 2019 | leaf_count += 1 2020 | 2021 | if node.left is not None: 2022 | if node.left.val > val: 2023 | is_descending = False 2024 | elif node.left.val < val: 2025 | is_ascending = False 2026 | next_nodes.append(node.left) 2027 | is_complete = not non_full_node_seen 2028 | else: 2029 | non_full_node_seen = True 2030 | 2031 | if node.right is not None: 2032 | if node.right.val > val: 2033 | is_descending = False 2034 | elif node.right.val < val: 2035 | is_ascending = False 2036 | next_nodes.append(node.right) 2037 | is_complete = not non_full_node_seen 2038 | else: 2039 | non_full_node_seen = True 2040 | 2041 | # If we see a node with only one child, it is not strict 2042 | is_strict &= (node.left is None) == (node.right is None) 2043 | 2044 | current_nodes = next_nodes 2045 | 2046 | return NodeProperties( 2047 | height=max_leaf_depth, 2048 | size=size, 2049 | is_max_heap=is_complete and is_descending, 2050 | is_min_heap=is_complete and is_ascending, 2051 | is_perfect=leaf_count == 2**max_leaf_depth, 2052 | is_strict=is_strict, 2053 | is_complete=is_complete, 2054 | leaf_count=leaf_count, 2055 | min_node_value=min_node_value, 2056 | max_node_value=max_node_value, 2057 | min_leaf_depth=min_leaf_depth, 2058 | max_leaf_depth=max_leaf_depth, 2059 | ) 2060 | 2061 | 2062 | def get_index(root: Node, descendent: Node) -> int: 2063 | """Return the level-order_ index given the root and a possible descendent. 2064 | 2065 | .. _level-order: 2066 | https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search 2067 | 2068 | :return: Level-order index of the descendent relative to the root node. 2069 | :rtype: int 2070 | :raise binarytree.exceptions.NodeTypeError: If root or descendent is 2071 | not an instance of :class:`binarytree.Node`. 2072 | :raise binarytree.exceptions.NodeReferenceError: If given a node that is 2073 | not a root/descendent. 2074 | 2075 | **Example**: 2076 | 2077 | .. doctest:: 2078 | 2079 | >>> from binarytree import Node, get_index 2080 | >>> 2081 | >>> root = Node(1) 2082 | >>> root.left = Node(2) 2083 | >>> root.right = Node(3) 2084 | >>> root.left.right = Node(4) 2085 | >>> 2086 | >>> get_index(root, root.left) 2087 | 1 2088 | >>> get_index(root, root.right) 2089 | 2 2090 | >>> get_index(root, root.left.right) 2091 | 4 2092 | >>> get_index(root.left, root.right) 2093 | Traceback (most recent call last): 2094 | ... 2095 | binarytree.exceptions.NodeReferenceError: given nodes are not in the same tree 2096 | """ 2097 | if root is None: 2098 | raise NodeTypeError("root must be a Node instance") 2099 | 2100 | if descendent is None: 2101 | raise NodeTypeError("descendent must be a Node instance") 2102 | 2103 | current_nodes: List[Optional[Node]] = [root] 2104 | current_index = 0 2105 | has_more_nodes = True 2106 | 2107 | while has_more_nodes: 2108 | has_more_nodes = False 2109 | next_nodes: List[Optional[Node]] = [] 2110 | 2111 | for node in current_nodes: 2112 | if node is not None and node is descendent: 2113 | return current_index 2114 | 2115 | if node is None: 2116 | next_nodes.append(None) 2117 | next_nodes.append(None) 2118 | else: 2119 | next_nodes.append(node.left) 2120 | next_nodes.append(node.right) 2121 | if node.left is not None or node.right is not None: 2122 | has_more_nodes = True 2123 | 2124 | current_index += 1 2125 | 2126 | current_nodes = next_nodes 2127 | 2128 | raise NodeReferenceError("given nodes are not in the same tree") 2129 | 2130 | 2131 | def get_parent(root: Optional[Node], child: Optional[Node]) -> Optional[Node]: 2132 | """Search the binary tree and return the parent of given child. 2133 | 2134 | :param root: Root node of the binary tree. 2135 | :type: binarytree.Node | None 2136 | :param child: Child node. 2137 | :rtype: binarytree.Node | None 2138 | :return: Parent node, or None if missing. 2139 | :rtype: binarytree.Node | None 2140 | 2141 | **Example**: 2142 | 2143 | .. doctest:: 2144 | 2145 | >>> from binarytree import Node, get_parent 2146 | >>> 2147 | >>> root = Node(1) 2148 | >>> root.left = Node(2) 2149 | >>> root.right = Node(3) 2150 | >>> root.left.right = Node(4) 2151 | >>> 2152 | >>> print(root) 2153 | 2154 | __1 2155 | / \\ 2156 | 2 3 2157 | \\ 2158 | 4 2159 | 2160 | >>> print(get_parent(root, root.left.right)) 2161 | 2162 | 2 2163 | \\ 2164 | 4 2165 | 2166 | """ 2167 | if child is None: 2168 | return None 2169 | 2170 | stack: List[Optional[Node]] = [root] 2171 | 2172 | while stack: 2173 | node = stack.pop() 2174 | if node: 2175 | if node.left is child or node.right is child: 2176 | return node 2177 | else: 2178 | stack.append(node.left) 2179 | stack.append(node.right) 2180 | return None 2181 | 2182 | 2183 | def build(values: NodeValueList) -> Optional[Node]: 2184 | """Build a tree from `list representation`_ and return its root node. 2185 | 2186 | .. _list representation: 2187 | https://en.wikipedia.org/wiki/Binary_tree#Arrays 2188 | 2189 | :param values: List representation of the binary tree, which is a list of 2190 | node values in breadth-first order starting from the root (current 2191 | node). If a node is at index i, its left child is always at 2i + 1, 2192 | right child at 2i + 2, and parent at floor((i - 1) / 2). "None" indicates 2193 | absence of a node at that index. See example below for an illustration. 2194 | :type values: [float | int | str | None] 2195 | :return: Root node of the binary tree. 2196 | :rtype: binarytree.Node | None 2197 | :raise binarytree.exceptions.NodeNotFoundError: If the list representation 2198 | is malformed (e.g. a parent node is missing). 2199 | 2200 | **Example**: 2201 | 2202 | .. doctest:: 2203 | 2204 | >>> from binarytree import build 2205 | >>> 2206 | >>> root = build([1, 2, 3, None, 4]) 2207 | >>> 2208 | >>> print(root) 2209 | 2210 | __1 2211 | / \\ 2212 | 2 3 2213 | \\ 2214 | 4 2215 | 2216 | 2217 | .. doctest:: 2218 | 2219 | >>> from binarytree import build 2220 | >>> 2221 | >>> root = build([None, 2, 3]) # doctest: +IGNORE_EXCEPTION_DETAIL 2222 | Traceback (most recent call last): 2223 | ... 2224 | binarytree.exceptions.NodeNotFoundError: parent node missing at index 0 2225 | """ 2226 | nodes = [None if v is None else Node(v) for v in values] 2227 | 2228 | for index in range(1, len(nodes)): 2229 | node = nodes[index] 2230 | if node is not None: 2231 | parent_index = (index - 1) // 2 2232 | parent = nodes[parent_index] 2233 | if parent is None: 2234 | raise NodeNotFoundError( 2235 | "parent node missing at index {}".format(parent_index) 2236 | ) 2237 | setattr(parent, _ATTR_LEFT if index % 2 else _ATTR_RIGHT, node) 2238 | 2239 | return nodes[0] if nodes else None 2240 | 2241 | 2242 | def build2(values: List[NodeValue]) -> Optional[Node]: 2243 | """Build a tree from a list of values and return its root node. 2244 | 2245 | :param values: List of node values like those for :func:`binarytree.build`, but 2246 | with a slightly different representation which associates two adjacent child 2247 | values with the first parent value that has not been associated yet. This 2248 | representation does not provide the same indexing properties where if a node 2249 | is at index i, its left child is always at 2i + 1, right child at 2i + 2, and 2250 | parent at floor((i - 1) / 2), but it allows for more compact lists as it 2251 | does not hold "None"s between nodes in each level. See example below for an 2252 | illustration. 2253 | :type values: [float | int | str | None] 2254 | :return: Root node of the binary tree. 2255 | :rtype: binarytree.Node | None 2256 | :raise binarytree.exceptions.NodeNotFoundError: If the list representation 2257 | is malformed (e.g. a parent node is missing). 2258 | 2259 | **Example**: 2260 | 2261 | .. doctest:: 2262 | 2263 | >>> from binarytree import build2 2264 | >>> 2265 | >>> root = build2([2, 5, None, 3, None, 1, 4]) 2266 | >>> 2267 | >>> print(root) 2268 | 2269 | 2 2270 | / 2271 | __5 2272 | / 2273 | 3 2274 | / \\ 2275 | 1 4 2276 | 2277 | 2278 | .. doctest:: 2279 | 2280 | >>> from binarytree import build2 2281 | >>> 2282 | >>> root = build2([None, 1, 2]) # doctest: +IGNORE_EXCEPTION_DETAIL 2283 | Traceback (most recent call last): 2284 | ... 2285 | binarytree.exceptions.NodeValueError: node value must be a float/int/str 2286 | """ 2287 | queue: Deque[Node] = deque() 2288 | root: Optional[Node] = None 2289 | 2290 | if values: 2291 | root = Node(values[0]) 2292 | queue.append(root) 2293 | 2294 | index = 1 2295 | while index < len(values): 2296 | node = queue.popleft() 2297 | 2298 | if values[index] is not None: 2299 | node.left = Node(values[index]) 2300 | queue.append(node.left) 2301 | index += 1 2302 | 2303 | if index < len(values) and values[index] is not None: 2304 | node.right = Node(values[index]) 2305 | queue.append(node.right) 2306 | index += 1 2307 | 2308 | return root 2309 | 2310 | 2311 | def tree( 2312 | height: int = 3, 2313 | is_perfect: bool = False, 2314 | letters: bool = False, 2315 | ) -> Optional[Node]: 2316 | """Generate a random binary tree and return its root node. 2317 | 2318 | :param height: Height of the tree (default: 3, range: 0 - 9 inclusive). 2319 | :type height: int 2320 | :param is_perfect: If set to True (default: False), a perfect binary tree 2321 | with all levels filled is returned. If set to False, a perfect binary 2322 | tree may still be generated by chance. 2323 | :type is_perfect: bool 2324 | :param letters: If set to True (default: False), uppercase alphabet letters are 2325 | used for node values instead of numbers. 2326 | :type letters: bool 2327 | :return: Root node of the binary tree. 2328 | :rtype: binarytree.Node 2329 | :raise binarytree.exceptions.TreeHeightError: If height is invalid. 2330 | 2331 | **Example**: 2332 | 2333 | .. doctest:: 2334 | 2335 | >>> from binarytree import tree 2336 | >>> 2337 | >>> root = tree() 2338 | >>> 2339 | >>> root.height 2340 | 3 2341 | 2342 | .. doctest:: 2343 | 2344 | >>> from binarytree import tree 2345 | >>> 2346 | >>> root = tree(height=5, is_perfect=True) 2347 | >>> 2348 | >>> root.height 2349 | 5 2350 | >>> root.is_perfect 2351 | True 2352 | 2353 | .. doctest:: 2354 | 2355 | >>> from binarytree import tree 2356 | >>> 2357 | >>> root = tree(height=20) # doctest: +IGNORE_EXCEPTION_DETAIL 2358 | Traceback (most recent call last): 2359 | ... 2360 | binarytree.exceptions.TreeHeightError: height must be an int between 0 - 9 2361 | """ 2362 | _validate_tree_height(height) 2363 | numbers = _generate_random_numbers(height) 2364 | values: NodeValueList = ( 2365 | list(map(number_to_letters, numbers)) if letters else numbers 2366 | ) 2367 | 2368 | if is_perfect: 2369 | return build(values) 2370 | 2371 | leaf_count = _generate_random_leaf_count(height) 2372 | root_node = Node(values.pop(0)) 2373 | leaves = set() 2374 | 2375 | for value in values: 2376 | node = root_node 2377 | depth = 0 2378 | inserted = False 2379 | 2380 | while depth < height and not inserted: 2381 | attr = random.choice((_ATTR_LEFT, _ATTR_RIGHT)) 2382 | if getattr(node, attr) is None: 2383 | setattr(node, attr, Node(value)) 2384 | inserted = True 2385 | node = getattr(node, attr) 2386 | depth += 1 2387 | 2388 | if inserted and depth == height: 2389 | leaves.add(node) 2390 | if len(leaves) == leaf_count: 2391 | break 2392 | 2393 | return root_node 2394 | 2395 | 2396 | def bst( 2397 | height: int = 3, 2398 | is_perfect: bool = False, 2399 | letters: bool = False, 2400 | ) -> Optional[Node]: 2401 | """Generate a random BST (binary search tree) and return its root node. 2402 | 2403 | :param height: Height of the BST (default: 3, range: 0 - 9 inclusive). 2404 | :type height: int 2405 | :param is_perfect: If set to True (default: False), a perfect BST with all 2406 | levels filled is returned. If set to False, a perfect BST may still be 2407 | generated by chance. 2408 | :type is_perfect: bool 2409 | :param letters: If set to True (default: False), uppercase alphabet letters are 2410 | used for node values instead of numbers. 2411 | :type letters: bool 2412 | :return: Root node of the BST. 2413 | :rtype: binarytree.Node 2414 | :raise binarytree.exceptions.TreeHeightError: If height is invalid. 2415 | 2416 | **Example**: 2417 | 2418 | .. doctest:: 2419 | 2420 | >>> from binarytree import bst 2421 | >>> 2422 | >>> root = bst() 2423 | >>> 2424 | >>> root.height 2425 | 3 2426 | >>> root.is_bst 2427 | True 2428 | 2429 | .. doctest:: 2430 | 2431 | >>> from binarytree import bst 2432 | >>> 2433 | >>> root = bst(10) # doctest: +IGNORE_EXCEPTION_DETAIL 2434 | Traceback (most recent call last): 2435 | ... 2436 | binarytree.exceptions.TreeHeightError: height must be an int between 0 - 9 2437 | """ 2438 | _validate_tree_height(height) 2439 | if is_perfect: 2440 | return _generate_perfect_bst(height) 2441 | 2442 | numbers = _generate_random_numbers(height) 2443 | values: NodeValueList = ( 2444 | list(map(number_to_letters, numbers)) if letters else numbers 2445 | ) 2446 | leaf_count = _generate_random_leaf_count(height) 2447 | 2448 | root_node = Node(values.pop(0)) 2449 | leaves = set() 2450 | 2451 | for value in values: 2452 | node = root_node 2453 | depth = 0 2454 | inserted = False 2455 | 2456 | while depth < height and not inserted: 2457 | attr = _ATTR_LEFT if node.val > value else _ATTR_RIGHT 2458 | if getattr(node, attr) is None: 2459 | setattr(node, attr, Node(value)) 2460 | inserted = True 2461 | node = getattr(node, attr) 2462 | depth += 1 2463 | 2464 | if inserted and depth == height: 2465 | leaves.add(node) 2466 | if len(leaves) == leaf_count: 2467 | break 2468 | 2469 | return root_node 2470 | 2471 | 2472 | def heap( 2473 | height: int = 3, 2474 | is_max: bool = True, 2475 | is_perfect: bool = False, 2476 | letters: bool = False, 2477 | ) -> Optional[Node]: 2478 | """Generate a random heap and return its root node. 2479 | 2480 | :param height: Height of the heap (default: 3, range: 0 - 9 inclusive). 2481 | :type height: int 2482 | :param is_max: If set to True (default: True), generate a max heap. If set 2483 | to False, generate a min heap. A binary tree with only the root node is 2484 | considered both a min and max heap. 2485 | :type is_max: bool 2486 | :param is_perfect: If set to True (default: False), a perfect heap with all 2487 | levels filled is returned. If set to False, a perfect heap may still be 2488 | generated by chance. 2489 | :type is_perfect: bool 2490 | :param letters: If set to True (default: False), uppercase alphabet letters are 2491 | used for node values instead of numbers. 2492 | :type letters: bool 2493 | :return: Root node of the heap. 2494 | :rtype: binarytree.Node 2495 | :raise binarytree.exceptions.TreeHeightError: If height is invalid. 2496 | 2497 | **Example**: 2498 | 2499 | .. doctest:: 2500 | 2501 | >>> from binarytree import heap 2502 | >>> 2503 | >>> root = heap() 2504 | >>> 2505 | >>> root.height 2506 | 3 2507 | >>> root.is_max_heap 2508 | True 2509 | 2510 | .. doctest:: 2511 | 2512 | >>> from binarytree import heap 2513 | >>> 2514 | >>> root = heap(4, is_max=False) 2515 | >>> 2516 | >>> root.height 2517 | 4 2518 | >>> root.is_min_heap 2519 | True 2520 | 2521 | .. doctest:: 2522 | 2523 | >>> from binarytree import heap 2524 | >>> 2525 | >>> root = heap(5, is_max=False, is_perfect=True) 2526 | >>> 2527 | >>> root.height 2528 | 5 2529 | >>> root.is_min_heap 2530 | True 2531 | >>> root.is_perfect 2532 | True 2533 | 2534 | .. doctest:: 2535 | 2536 | >>> from binarytree import heap 2537 | >>> 2538 | >>> root = heap(-1) # doctest: +IGNORE_EXCEPTION_DETAIL 2539 | Traceback (most recent call last): 2540 | ... 2541 | binarytree.exceptions.TreeHeightError: height must be an int between 0 - 9 2542 | """ 2543 | _validate_tree_height(height) 2544 | values = _generate_random_numbers(height) 2545 | 2546 | if not is_perfect: 2547 | # Randomly cut some leaf nodes away 2548 | random_cut = random.randint(2**height, len(values)) 2549 | values = values[:random_cut] 2550 | 2551 | if is_max: 2552 | negated = [-v for v in values] 2553 | heapq.heapify(negated) 2554 | values = [-v for v in negated] 2555 | else: 2556 | heapq.heapify(values) 2557 | 2558 | return build(list(map(number_to_letters, values)) if letters else values) 2559 | -------------------------------------------------------------------------------- /binarytree/exceptions.py: -------------------------------------------------------------------------------- 1 | class BinaryTreeError(Exception): 2 | """Base (catch-all) binarytree exception.""" 3 | 4 | 5 | class NodeIndexError(BinaryTreeError): 6 | """Node index was invalid.""" 7 | 8 | 9 | class NodeModifyError(BinaryTreeError): 10 | """User tried to overwrite or delete the root node.""" 11 | 12 | 13 | class NodeNotFoundError(BinaryTreeError): 14 | """Node was missing from the binary tree.""" 15 | 16 | 17 | class NodeReferenceError(BinaryTreeError): 18 | """Node reference was invalid (e.g. cyclic reference).""" 19 | 20 | 21 | class NodeTypeError(BinaryTreeError): 22 | """Node was not an instance of :class:`binarytree.Node`.""" 23 | 24 | 25 | class NodeValueError(BinaryTreeError): 26 | """Node value was not a number (e.g. float, int, str).""" 27 | 28 | 29 | class TreeHeightError(BinaryTreeError): 30 | """Tree height was invalid.""" 31 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | project = "binarytree" 2 | copyright = "2016-2021, Joohwan Oh" 3 | author = "Joohwan Oh" 4 | extensions = [ 5 | "sphinx_rtd_theme", 6 | "sphinx.ext.autodoc", 7 | "sphinx.ext.doctest", 8 | "sphinx.ext.viewcode", 9 | ] 10 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 11 | html_theme = "sphinx_rtd_theme" 12 | master_doc = "index" 13 | -------------------------------------------------------------------------------- /docs/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions 2 | ---------- 3 | 4 | This page contains exceptions raised by **binarytree**: 5 | 6 | .. automodule:: binarytree.exceptions 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/graphviz.rst: -------------------------------------------------------------------------------- 1 | Graphviz and Jupyter Notebook 2 | ----------------------------- 3 | 4 | From version 6.0.0, binarytree can integrate with Graphviz_ to render trees in image 5 | viewers, browsers and Jupyter notebooks using the python-graphviz_ library. 6 | 7 | In order to use this feature, you must first install the Graphviz software in your OS 8 | and ensure its executables are on your PATH system variable (usually done automatically 9 | during installation): 10 | 11 | .. code-block:: bash 12 | 13 | # Ubuntu and Debian 14 | $ sudo apt install graphviz 15 | 16 | # Fedora and CentOS 17 | $ sudo yum install graphviz 18 | 19 | # Windows using choco (or winget) 20 | $ choco install graphviz 21 | 22 | Use :func:`binarytree.Node.graphviz` to generate `graphviz.Digraph`_ objects: 23 | 24 | .. code-block:: python 25 | 26 | from binarytree import tree 27 | 28 | t = tree() 29 | 30 | # Generate a graphviz.Digraph object 31 | # Arguments to this method are passed into Digraph.__init__ 32 | graph = t.graphviz() 33 | 34 | # Get DOT (graph description language) body 35 | graph.body 36 | 37 | # Render the binary tree 38 | graph.render() 39 | 40 | With Graphviz you can also visualize binary trees in `Jupyter notebooks`_: 41 | 42 | .. image:: https://user-images.githubusercontent.com/2701938/107016813-3c818600-6753-11eb-8140-6b7a95791c08.gif 43 | :alt: Jupyter Notebook GIF 44 | 45 | .. _DOT: https://graphviz.org/doc/info/lang.html 46 | .. _Graphviz: https://graphviz.org 47 | .. _python-graphviz: https://pypi.org/project/graphviz 48 | .. _graphviz.Digraph: https://graphviz.readthedocs.io/en/stable/api.html#digraph 49 | .. _Jupyter notebooks: https://jupyter.org 50 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Binarytree: Python Library for Studying Binary Trees 2 | ---------------------------------------------------- 3 | 4 | .. image:: https://img.shields.io/github/license/joowani/binarytree?color=bright 5 | :alt: GitHub License 6 | :target: https://github.com/joowani/binarytree/blob/main/LICENSE 7 | 8 | .. image:: https://img.shields.io/badge/python-3.7%2B-blue 9 | :alt: Python Versions 10 | 11 | Welcome to the documentation for **binarytree**. 12 | 13 | **Binarytree** is Python library which lets you generate, visualize, inspect and 14 | manipulate `binary trees`_. Skip the tedious work of setting up test data, and dive 15 | straight into practising algorithms. Heaps_ and `binary search trees`_ are also 16 | supported. Self-balancing search trees like `red-black`_ or `AVL`_ will be added in 17 | the future. 18 | 19 | .. _binary trees: https://en.wikipedia.org/wiki/Binary_tree 20 | .. _Heaps: https://en.wikipedia.org/wiki/Heap_(data_structure) 21 | .. _binary search trees: https://en.wikipedia.org/wiki/Binary_search_tree 22 | .. _red-black: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree 23 | .. _AVL: https://en.wikipedia.org/wiki/AVL_tree 24 | 25 | Requirements 26 | ============ 27 | 28 | Python 3.7+ 29 | 30 | Installation 31 | ============ 32 | 33 | Install via pip_: 34 | 35 | .. code-block:: bash 36 | 37 | pip install binarytree --upgrade 38 | 39 | For conda_ users: 40 | 41 | .. code-block:: bash 42 | 43 | conda install binarytree -c conda-forge 44 | 45 | .. _pip: https://pip.pypa.io 46 | .. _conda: https://docs.conda.io 47 | 48 | Contents 49 | ======== 50 | 51 | .. toctree:: 52 | :maxdepth: 1 53 | 54 | overview 55 | specs 56 | exceptions 57 | graphviz 58 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | Overview 2 | -------- 3 | 4 | Binarytree uses the following class to represent a node: 5 | 6 | .. testcode:: 7 | 8 | class Node: 9 | 10 | def __init__(self, value, left=None, right=None): 11 | self.value = value # The node value (float/int/str) 12 | self.left = left # Left child 13 | self.right = right # Right child 14 | 15 | Generate and pretty-print various types of binary trees: 16 | 17 | .. code-block:: python 18 | 19 | >>> from binarytree import tree, bst, heap 20 | >>> 21 | >>> # Generate a random binary tree and return its root node. 22 | >>> my_tree = tree(height=3, is_perfect=False) 23 | >>> 24 | >>> # Generate a random BST and return its root node. 25 | >>> my_bst = bst(height=3, is_perfect=True) 26 | >>> 27 | >>> # Generate a random max heap and return its root node. 28 | >>> my_heap = heap(height=3, is_max=True, is_perfect=False) 29 | >>> 30 | >>> # Pretty-print the trees in stdout. 31 | >>> print(my_tree) 32 | 33 | _______1_____ 34 | / \ 35 | 4__ ___3 36 | / \ / \ 37 | 0 9 13 14 38 | / \ \ 39 | 7 10 2 40 | 41 | >>> print(my_bst) 42 | 43 | ______7_______ 44 | / \ 45 | __3__ ___11___ 46 | / \ / \ 47 | 1 5 9 _13 48 | / \ / \ / \ / \ 49 | 0 2 4 6 8 10 12 14 50 | 51 | >>> print(my_heap) 52 | 53 | _____14__ 54 | / \ 55 | ____13__ 9 56 | / \ / \ 57 | 12 7 3 8 58 | / \ / 59 | 0 10 6 60 | 61 | Generate trees with letter values instead of numbers: 62 | 63 | .. code-block:: python 64 | 65 | >>> from binarytree import tree 66 | 67 | >>> my_tree = tree(height=3, is_perfect=False, letters=True) 68 | 69 | >>> print(my_tree) 70 | 71 | ____H____ 72 | / \ 73 | __E__ F__ 74 | / \ / \ 75 | M G J B 76 | \ / / / \ 77 | O L D I A 78 | 79 | Build your own trees: 80 | 81 | .. doctest:: 82 | 83 | >>> from binarytree import Node 84 | >>> 85 | >>> root = Node(1) 86 | >>> root.left = Node(2) 87 | >>> root.right = Node(3) 88 | >>> root.left.right = Node(4) 89 | >>> 90 | >>> print(root) 91 | 92 | __1 93 | / \ 94 | 2 3 95 | \ 96 | 4 97 | 98 | 99 | Inspect tree properties: 100 | 101 | .. doctest:: 102 | 103 | >>> from binarytree import Node 104 | >>> 105 | >>> root = Node(1) 106 | >>> root.left = Node(2) 107 | >>> root.right = Node(3) 108 | >>> root.left.left = Node(4) 109 | >>> root.left.right = Node(5) 110 | >>> 111 | >>> print(root) 112 | 113 | __1 114 | / \ 115 | 2 3 116 | / \ 117 | 4 5 118 | 119 | >>> root.height 120 | 2 121 | >>> root.is_balanced 122 | True 123 | >>> root.is_bst 124 | False 125 | >>> root.is_complete 126 | True 127 | >>> root.is_max_heap 128 | False 129 | >>> root.is_min_heap 130 | True 131 | >>> root.is_perfect 132 | False 133 | >>> root.is_strict 134 | True 135 | >>> root.leaf_count 136 | 3 137 | >>> root.max_leaf_depth 138 | 2 139 | >>> root.max_node_value 140 | 5 141 | >>> root.min_leaf_depth 142 | 1 143 | >>> root.min_node_value 144 | 1 145 | >>> root.size 146 | 5 147 | 148 | >>> properties = root.properties # Get all properties at once. 149 | >>> properties['height'] 150 | 2 151 | >>> properties['is_balanced'] 152 | True 153 | >>> properties['max_leaf_depth'] 154 | 2 155 | 156 | >>> root.leaves 157 | [Node(3), Node(4), Node(5)] 158 | 159 | >>> root.levels 160 | [[Node(1)], [Node(2), Node(3)], [Node(4), Node(5)]] 161 | 162 | Compare and clone trees: 163 | 164 | .. doctest:: 165 | 166 | >>> from binarytree import tree 167 | >>> original = tree() 168 | >>> clone = original.clone() 169 | >>> original.equals(clone) 170 | True 171 | 172 | Use `level-order (breadth-first)`_ indexes to manipulate nodes: 173 | 174 | .. _level-order (breadth-first): 175 | https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search 176 | 177 | .. doctest:: 178 | 179 | >>> from binarytree import Node 180 | >>> 181 | >>> root = Node(1) # index: 0, value: 1 182 | >>> root.left = Node(2) # index: 1, value: 2 183 | >>> root.right = Node(3) # index: 2, value: 3 184 | >>> root.left.right = Node(4) # index: 4, value: 4 185 | >>> root.left.right.left = Node(5) # index: 9, value: 5 186 | >>> 187 | >>> print(root) 188 | 189 | ____1 190 | / \ 191 | 2__ 3 192 | \ 193 | 4 194 | / 195 | 5 196 | 197 | >>> # Use binarytree.Node.pprint instead of print to display indexes. 198 | >>> root.pprint(index=True) 199 | 200 | _________0-1_ 201 | / \ 202 | 1-2_____ 2-3 203 | \ 204 | _4-4 205 | / 206 | 9-5 207 | 208 | >>> # Return the node/subtree at index 9. 209 | >>> root[9] 210 | Node(5) 211 | 212 | >>> # Replace the node/subtree at index 4. 213 | >>> root[4] = Node(6, left=Node(7), right=Node(8)) 214 | >>> root.pprint(index=True) 215 | 216 | ______________0-1_ 217 | / \ 218 | 1-2_____ 2-3 219 | \ 220 | _4-6_ 221 | / \ 222 | 9-7 10-8 223 | 224 | >>> # Delete the node/subtree at index 1. 225 | >>> del root[1] 226 | >>> root.pprint(index=True) 227 | 228 | 0-1_ 229 | \ 230 | 2-3 231 | 232 | 233 | Traverse trees using different algorithms: 234 | 235 | .. doctest:: 236 | 237 | >>> from binarytree import Node 238 | >>> 239 | >>> root = Node(1) 240 | >>> root.left = Node(2) 241 | >>> root.right = Node(3) 242 | >>> root.left.left = Node(4) 243 | >>> root.left.right = Node(5) 244 | >>> 245 | >>> print(root) 246 | 247 | __1 248 | / \ 249 | 2 3 250 | / \ 251 | 4 5 252 | 253 | >>> root.inorder 254 | [Node(4), Node(2), Node(5), Node(1), Node(3)] 255 | 256 | >>> root.preorder 257 | [Node(1), Node(2), Node(4), Node(5), Node(3)] 258 | 259 | >>> root.postorder 260 | [Node(4), Node(5), Node(2), Node(3), Node(1)] 261 | 262 | >>> root.levelorder 263 | [Node(1), Node(2), Node(3), Node(4), Node(5)] 264 | 265 | >>> list(root) # Equivalent to root.levelorder. 266 | [Node(1), Node(2), Node(3), Node(4), Node(5)] 267 | 268 | Convert to `List representations`_: 269 | 270 | .. _List representations: 271 | https://en.wikipedia.org/wiki/Binary_tree#Arrays 272 | 273 | .. doctest:: 274 | 275 | >>> from binarytree import build 276 | >>> 277 | >>> # Build a tree from list representation. 278 | >>> values = [7, 3, 2, 6, 9, None, 1, 5, 8] 279 | >>> root = build(values) 280 | >>> print(root) 281 | 282 | __7 283 | / \ 284 | __3 2 285 | / \ \ 286 | 6 9 1 287 | / \ 288 | 5 8 289 | 290 | >>> # Convert the tree back to list representation. 291 | >>> root.values 292 | [7, 3, 2, 6, 9, None, 1, 5, 8] 293 | 294 | Binarytree supports another representation which is more compact but without 295 | the `indexing properties`_ (this method is often used in Leetcode_): 296 | 297 | .. _indexing properties: https://en.wikipedia.org/wiki/Binary_tree#Arrays 298 | .. _Leetcode: https://leetcode.com/ 299 | 300 | .. doctest:: 301 | 302 | >>> from binarytree import build, build2, Node 303 | >>> 304 | >>> # First let's create an example tree. 305 | >>> root = Node(1) 306 | >>> root.left = Node(2) 307 | >>> root.left.left = Node(3) 308 | >>> root.left.left.left = Node(4) 309 | >>> root.left.left.right = Node(5) 310 | >>> print(root) 311 | 312 | 1 313 | / 314 | __2 315 | / 316 | 3 317 | / \ 318 | 4 5 319 | 320 | 321 | >>> # First representation is already shown above. 322 | >>> # All "null" nodes in each level are present. 323 | >>> root.values 324 | [1, 2, None, 3, None, None, None, 4, 5] 325 | 326 | >>> # Second representation is more compact but without the indexing properties. 327 | >>> root.values2 328 | [1, 2, None, 3, None, 4, 5] 329 | 330 | >>> # Build trees from both list representations. 331 | >>> tree1 = build(root.values) 332 | >>> tree2 = build2(root.values2) 333 | >>> tree1.equals(tree2) 334 | True 335 | 336 | See :doc:`specs` for more details. 337 | -------------------------------------------------------------------------------- /docs/specs.rst: -------------------------------------------------------------------------------- 1 | API Specification 2 | ----------------- 3 | 4 | This page covers the API specification for the following classes and utility 5 | functions: 6 | 7 | * :class:`binarytree.Node` 8 | * :func:`binarytree.build` 9 | * :func:`binarytree.build2` 10 | * :func:`binarytree.tree` 11 | * :func:`binarytree.bst` 12 | * :func:`binarytree.heap` 13 | * :func:`binarytree.get_parent` 14 | 15 | Class: binarytree.Node 16 | ====================== 17 | 18 | .. autoclass:: binarytree.Node 19 | :members: 20 | :special-members: 21 | :private-members: 22 | :exclude-members: __weakref__, __repr__, __setattr__, __init__ 23 | 24 | Function: binarytree.build 25 | ========================== 26 | 27 | .. autofunction:: binarytree.build 28 | 29 | Function: binarytree.build2 30 | =========================== 31 | 32 | .. autofunction:: binarytree.build2 33 | 34 | Function: binarytree.tree 35 | ========================= 36 | 37 | .. autofunction:: binarytree.tree 38 | 39 | Function: binarytree.bst 40 | ======================== 41 | 42 | .. autofunction:: binarytree.bst 43 | 44 | Function: binarytree.heap 45 | ========================= 46 | 47 | .. autofunction:: binarytree.heap 48 | 49 | Function: binarytree.get_index 50 | =============================== 51 | 52 | .. autofunction:: binarytree.get_index 53 | 54 | Function: binarytree.get_parent 55 | =============================== 56 | 57 | .. autofunction:: binarytree.get_parent 58 | -------------------------------------------------------------------------------- /gifs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joowani/binarytree/74e0c0bf204a0a2789c45a07264718f963db37fe/gifs/demo.gif -------------------------------------------------------------------------------- /gifs/jupyter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joowani/binarytree/74e0c0bf204a0a2789c45a07264718f963db37fe/gifs/jupyter.gif -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.coverage.run] 2 | omit = ["binarytree/version.py"] 3 | 4 | [tool.isort] 5 | profile = "black" 6 | 7 | [tool.pytest.ini_options] 8 | addopts = "-s -vv -p no:warnings" 9 | minversion = "6.0" 10 | testpaths = ["tests"] 11 | 12 | [tool.setuptools_scm] 13 | write_to = "binarytree/version.py" 14 | 15 | [tool.mypy] 16 | warn_return_any = true 17 | warn_unused_configs = true 18 | ignore_missing_imports = true 19 | strict = true 20 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E203, E741, W503 4 | exclude =.git .idea .*_cache dist htmlcov venv 5 | per-file-ignores = __init__.py:F401 conf.py:E402 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | with open("README.md") as fp: 4 | description = fp.read() 5 | 6 | setup( 7 | name="binarytree", 8 | description="Python Library for Studying Binary Trees", 9 | long_description=description, 10 | long_description_content_type="text/markdown", 11 | author="Joohwan Oh", 12 | author_email="joohwan.oh@outlook.com", 13 | url="https://github.com/joowani/binarytree", 14 | keywords=["tree", "heap", "bst", "education"], 15 | packages=find_packages(exclude=["tests"]), 16 | include_package_data=True, 17 | python_requires=">=3.7", 18 | license="MIT", 19 | use_scm_version=True, 20 | setup_requires=["setuptools_scm"], 21 | install_requires=[ 22 | "graphviz", 23 | "setuptools>=60.8.2", 24 | "setuptools_scm[toml]>=5.0.1", 25 | ], 26 | extras_require={ 27 | "dev": [ 28 | "black>=22.1.0", 29 | "flake8>=4.0.1", 30 | "isort>=5.10.1", 31 | "mypy>=0.931", 32 | "pre-commit>=2.17.0", 33 | "pytest>=6.2.1", 34 | "pytest-cov>=2.10.1", 35 | "sphinx", 36 | "sphinx_rtd_theme", 37 | "types-setuptools", 38 | "types-dataclasses", 39 | ], 40 | }, 41 | classifiers=[ 42 | "Intended Audience :: Developers", 43 | "License :: OSI Approved :: MIT License", 44 | "Natural Language :: English", 45 | "Operating System :: MacOS", 46 | "Operating System :: Microsoft :: Windows", 47 | "Operating System :: Unix", 48 | "Programming Language :: Python :: 3", 49 | "Topic :: Documentation :: Sphinx", 50 | "Topic :: Education", 51 | ], 52 | ) 53 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joowani/binarytree/74e0c0bf204a0a2789c45a07264718f963db37fe/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_tree.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import copy 4 | import random 5 | from typing import Any, List, Optional 6 | 7 | import pytest 8 | 9 | from binarytree import ( 10 | Node, 11 | bst, 12 | build, 13 | build2, 14 | get_index, 15 | get_parent, 16 | heap, 17 | number_to_letters, 18 | tree, 19 | ) 20 | from binarytree.exceptions import ( 21 | NodeIndexError, 22 | NodeModifyError, 23 | NodeNotFoundError, 24 | NodeReferenceError, 25 | NodeTypeError, 26 | NodeValueError, 27 | TreeHeightError, 28 | ) 29 | from tests.utils import builtin_print, pprint_default, pprint_with_index 30 | 31 | REPETITIONS = 20 32 | 33 | EXPECTED_SVG_XML_SINGLE_NODE = """ 34 | 35 | 47 | 48 | 49 | 0 50 | 51 | 52 | """ 53 | 54 | EXPECTED_SVG_XML_MULTIPLE_NODES = """ 55 | 56 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 0 75 | 76 | 1 77 | 78 | 2 79 | 80 | 3 81 | 82 | 4 83 | 84 | 85 | """ 86 | EMPTY_LIST: List[Optional[int]] = [] 87 | 88 | 89 | def test_node_init_and_setattr_with_integers() -> None: 90 | root = Node(1) 91 | assert root.left is None 92 | assert root.right is None 93 | assert root.val == 1 94 | assert root.value == 1 95 | assert repr(root) == "Node(1)" 96 | 97 | root.value = 2 98 | assert root.value == 2 99 | assert root.val == 2 100 | assert repr(root) == "Node(2)" 101 | 102 | root.val = 1 103 | assert root.value == 1 104 | assert root.val == 1 105 | assert repr(root) == "Node(1)" 106 | 107 | left_child = Node(2) 108 | root.left = left_child 109 | assert root.left is left_child 110 | assert root.right is None 111 | assert root.val == 1 112 | assert root.left.left is None 113 | assert root.left.right is None 114 | assert root.left.val == 2 115 | assert repr(left_child) == "Node(2)" 116 | 117 | right_child = Node(3) 118 | root.right = right_child 119 | assert root.left is left_child 120 | assert root.right is right_child 121 | assert root.val == 1 122 | assert root.right.left is None 123 | assert root.right.right is None 124 | assert root.right.val == 3 125 | assert repr(right_child) == "Node(3)" 126 | 127 | last_node = Node(4) 128 | left_child.right = last_node 129 | assert root.left.right is last_node 130 | assert repr(root.left.right) == "Node(4)" 131 | 132 | 133 | def test_node_init_and_setattr_with_floats() -> None: 134 | root = Node(1.5) 135 | assert root.left is None 136 | assert root.right is None 137 | assert root.val == 1.5 138 | assert root.value == 1.5 139 | assert repr(root) == "Node(1.5)" 140 | 141 | root.value = 2.5 142 | assert root.value == 2.5 143 | assert root.val == 2.5 144 | assert repr(root) == "Node(2.5)" 145 | 146 | root.val = 1.5 147 | assert root.value == 1.5 148 | assert root.val == 1.5 149 | assert repr(root) == "Node(1.5)" 150 | 151 | left_child = Node(2.5) 152 | root.left = left_child 153 | assert root.left is left_child 154 | assert root.right is None 155 | assert root.val == 1.5 156 | assert root.left.left is None 157 | assert root.left.right is None 158 | assert root.left.val == 2.5 159 | assert repr(left_child) == "Node(2.5)" 160 | 161 | right_child = Node(3.5) 162 | root.right = right_child 163 | assert root.left is left_child 164 | assert root.right is right_child 165 | assert root.val == 1.5 166 | assert root.right.left is None 167 | assert root.right.right is None 168 | assert root.right.val == 3.5 169 | assert repr(right_child) == "Node(3.5)" 170 | 171 | last_node = Node(4.5) 172 | left_child.right = last_node 173 | assert root.left.right is last_node 174 | assert repr(root.left.right) == "Node(4.5)" 175 | 176 | 177 | def test_node_init_and_setattr_with_letters() -> None: 178 | root = Node("A") 179 | assert root.left is None 180 | assert root.right is None 181 | assert root.val == "A" 182 | assert root.value == "A" 183 | assert repr(root) == "Node(A)" 184 | 185 | root.value = "B" 186 | assert root.value == "B" 187 | assert root.val == "B" 188 | assert repr(root) == "Node(B)" 189 | 190 | root.val = "A" 191 | assert root.value == "A" 192 | assert root.val == "A" 193 | assert repr(root) == "Node(A)" 194 | 195 | left_child = Node("B") 196 | root.left = left_child 197 | assert root.left is left_child 198 | assert root.right is None 199 | assert root.val == "A" 200 | assert root.left.left is None 201 | assert root.left.right is None 202 | assert root.left.val == "B" 203 | assert repr(left_child) == "Node(B)" 204 | 205 | right_child = Node("C") 206 | root.right = right_child 207 | assert root.left is left_child 208 | assert root.right is right_child 209 | assert root.val == "A" 210 | assert root.right.left is None 211 | assert root.right.right is None 212 | assert root.right.val == "C" 213 | assert repr(right_child) == "Node(C)" 214 | 215 | last_node = Node("D") 216 | left_child.right = last_node 217 | assert root.left.right is last_node 218 | assert repr(root.left.right) == "Node(D)" 219 | 220 | 221 | def test_node_init_and_setattr_error_cases() -> None: 222 | root, left_child, right_child = Node(1), Node(2), Node(3) 223 | root.left = left_child 224 | root.right = right_child 225 | 226 | with pytest.raises(NodeValueError) as err1: 227 | Node(EMPTY_LIST) 228 | assert str(err1.value) == "node value must be a float/int/str" 229 | 230 | with pytest.raises(NodeValueError) as err2: 231 | Node(1).val = EMPTY_LIST 232 | assert str(err2.value) == "node value must be a float/int/str" 233 | 234 | with pytest.raises(NodeTypeError) as err3: 235 | Node(1, "this_is_not_a_node") # type: ignore 236 | assert str(err3.value) == "left child must be a Node instance" 237 | 238 | with pytest.raises(NodeTypeError) as err4: 239 | Node(1, Node(1), "this_is_not_a_node") # type: ignore 240 | assert str(err4.value) == "right child must be a Node instance" 241 | 242 | with pytest.raises(NodeTypeError) as err5: 243 | root.left = "this_is_not_a_node" # type: ignore 244 | assert root.left is left_child 245 | assert str(err5.value) == "left child must be a Node instance" 246 | 247 | with pytest.raises(NodeTypeError) as err6: 248 | root.right = "this_is_not_a_node" # type: ignore 249 | assert root.right is right_child 250 | assert str(err6.value) == "right child must be a Node instance" 251 | 252 | 253 | def test_tree_equals_with_integers() -> None: 254 | root1 = Node(1) 255 | root2 = Node(1) 256 | assert root1.equals(None) is False # type: ignore 257 | assert root1.equals(1) is False # type: ignore 258 | assert root1.equals(Node(2)) is False 259 | assert root1.equals(root2) is True 260 | assert root2.equals(root1) is True 261 | 262 | root1.left = Node(2) 263 | assert root1.equals(root2) is False 264 | assert root2.equals(root1) is False 265 | 266 | root2.left = Node(2) 267 | assert root1.equals(root2) is True 268 | assert root2.equals(root1) is True 269 | 270 | root1.right = Node(3) 271 | assert root1.equals(root2) is False 272 | assert root2.equals(root1) is False 273 | 274 | root2.right = Node(3) 275 | assert root1.equals(root2) is True 276 | assert root2.equals(root1) is True 277 | 278 | root1.right.left = Node(4) 279 | assert root1.equals(root2) is False 280 | assert root2.equals(root1) is False 281 | 282 | root2.right.left = Node(4) 283 | assert root1.equals(root2) is True 284 | assert root2.equals(root1) is True 285 | 286 | 287 | def test_tree_equals_with_floats() -> None: 288 | root1 = Node(1.5) 289 | root2 = Node(1.5) 290 | assert root1.equals(None) is False # type: ignore 291 | assert root1.equals(1.5) is False # type: ignore 292 | assert root1.equals(Node(2.5)) is False 293 | assert root1.equals(root2) is True 294 | assert root2.equals(root1) is True 295 | 296 | root1.left = Node(2.5) 297 | assert root1.equals(root2) is False 298 | assert root2.equals(root1) is False 299 | 300 | root2.left = Node(2.5) 301 | assert root1.equals(root2) is True 302 | assert root2.equals(root1) is True 303 | 304 | root1.right = Node(3.5) 305 | assert root1.equals(root2) is False 306 | assert root2.equals(root1) is False 307 | 308 | root2.right = Node(3.5) 309 | assert root1.equals(root2) is True 310 | assert root2.equals(root1) is True 311 | 312 | root1.right.left = Node(4.5) 313 | assert root1.equals(root2) is False 314 | assert root2.equals(root1) is False 315 | 316 | root2.right.left = Node(4.5) 317 | assert root1.equals(root2) is True 318 | assert root2.equals(root1) is True 319 | 320 | 321 | def test_tree_equals_with_letters() -> None: 322 | root1 = Node("A") 323 | root2 = Node("A") 324 | assert root1.equals(None) is False # type: ignore 325 | assert root1.equals("A") is False # type: ignore 326 | assert root1.equals(Node("B")) is False 327 | assert root1.equals(root2) is True 328 | assert root2.equals(root1) is True 329 | 330 | root1.left = Node("B") 331 | assert root1.equals(root2) is False 332 | assert root2.equals(root1) is False 333 | 334 | root2.left = Node("B") 335 | assert root1.equals(root2) is True 336 | assert root2.equals(root1) is True 337 | 338 | root1.right = Node("C") 339 | assert root1.equals(root2) is False 340 | assert root2.equals(root1) is False 341 | 342 | root2.right = Node("C") 343 | assert root1.equals(root2) is True 344 | assert root2.equals(root1) is True 345 | 346 | root1.right.left = Node("D") 347 | assert root1.equals(root2) is False 348 | assert root2.equals(root1) is False 349 | 350 | root2.right.left = Node("D") 351 | assert root1.equals(root2) is True 352 | assert root2.equals(root1) is True 353 | 354 | 355 | def test_tree_clone_with_numbers() -> None: 356 | for _ in range(REPETITIONS): 357 | root = tree(letters=False) 358 | assert root is not None 359 | clone = root.clone() 360 | assert root.values == clone.values 361 | assert root.equals(clone) 362 | assert clone.equals(root) 363 | assert root.properties == clone.properties 364 | 365 | 366 | def test_tree_clone_with_letters() -> None: 367 | for _ in range(REPETITIONS): 368 | root = tree(letters=True) 369 | assert root is not None 370 | clone = root.clone() 371 | assert root.values == clone.values 372 | assert root.equals(clone) 373 | assert clone.equals(root) 374 | assert root.properties == clone.properties 375 | 376 | 377 | def test_list_representation_1() -> None: 378 | root = build(EMPTY_LIST) 379 | assert root is None 380 | 381 | root = build([1]) 382 | assert root is not None 383 | assert root.val == 1 384 | assert root.left is None 385 | assert root.right is None 386 | 387 | root = build([1, 2]) 388 | assert root is not None 389 | assert root.val == 1 390 | assert root.left is not None 391 | assert root.left.val == 2 392 | assert root.right is None 393 | 394 | root = build([1, 2, 3]) 395 | assert root is not None 396 | assert root.val == 1 397 | assert root.left is not None 398 | assert root.left.val == 2 399 | assert root.right is not None 400 | assert root.right.val == 3 401 | assert root.left.left is None 402 | assert root.left.right is None 403 | assert root.right.left is None 404 | assert root.right.right is None 405 | 406 | root = build([1, 2, 3, None, 4]) 407 | assert root is not None 408 | assert root.val == 1 409 | assert root.left is not None 410 | assert root.left.val == 2 411 | assert root.right is not None 412 | assert root.right.val == 3 413 | assert root.left.left is None 414 | assert root.left.right is not None 415 | assert root.left.right.val == 4 416 | assert root.right.left is None 417 | assert root.right.right is None 418 | assert root.left.right.left is None 419 | assert root.left.right.right is None 420 | 421 | with pytest.raises(NodeNotFoundError) as err: 422 | build([None, 1, 2]) 423 | assert str(err.value) == "parent node missing at index 0" 424 | 425 | with pytest.raises(NodeNotFoundError) as err: 426 | build([1, None, 2, 3, 4]) 427 | assert str(err.value) == "parent node missing at index 1" 428 | 429 | root = Node(1) 430 | assert root.values == [1] 431 | 432 | root.right = Node(3) 433 | assert root.values == [1, None, 3] 434 | 435 | root.left = Node(2) 436 | assert root.values == [1, 2, 3] 437 | 438 | root.right.left = Node(4) 439 | assert root.values == [1, 2, 3, None, None, 4] 440 | 441 | root.right.right = Node(5) 442 | assert root.values == [1, 2, 3, None, None, 4, 5] 443 | 444 | root.left.left = Node(6) 445 | assert root.values == [1, 2, 3, 6, None, 4, 5] 446 | 447 | root.left.right = Node(7) 448 | assert root.values == [1, 2, 3, 6, 7, 4, 5] 449 | 450 | for _ in range(REPETITIONS): 451 | t1 = tree() 452 | assert t1 is not None 453 | 454 | t2 = build(t1.values) 455 | assert t2 is not None 456 | 457 | assert t1.values == t2.values 458 | 459 | 460 | def test_list_representation_2() -> None: 461 | root = build2(EMPTY_LIST) 462 | assert root is None 463 | 464 | root = build2([1]) 465 | assert root is not None 466 | assert root.val == 1 467 | assert root.left is None 468 | assert root.right is None 469 | 470 | root = build2([1, 2]) 471 | assert root is not None 472 | assert root.val == 1 473 | assert root.left is not None 474 | assert root.left.val == 2 475 | assert root.right is None 476 | 477 | root = build2([1, 2, 3]) 478 | assert root is not None 479 | assert root.val == 1 480 | assert root.left is not None 481 | assert root.left.val == 2 482 | assert root.right is not None 483 | assert root.right.val == 3 484 | assert root.left.left is None 485 | assert root.left.right is None 486 | assert root.right.left is None 487 | assert root.right.right is None 488 | 489 | root = build2([1, 2, 3, None, 4]) 490 | assert root is not None 491 | assert root.val == 1 492 | assert root.left is not None 493 | assert root.left.val == 2 494 | assert root.right is not None 495 | assert root.right.val == 3 496 | assert root.left.left is None 497 | assert root.left.right is not None 498 | assert root.left.right.val == 4 499 | assert root.right.left is None 500 | assert root.right.right is None 501 | assert root.left.right.left is None 502 | assert root.left.right.right is None 503 | 504 | root = build2([1, None, 2, 3, 4]) 505 | assert root is not None 506 | assert root.val == 1 507 | assert root.left is None 508 | assert root.right is not None 509 | assert root.right.val == 2 510 | assert root.right.left is not None 511 | assert root.right.left.val == 3 512 | assert root.right.right is not None 513 | assert root.right.right.val == 4 514 | 515 | root = build2([2, 5, None, 3, None, 1, 4]) 516 | assert root is not None 517 | assert root.val == 2 518 | assert root.left is not None 519 | assert root.left.val == 5 520 | assert root.right is None 521 | assert root.left.left is not None 522 | assert root.left.left.val == 3 523 | assert root.left.left.left is not None 524 | assert root.left.left.left.val == 1 525 | assert root.left.left.right is not None 526 | assert root.left.left.right.val == 4 527 | 528 | with pytest.raises(NodeValueError): 529 | build2([None, 1, 2]) 530 | 531 | root = Node(1) 532 | assert root.values2 == [1] 533 | root.right = Node(3) 534 | assert root.values2 == [1, None, 3] 535 | root.left = Node(2) 536 | assert root.values2 == [1, 2, 3] 537 | root.right.left = Node(4) 538 | assert root.values2 == [1, 2, 3, None, None, 4] 539 | root.right.right = Node(5) 540 | assert root.values2 == [1, 2, 3, None, None, 4, 5] 541 | root.left.left = Node(6) 542 | assert root.values2 == [1, 2, 3, 6, None, 4, 5] 543 | root.left.right = Node(7) 544 | assert root.values2 == [1, 2, 3, 6, 7, 4, 5] 545 | 546 | root = Node(1) 547 | assert root.values2 == [1] 548 | root.left = Node(2) 549 | assert root.values2 == [1, 2] 550 | root.right = Node(3) 551 | assert root.values2 == [1, 2, 3] 552 | root.right = None 553 | root.left.left = Node(3) 554 | assert root.values2 == [1, 2, None, 3] 555 | root.left.left.left = Node(4) 556 | assert root.values2 == [1, 2, None, 3, None, 4] 557 | root.left.left.right = Node(5) 558 | assert root.values2 == [1, 2, None, 3, None, 4, 5] 559 | 560 | for _ in range(REPETITIONS): 561 | t1 = tree() 562 | assert t1 is not None 563 | 564 | t2 = build2(t1.values2) 565 | assert t2 is not None 566 | 567 | assert t1.values2 == t2.values2 568 | 569 | 570 | def test_tree_get_node_by_level_order_index() -> None: 571 | root = Node(1) 572 | root.left = Node(2) 573 | root.right = Node(3) 574 | root.left.left = Node(4) 575 | root.left.right = Node(5) 576 | root.left.right.left = Node(6) 577 | 578 | assert root[0] is root 579 | assert root[1] is root.left 580 | assert root[2] is root.right 581 | assert root[3] is root.left.left 582 | assert root[4] is root.left.right 583 | assert root[9] is root.left.right.left 584 | 585 | for index in [5, 6, 7, 8, 10]: 586 | with pytest.raises(NodeNotFoundError) as err1: 587 | assert root[index] 588 | assert str(err1.value) == "node missing at index {}".format(index) 589 | 590 | with pytest.raises(NodeIndexError) as err2: 591 | assert root[-1] 592 | assert str(err2.value) == "node index must be a non-negative int" 593 | 594 | 595 | def test_tree_set_node_by_level_order_index() -> None: 596 | root = Node(1) 597 | root.left = Node(2) 598 | root.right = Node(3) 599 | root.left.left = Node(4) 600 | root.left.right = Node(5) 601 | root.left.right.left = Node(6) 602 | 603 | new_node_1 = Node(7) 604 | new_node_2 = Node(8) 605 | new_node_3 = Node(9) 606 | 607 | with pytest.raises(NodeModifyError) as err1: 608 | root[0] = new_node_1 609 | assert str(err1.value) == "cannot modify the root node" 610 | 611 | with pytest.raises(NodeIndexError) as err2: 612 | root[-1] = new_node_1 613 | assert str(err2.value) == "node index must be a non-negative int" 614 | 615 | with pytest.raises(NodeNotFoundError) as err3: 616 | root[100] = new_node_1 617 | assert str(err3.value) == "parent node missing at index 49" 618 | 619 | root[10] = new_node_1 620 | assert root.val == 1 621 | assert root.left.val == 2 622 | assert root.right.val == 3 623 | assert root.left.left.val == 4 624 | assert root.left.right.val == 5 625 | assert root.left.right.left.val == 6 626 | assert root.left.right.right is new_node_1 627 | 628 | root[4] = new_node_2 629 | assert root.val == 1 630 | assert root.left.val == 2 631 | assert root.right.val == 3 632 | assert root.left.left.val == 4 633 | assert root.left.right.val == 8 634 | assert root.left.right.left is None 635 | assert root.left.right.right is None 636 | 637 | root[1] = new_node_3 638 | root[2] = new_node_2 639 | assert root.left is new_node_3 640 | assert root.right is new_node_2 641 | 642 | 643 | def test_tree_delete_node_by_level_order_index() -> None: 644 | root = Node(1) 645 | root.left = Node(2) 646 | root.right = Node(3) 647 | root.left.left = Node(4) 648 | root.left.right = Node(5) 649 | root.left.right.left = Node(6) 650 | 651 | with pytest.raises(NodeModifyError) as err1: 652 | del root[0] 653 | assert str(err1.value) == "cannot delete the root node" 654 | 655 | with pytest.raises(NodeIndexError) as err2: 656 | del root[-1] 657 | assert str(err2.value) == "node index must be a non-negative int" 658 | 659 | with pytest.raises(NodeNotFoundError) as err3: 660 | del root[10] 661 | assert str(err3.value) == "no node to delete at index 10" 662 | 663 | with pytest.raises(NodeNotFoundError) as err4: 664 | del root[100] 665 | assert str(err4.value) == "no node to delete at index 100" 666 | 667 | del root[3] 668 | assert root.left.left is None 669 | assert root.left.val == 2 670 | assert root.left.right.val == 5 671 | assert root.left.right.right is None 672 | assert root.left.right.left.val == 6 673 | assert root.left.right.left.left is None 674 | assert root.left.right.left.right is None 675 | assert root.right.val == 3 676 | assert root.right.left is None 677 | assert root.right.right is None 678 | assert root.size == 5 679 | 680 | del root[2] 681 | assert root.left.left is None 682 | assert root.left.val == 2 683 | assert root.left.right.val == 5 684 | assert root.left.right.right is None 685 | assert root.left.right.left.val == 6 686 | assert root.left.right.left.left is None 687 | assert root.left.right.left.right is None 688 | assert root.right is None 689 | assert root.size == 4 690 | 691 | del root[4] 692 | assert root.left.left is None 693 | assert root.left.right is None 694 | assert root.right is None 695 | assert root.size == 2 696 | 697 | del root[1] 698 | assert root.left is None 699 | assert root.right is None 700 | assert root.size == 1 701 | 702 | 703 | def test_tree_print_with_integers_no_index() -> None: 704 | for printer in [builtin_print, pprint_default]: 705 | lines = printer([1]) 706 | assert lines == ["1"] 707 | lines = printer([1, 2]) 708 | assert lines == [" 1", " /", "2"] 709 | lines = printer([1, None, 3]) 710 | assert lines == ["1", " \\", " 3"] 711 | lines = printer([1, 2, 3]) 712 | assert lines == [" 1", " / \\", "2 3"] 713 | lines = printer([1, 2, 3, None, 5]) 714 | assert lines == [" __1", " / \\", "2 3", " \\", " 5"] 715 | lines = printer([1, 2, 3, None, 5, 6]) 716 | assert lines == [" __1__", " / \\", "2 3", " \\ /", " 5 6"] 717 | lines = printer([1, 2, 3, None, 5, 6, 7]) 718 | assert lines == [ 719 | " __1__", 720 | " / \\", 721 | "2 3", 722 | " \\ / \\", 723 | " 5 6 7", 724 | ] 725 | lines = printer([1, 2, 3, 8, 5, 6, 7]) 726 | assert lines == [ 727 | " __1__", 728 | " / \\", 729 | " 2 3", 730 | " / \\ / \\", 731 | "8 5 6 7", 732 | ] 733 | 734 | 735 | def test_tree_print_with_integers_with_index() -> None: 736 | lines = pprint_with_index([1]) 737 | assert lines == ["0:1"] 738 | lines = pprint_with_index([1, 2]) 739 | assert lines == [" _0:1", " /", "1:2"] 740 | lines = pprint_with_index([1, None, 3]) 741 | assert lines == ["0:1_", " \\", " 2:3"] 742 | lines = pprint_with_index([1, 2, 3]) 743 | assert lines == [" _0:1_", " / \\", "1:2 2:3"] 744 | lines = pprint_with_index([1, 2, 3, None, 5]) 745 | assert lines == [ 746 | " _____0:1_", 747 | " / \\", 748 | "1:2_ 2:3", 749 | " \\", 750 | " 4:5", 751 | ] 752 | lines = pprint_with_index([1, 2, 3, None, 5, 6]) 753 | assert lines == [ 754 | " _____0:1_____", 755 | " / \\", 756 | "1:2_ _2:3", 757 | " \\ /", 758 | " 4:5 5:6", 759 | ] 760 | lines = pprint_with_index([1, 2, 3, None, 5, 6, 7]) 761 | assert lines == [ 762 | " _____0:1_____", 763 | " / \\", 764 | "1:2_ _2:3_", 765 | " \\ / \\", 766 | " 4:5 5:6 6:7", 767 | ] 768 | lines = pprint_with_index([1, 2, 3, 8, 5, 6, 7]) 769 | assert lines == [ 770 | " _____0:1_____", 771 | " / \\", 772 | " _1:2_ _2:3_", 773 | " / \\ / \\", 774 | "3:8 4:5 5:6 6:7", 775 | ] 776 | 777 | 778 | def test_tree_print_with_letters_no_index() -> None: 779 | for printer in [builtin_print, pprint_default]: 780 | lines = printer(["A"]) 781 | assert lines == ["A"] 782 | lines = printer(["A", "B"]) 783 | assert lines == [" A", " /", "B"] 784 | lines = printer(["A", None, "C"]) 785 | assert lines == ["A", " \\", " C"] 786 | lines = printer(["A", "B", "C"]) 787 | assert lines == [" A", " / \\", "B C"] 788 | lines = printer(["A", "B", "C", None, "E"]) 789 | assert lines == [" __A", " / \\", "B C", " \\", " E"] 790 | lines = printer(["A", "B", "C", None, "E", "F"]) 791 | assert lines == [" __A__", " / \\", "B C", " \\ /", " E F"] 792 | lines = printer(["A", "B", "C", None, "E", "F", "G"]) 793 | assert lines == [ 794 | " __A__", 795 | " / \\", 796 | "B C", 797 | " \\ / \\", 798 | " E F G", 799 | ] 800 | lines = printer(["A", "B", "C", "D", "E", "F", "G"]) 801 | assert lines == [ 802 | " __A__", 803 | " / \\", 804 | " B C", 805 | " / \\ / \\", 806 | "D E F G", 807 | ] 808 | 809 | 810 | def test_tree_print_with_letters_with_index() -> None: 811 | lines = pprint_with_index(["A"]) 812 | assert lines == ["0:A"] 813 | lines = pprint_with_index(["A", "B"]) 814 | assert lines == [" _0:A", " /", "1:B"] 815 | lines = pprint_with_index(["A", None, "C"]) 816 | assert lines == ["0:A_", " \\", " 2:C"] 817 | lines = pprint_with_index(["A", "B", "C"]) 818 | assert lines == [" _0:A_", " / \\", "1:B 2:C"] 819 | lines = pprint_with_index(["A", "B", "C", None, "E"]) 820 | assert lines == [ 821 | " _____0:A_", 822 | " / \\", 823 | "1:B_ 2:C", 824 | " \\", 825 | " 4:E", 826 | ] 827 | lines = pprint_with_index(["A", "B", "C", None, "E", "F"]) 828 | assert lines == [ 829 | " _____0:A_____", 830 | " / \\", 831 | "1:B_ _2:C", 832 | " \\ /", 833 | " 4:E 5:F", 834 | ] 835 | lines = pprint_with_index(["A", "B", "C", None, "E", "F", "G"]) 836 | assert lines == [ 837 | " _____0:A_____", 838 | " / \\", 839 | "1:B_ _2:C_", 840 | " \\ / \\", 841 | " 4:E 5:F 6:G", 842 | ] 843 | lines = pprint_with_index(["A", "B", "C", "D", "E", "F", "G"]) 844 | assert lines == [ 845 | " _____0:A_____", 846 | " / \\", 847 | " _1:B_ _2:C_", 848 | " / \\ / \\", 849 | "3:D 4:E 5:F 6:G", 850 | ] 851 | 852 | 853 | def test_tree_validate() -> None: 854 | class TestNode(Node): 855 | def __setattr__(self, attr: str, value: Any) -> None: 856 | object.__setattr__(self, attr, value) 857 | 858 | root = Node(1) 859 | root.validate() # Should pass 860 | 861 | root = Node(1) 862 | root.left = Node(2) 863 | root.validate() # Should pass 864 | 865 | root = Node(1) 866 | root.left = Node(2) 867 | root.right = Node(3) 868 | root.validate() # Should pass 869 | 870 | root = Node(1) 871 | root.left = Node(2) 872 | root.right = Node(3) 873 | root.left.left = Node(4) 874 | root.left.right = Node(5) 875 | root.left.right.left = Node(6) 876 | root.validate() # Should pass 877 | 878 | root = TestNode(1) 879 | root.left = "not_a_node" # type: ignore 880 | with pytest.raises(NodeTypeError) as err1: 881 | root.validate() 882 | assert str(err1.value) == "invalid node instance at index 1" 883 | 884 | root = TestNode(1) 885 | root.right = TestNode(2) 886 | root.right.val = EMPTY_LIST 887 | with pytest.raises(NodeValueError) as err2: 888 | root.validate() 889 | assert str(err2.value) == "invalid node value at index 2" 890 | 891 | root = TestNode(1) 892 | root.left = TestNode(2) 893 | root.left.right = root 894 | with pytest.raises(NodeReferenceError) as err3: 895 | root.validate() 896 | assert str(err3.value) == "cyclic reference at Node(1) (level-order index 4)" 897 | 898 | 899 | def test_tree_validate_with_letters() -> None: 900 | class TestNode(Node): 901 | def __setattr__(self, attr: str, value: Any) -> None: 902 | object.__setattr__(self, attr, value) 903 | 904 | root = Node("A") 905 | root.validate() # Should pass 906 | 907 | root = Node("A") 908 | root.left = Node("B") 909 | root.validate() # Should pass 910 | 911 | root = Node("A") 912 | root.left = Node("B") 913 | root.right = Node(3) 914 | root.validate() # Should pass 915 | 916 | root = Node("A") 917 | root.left = Node("B") 918 | root.right = Node(3) 919 | root.left.left = Node(4) 920 | root.left.right = Node(5) 921 | root.left.right.left = Node(6) 922 | root.validate() # Should pass 923 | 924 | root = TestNode("A") 925 | root.left = "not_a_node" # type: ignore 926 | with pytest.raises(NodeTypeError) as err1: 927 | root.validate() 928 | assert str(err1.value) == "invalid node instance at index 1" 929 | 930 | root = TestNode("A") 931 | root.right = TestNode("B") 932 | root.right.val = EMPTY_LIST 933 | with pytest.raises(NodeValueError) as err2: 934 | root.validate() 935 | assert str(err2.value) == "invalid node value at index 2" 936 | 937 | root = TestNode("A") 938 | root.left = TestNode("B") 939 | root.left.right = root 940 | with pytest.raises(NodeReferenceError) as err3: 941 | root.validate() 942 | assert str(err3.value) == "cyclic reference at Node(A) (level-order index 4)" 943 | 944 | 945 | def test_tree_properties() -> None: 946 | root = Node(1) 947 | assert root.properties == { 948 | "height": 0, 949 | "is_balanced": True, 950 | "is_bst": True, 951 | "is_complete": True, 952 | "is_max_heap": True, 953 | "is_min_heap": True, 954 | "is_perfect": True, 955 | "is_strict": True, 956 | "is_symmetric": True, 957 | "leaf_count": 1, 958 | "max_leaf_depth": 0, 959 | "max_node_value": 1, 960 | "min_leaf_depth": 0, 961 | "min_node_value": 1, 962 | "size": 1, 963 | } 964 | assert root.height == 0 965 | assert root.is_balanced is True 966 | assert root.is_bst is True 967 | assert root.is_complete is True 968 | assert root.is_max_heap is True 969 | assert root.is_min_heap is True 970 | assert root.is_perfect is True 971 | assert root.is_strict is True 972 | assert root.is_symmetric is True 973 | assert root.leaf_count == 1 974 | assert root.max_leaf_depth == 0 975 | assert root.max_node_value == 1 976 | assert root.min_leaf_depth == 0 977 | assert root.min_node_value == 1 978 | assert root.size == len(root) == 1 979 | 980 | root.left = Node(2) 981 | assert root.properties == { 982 | "height": 1, 983 | "is_balanced": True, 984 | "is_bst": False, 985 | "is_complete": True, 986 | "is_max_heap": False, 987 | "is_min_heap": True, 988 | "is_perfect": False, 989 | "is_strict": False, 990 | "is_symmetric": False, 991 | "leaf_count": 1, 992 | "max_leaf_depth": 1, 993 | "max_node_value": 2, 994 | "min_leaf_depth": 1, 995 | "min_node_value": 1, 996 | "size": 2, 997 | } 998 | assert root.height == 1 999 | assert root.is_balanced is True 1000 | assert root.is_bst is False 1001 | assert root.is_complete is True 1002 | assert root.is_max_heap is False 1003 | assert root.is_min_heap is True 1004 | assert root.is_perfect is False 1005 | assert root.is_strict is False 1006 | assert root.is_symmetric is False 1007 | assert root.leaf_count == 1 1008 | assert root.max_leaf_depth == 1 1009 | assert root.max_node_value == 2 1010 | assert root.min_leaf_depth == 1 1011 | assert root.min_node_value == 1 1012 | assert root.size == len(root) == 2 1013 | 1014 | root.right = Node(3) 1015 | assert root.properties == { 1016 | "height": 1, 1017 | "is_balanced": True, 1018 | "is_bst": False, 1019 | "is_complete": True, 1020 | "is_max_heap": False, 1021 | "is_min_heap": True, 1022 | "is_perfect": True, 1023 | "is_strict": True, 1024 | "is_symmetric": False, 1025 | "leaf_count": 2, 1026 | "max_leaf_depth": 1, 1027 | "max_node_value": 3, 1028 | "min_leaf_depth": 1, 1029 | "min_node_value": 1, 1030 | "size": 3, 1031 | } 1032 | assert root.height == 1 1033 | assert root.is_balanced is True 1034 | assert root.is_bst is False 1035 | assert root.is_complete is True 1036 | assert root.is_max_heap is False 1037 | assert root.is_min_heap is True 1038 | assert root.is_perfect is True 1039 | assert root.is_strict is True 1040 | assert root.is_symmetric is False 1041 | assert root.leaf_count == 2 1042 | assert root.max_leaf_depth == 1 1043 | assert root.max_node_value == 3 1044 | assert root.min_leaf_depth == 1 1045 | assert root.min_node_value == 1 1046 | assert root.size == len(root) == 3 1047 | 1048 | root.left.left = Node(4) 1049 | assert root.properties == { 1050 | "height": 2, 1051 | "is_balanced": True, 1052 | "is_bst": False, 1053 | "is_complete": True, 1054 | "is_max_heap": False, 1055 | "is_min_heap": True, 1056 | "is_perfect": False, 1057 | "is_strict": False, 1058 | "is_symmetric": False, 1059 | "leaf_count": 2, 1060 | "max_leaf_depth": 2, 1061 | "max_node_value": 4, 1062 | "min_leaf_depth": 1, 1063 | "min_node_value": 1, 1064 | "size": 4, 1065 | } 1066 | assert root.height == 2 1067 | assert root.is_balanced is True 1068 | assert root.is_bst is False 1069 | assert root.is_complete is True 1070 | assert root.is_max_heap is False 1071 | assert root.is_min_heap is True 1072 | assert root.is_perfect is False 1073 | assert root.is_strict is False 1074 | assert root.is_symmetric is False 1075 | assert root.leaf_count == 2 1076 | assert root.max_leaf_depth == 2 1077 | assert root.max_node_value == 4 1078 | assert root.min_leaf_depth == 1 1079 | assert root.min_node_value == 1 1080 | assert root.size == len(root) == 4 1081 | 1082 | root.right.left = Node(5) 1083 | assert root.properties == { 1084 | "height": 2, 1085 | "is_balanced": True, 1086 | "is_bst": False, 1087 | "is_complete": False, 1088 | "is_max_heap": False, 1089 | "is_min_heap": False, 1090 | "is_perfect": False, 1091 | "is_strict": False, 1092 | "is_symmetric": False, 1093 | "leaf_count": 2, 1094 | "max_leaf_depth": 2, 1095 | "max_node_value": 5, 1096 | "min_leaf_depth": 2, 1097 | "min_node_value": 1, 1098 | "size": 5, 1099 | } 1100 | assert root.height == 2 1101 | assert root.is_balanced is True 1102 | assert root.is_bst is False 1103 | assert root.is_complete is False 1104 | assert root.is_max_heap is False 1105 | assert root.is_min_heap is False 1106 | assert root.is_perfect is False 1107 | assert root.is_strict is False 1108 | assert root.is_symmetric is False 1109 | assert root.leaf_count == 2 1110 | assert root.max_leaf_depth == 2 1111 | assert root.max_node_value == 5 1112 | assert root.min_leaf_depth == 2 1113 | assert root.min_node_value == 1 1114 | assert root.size == len(root) == 5 1115 | 1116 | root.right.left.left = Node(6) 1117 | assert root.properties == { 1118 | "height": 3, 1119 | "is_balanced": False, 1120 | "is_bst": False, 1121 | "is_complete": False, 1122 | "is_max_heap": False, 1123 | "is_min_heap": False, 1124 | "is_perfect": False, 1125 | "is_strict": False, 1126 | "is_symmetric": False, 1127 | "leaf_count": 2, 1128 | "max_leaf_depth": 3, 1129 | "max_node_value": 6, 1130 | "min_leaf_depth": 2, 1131 | "min_node_value": 1, 1132 | "size": 6, 1133 | } 1134 | assert root.height == 3 1135 | assert root.is_balanced is False 1136 | assert root.is_bst is False 1137 | assert root.is_complete is False 1138 | assert root.is_max_heap is False 1139 | assert root.is_min_heap is False 1140 | assert root.is_perfect is False 1141 | assert root.is_strict is False 1142 | assert root.is_symmetric is False 1143 | assert root.leaf_count == 2 1144 | assert root.max_leaf_depth == 3 1145 | assert root.max_node_value == 6 1146 | assert root.min_leaf_depth == 2 1147 | assert root.min_node_value == 1 1148 | assert root.size == len(root) == 6 1149 | 1150 | root.left.left.left = Node(7) 1151 | assert root.properties == { 1152 | "height": 3, 1153 | "is_balanced": False, 1154 | "is_bst": False, 1155 | "is_complete": False, 1156 | "is_max_heap": False, 1157 | "is_min_heap": False, 1158 | "is_perfect": False, 1159 | "is_strict": False, 1160 | "is_symmetric": False, 1161 | "leaf_count": 2, 1162 | "max_leaf_depth": 3, 1163 | "max_node_value": 7, 1164 | "min_leaf_depth": 3, 1165 | "min_node_value": 1, 1166 | "size": 7, 1167 | } 1168 | assert root.height == 3 1169 | assert root.is_balanced is False 1170 | assert root.is_bst is False 1171 | assert root.is_complete is False 1172 | assert root.is_max_heap is False 1173 | assert root.is_min_heap is False 1174 | assert root.is_perfect is False 1175 | assert root.is_strict is False 1176 | assert root.is_symmetric is False 1177 | assert root.leaf_count == 2 1178 | assert root.max_leaf_depth == 3 1179 | assert root.max_node_value == 7 1180 | assert root.min_leaf_depth == 3 1181 | assert root.min_node_value == 1 1182 | assert root.size == len(root) == 7 1183 | 1184 | 1185 | def test_tree_traversal() -> None: 1186 | n1 = Node(1) 1187 | assert n1.levels == [[n1]] 1188 | assert n1.leaves == [n1] 1189 | assert n1.inorder == [n1] 1190 | assert n1.preorder == [n1] 1191 | assert n1.postorder == [n1] 1192 | assert n1.levelorder == [n1] 1193 | 1194 | n2 = Node(2) 1195 | n1.left = n2 1196 | assert n1.levels == [[n1], [n2]] 1197 | assert n1.leaves == [n2] 1198 | assert n1.inorder == [n2, n1] 1199 | assert n1.preorder == [n1, n2] 1200 | assert n1.postorder == [n2, n1] 1201 | assert n1.levelorder == [n1, n2] 1202 | 1203 | n3 = Node(3) 1204 | n1.right = n3 1205 | assert n1.levels == [[n1], [n2, n3]] 1206 | assert n1.leaves == [n2, n3] 1207 | assert n1.inorder == [n2, n1, n3] 1208 | assert n1.preorder == [n1, n2, n3] 1209 | assert n1.postorder == [n2, n3, n1] 1210 | assert n1.levelorder == [n1, n2, n3] 1211 | 1212 | n4 = Node(4) 1213 | n5 = Node(5) 1214 | n2.left = n4 1215 | n2.right = n5 1216 | 1217 | assert n1.levels == [[n1], [n2, n3], [n4, n5]] 1218 | assert n1.leaves == [n3, n4, n5] 1219 | assert n1.inorder == [n4, n2, n5, n1, n3] 1220 | assert n1.preorder == [n1, n2, n4, n5, n3] 1221 | assert n1.postorder == [n4, n5, n2, n3, n1] 1222 | assert n1.levelorder == [n1, n2, n3, n4, n5] 1223 | 1224 | 1225 | def test_tree_generation() -> None: 1226 | for invalid_height in ["foo", -1, None]: 1227 | with pytest.raises(TreeHeightError) as err: 1228 | tree(height=invalid_height) # type: ignore 1229 | assert str(err.value) == "height must be an int between 0 - 9" 1230 | 1231 | root = tree(height=0) 1232 | assert root is not None 1233 | 1234 | root.validate() 1235 | assert root.height == 0 1236 | assert root.left is None 1237 | assert root.right is None 1238 | assert isinstance(root.val, int) 1239 | 1240 | for _ in range(REPETITIONS): 1241 | random_height = random.randint(1, 9) 1242 | 1243 | root = tree(random_height) 1244 | assert root is not None 1245 | 1246 | root.validate() 1247 | assert root.height == random_height 1248 | 1249 | for _ in range(REPETITIONS): 1250 | random_height = random.randint(1, 9) 1251 | 1252 | root = tree(random_height, is_perfect=True) 1253 | assert root is not None 1254 | 1255 | root.validate() 1256 | assert root.height == random_height 1257 | assert root.is_perfect is True 1258 | assert root.is_balanced is True 1259 | assert root.is_strict is True 1260 | 1261 | 1262 | def test_bst_generation() -> None: 1263 | for invalid_height in ["foo", -1, None]: 1264 | with pytest.raises(TreeHeightError) as err: 1265 | bst(height=invalid_height) # type: ignore 1266 | assert str(err.value) == "height must be an int between 0 - 9" 1267 | 1268 | root = bst(height=0) 1269 | assert root is not None 1270 | root.validate() 1271 | assert root.height == 0 1272 | assert root.left is None 1273 | assert root.right is None 1274 | assert isinstance(root.val, int) 1275 | 1276 | for _ in range(REPETITIONS): 1277 | random_height = random.randint(1, 9) 1278 | root = bst(random_height) 1279 | assert root is not None 1280 | root.validate() 1281 | assert root.is_bst is True 1282 | assert root.height == random_height 1283 | 1284 | for _ in range(REPETITIONS): 1285 | random_height = random.randint(1, 9) 1286 | root = bst(random_height, letters=True) 1287 | assert root is not None 1288 | root.validate() 1289 | assert root.is_bst is True 1290 | assert root.height == random_height 1291 | 1292 | for _ in range(REPETITIONS): 1293 | random_height = random.randint(1, 9) 1294 | root = bst(random_height, is_perfect=True) 1295 | assert root is not None 1296 | root.validate() 1297 | assert root.height == random_height 1298 | 1299 | if not root.is_bst: 1300 | raise Exception("boo") 1301 | 1302 | assert root.is_bst is True 1303 | assert root.is_perfect is True 1304 | assert root.is_balanced is True 1305 | assert root.is_strict is True 1306 | 1307 | for _ in range(REPETITIONS): 1308 | random_height = random.randint(1, 9) 1309 | root = bst(random_height, letters=True, is_perfect=True) 1310 | assert root is not None 1311 | root.validate() 1312 | assert root.height == random_height 1313 | 1314 | if not root.is_bst: 1315 | raise Exception("boo") 1316 | 1317 | assert root.is_bst is True 1318 | assert root.is_perfect is True 1319 | assert root.is_balanced is True 1320 | assert root.is_strict is True 1321 | 1322 | 1323 | def test_heap_generation() -> None: 1324 | for invalid_height in ["foo", -1, None]: 1325 | with pytest.raises(TreeHeightError) as err: 1326 | heap(height=invalid_height) # type: ignore 1327 | assert str(err.value) == "height must be an int between 0 - 9" 1328 | 1329 | root = heap(height=0) 1330 | assert root is not None 1331 | root.validate() 1332 | assert root.height == 0 1333 | assert root.left is None 1334 | assert root.right is None 1335 | assert isinstance(root.val, int) 1336 | 1337 | for _ in range(REPETITIONS): 1338 | random_height = random.randint(1, 9) 1339 | root = heap(random_height, is_max=True) 1340 | assert root is not None 1341 | root.validate() 1342 | assert root.is_max_heap is True 1343 | assert root.is_min_heap is False 1344 | assert root.height == random_height 1345 | 1346 | for _ in range(REPETITIONS): 1347 | random_height = random.randint(1, 9) 1348 | root = heap(random_height, letters=True, is_max=True) 1349 | assert root is not None 1350 | root.validate() 1351 | assert root.is_max_heap is True 1352 | assert root.is_min_heap is False 1353 | assert root.height == random_height 1354 | 1355 | for _ in range(REPETITIONS): 1356 | random_height = random.randint(1, 9) 1357 | root = heap(random_height, is_max=False) 1358 | assert root is not None 1359 | root.validate() 1360 | assert root.is_max_heap is False 1361 | assert root.is_min_heap is True 1362 | assert root.height == random_height 1363 | 1364 | for _ in range(REPETITIONS): 1365 | random_height = random.randint(1, 9) 1366 | root = heap(random_height, letters=True, is_max=False) 1367 | assert root is not None 1368 | root.validate() 1369 | assert root.is_max_heap is False 1370 | assert root.is_min_heap is True 1371 | assert root.height == random_height 1372 | 1373 | for _ in range(REPETITIONS): 1374 | random_height = random.randint(1, 9) 1375 | root = heap(random_height, is_perfect=True) 1376 | assert root is not None 1377 | root.validate() 1378 | assert root.is_max_heap is True 1379 | assert root.is_min_heap is False 1380 | assert root.is_perfect is True 1381 | assert root.is_balanced is True 1382 | assert root.is_strict is True 1383 | assert root.height == random_height 1384 | 1385 | for _ in range(REPETITIONS): 1386 | random_height = random.randint(1, 9) 1387 | root = heap(random_height, letters=True, is_perfect=True) 1388 | assert root is not None 1389 | root.validate() 1390 | assert root.is_max_heap is True 1391 | assert root.is_min_heap is False 1392 | assert root.is_perfect is True 1393 | assert root.is_balanced is True 1394 | assert root.is_strict is True 1395 | assert root.height == random_height 1396 | 1397 | 1398 | def test_heap_float_values() -> None: 1399 | root = Node(1.0) 1400 | root.left = Node(0.5) 1401 | root.right = Node(1.5) 1402 | 1403 | assert root.height == 1 1404 | assert root.is_balanced is True 1405 | assert root.is_bst is True 1406 | assert root.is_complete is True 1407 | assert root.is_max_heap is False 1408 | assert root.is_min_heap is False 1409 | assert root.is_perfect is True 1410 | assert root.is_strict is True 1411 | assert root.leaf_count == 2 1412 | assert root.max_leaf_depth == 1 1413 | assert root.max_node_value == 1.5 1414 | assert root.min_leaf_depth == 1 1415 | assert root.min_node_value == 0.5 1416 | assert root.size == 3 1417 | 1418 | for printer in [builtin_print, pprint_default]: 1419 | lines = printer([1.0]) 1420 | assert lines == ["1.0"] 1421 | lines = printer([1.0, 2.0]) 1422 | assert lines == [" _1.0", " /", "2.0"] 1423 | lines = printer([1.0, None, 3.0]) 1424 | assert lines == ["1.0_", " \\", " 3.0"] 1425 | lines = printer([1.0, 2.0, 3.0]) 1426 | assert lines == [" _1.0_", " / \\", "2.0 3.0"] 1427 | lines = printer([1.0, 2.0, 3.0, None, 5.0]) 1428 | assert lines == [ 1429 | " _____1.0_", 1430 | " / \\", 1431 | "2.0_ 3.0", 1432 | " \\", 1433 | " 5.0", 1434 | ] 1435 | 1436 | 1437 | def test_heap_float_values_builders() -> None: 1438 | for builder in [tree, bst, heap]: 1439 | for _ in range(REPETITIONS): 1440 | root = builder() # type: ignore 1441 | root_copy = copy.deepcopy(root) 1442 | 1443 | for node in root: 1444 | node.value += 0.1 1445 | 1446 | assert root.height == root_copy.height 1447 | assert root.is_balanced == root_copy.is_balanced 1448 | assert root.is_bst == root_copy.is_bst 1449 | assert root.is_complete == root_copy.is_complete 1450 | assert root.is_max_heap == root_copy.is_max_heap 1451 | assert root.is_min_heap == root_copy.is_min_heap 1452 | assert root.is_perfect == root_copy.is_perfect 1453 | assert root.is_strict == root_copy.is_strict 1454 | assert root.is_symmetric == root_copy.is_symmetric 1455 | assert root.leaf_count == root_copy.leaf_count 1456 | assert root.max_leaf_depth == root_copy.max_leaf_depth 1457 | assert root.max_node_value == root_copy.max_node_value + 0.1 1458 | assert root.min_leaf_depth == root_copy.min_leaf_depth 1459 | assert root.min_node_value == root_copy.min_node_value + 0.1 1460 | assert root.size == root_copy.size 1461 | 1462 | 1463 | def test_get_index_utility_function() -> None: 1464 | root = Node(0) 1465 | root.left = Node(1) 1466 | root.right = Node(2) 1467 | root.left.left = Node(3) 1468 | root.right.right = Node(4) 1469 | 1470 | assert get_index(root, root) == 0 1471 | assert get_index(root, root.left) == 1 1472 | assert get_index(root, root.right) == 2 1473 | assert get_index(root, root.left.left) == 3 1474 | assert get_index(root, root.right.right) == 6 1475 | 1476 | with pytest.raises(NodeReferenceError) as err1: 1477 | get_index(root.left, root.right) 1478 | assert str(err1.value) == "given nodes are not in the same tree" 1479 | 1480 | with pytest.raises(NodeTypeError) as err2: 1481 | get_index(root, None) # type: ignore 1482 | assert str(err2.value) == "descendent must be a Node instance" 1483 | 1484 | with pytest.raises(NodeTypeError) as err3: 1485 | get_index(None, root.left) # type: ignore 1486 | assert str(err3.value) == "root must be a Node instance" 1487 | 1488 | 1489 | def test_get_parent_utility_function() -> None: 1490 | root = Node(0) 1491 | root.left = Node(1) 1492 | root.right = Node(2) 1493 | root.left.left = Node(3) 1494 | root.right.right = Node(4) 1495 | 1496 | assert get_parent(root, root.left.left) == root.left 1497 | assert get_parent(root, root.left) == root 1498 | assert get_parent(root, root) is None 1499 | assert get_parent(root, root.right.right) == root.right 1500 | assert get_parent(root, root.right) == root 1501 | assert get_parent(root, Node(5)) is None 1502 | assert get_parent(None, root.left) is None 1503 | assert get_parent(root, None) is None 1504 | 1505 | 1506 | def test_svg_generation() -> None: 1507 | root = Node(0) 1508 | assert root.svg() == EXPECTED_SVG_XML_SINGLE_NODE 1509 | 1510 | root.left = Node(1) 1511 | root.right = Node(2) 1512 | root.left.left = Node(3) 1513 | root.right.right = Node(4) 1514 | assert root.svg() == EXPECTED_SVG_XML_MULTIPLE_NODES 1515 | 1516 | 1517 | def test_number_to_letters_utility_function() -> None: 1518 | with pytest.raises(AssertionError): 1519 | number_to_letters(-1) 1520 | 1521 | assert number_to_letters(0) == "A" 1522 | assert number_to_letters(1) == "B" 1523 | assert number_to_letters(25) == "Z" 1524 | assert number_to_letters(26) == "ZA" 1525 | assert number_to_letters(51) == "ZZ" 1526 | assert number_to_letters(52) == "ZZA" 1527 | 1528 | for _ in range(REPETITIONS): 1529 | num1 = random.randint(0, 1000) 1530 | num2 = random.randint(0, 1000) 1531 | str1 = number_to_letters(num1) 1532 | str2 = number_to_letters(num2) 1533 | assert (num1 < num2) == (str1 < str2) 1534 | assert (num1 > num2) == (str1 > str2) 1535 | assert (num1 == num2) == (str1 == str2) 1536 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import sys 4 | from typing import Any, List 5 | 6 | try: 7 | # noinspection PyCompatibility 8 | from StringIO import StringIO 9 | except ImportError: 10 | from io import StringIO 11 | 12 | from binarytree import NodeValueList, build 13 | 14 | 15 | class CaptureOutput(List[str]): 16 | """Context manager to catch stdout.""" 17 | 18 | def __enter__(self) -> "CaptureOutput": 19 | self._original_stdout = sys.stdout 20 | self._temp_stdout = StringIO() 21 | sys.stdout = self._temp_stdout 22 | return self 23 | 24 | def __exit__(self, *args: Any) -> None: 25 | lines = self._temp_stdout.getvalue().splitlines() 26 | self.extend(line.rstrip() for line in lines) 27 | sys.stdout = self._original_stdout 28 | 29 | 30 | def pprint_default(values: NodeValueList) -> List[str]: 31 | """Helper function for testing Node.pprint with default arguments.""" 32 | root = build(values) 33 | assert root is not None 34 | 35 | with CaptureOutput() as output: 36 | root.pprint(index=False, delimiter="-") 37 | assert output[0] == "" and output[-1] == "" 38 | return [line for line in output if line != ""] 39 | 40 | 41 | def pprint_with_index(values: NodeValueList) -> List[str]: 42 | """Helper function for testing Node.pprint with indexes.""" 43 | root = build(values) 44 | assert root is not None 45 | 46 | with CaptureOutput() as output: 47 | root.pprint(index=True, delimiter=":") 48 | assert output[0] == "" and output[-1] == "" 49 | return [line for line in output if line != ""] 50 | 51 | 52 | def builtin_print(values: NodeValueList) -> List[str]: 53 | """Helper function for testing builtin print on Node.""" 54 | root = build(values) 55 | assert root is not None 56 | 57 | with CaptureOutput() as output: 58 | print(root) 59 | assert output[0] == "" and output[-1] == "" 60 | return [line for line in output if line != ""] 61 | --------------------------------------------------------------------------------