├── .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 | 
4 | 
5 | [](https://codecov.io/gh/joowani/binarytree)
6 | [](https://badge.fury.io/py/binarytree)
7 | [](https://github.com/joowani/binarytree/blob/main/LICENSE)
8 | 
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 | 
23 |
24 | Binarytree can be used with [Graphviz](https://graphviz.org) and
25 | [Jupyter Notebooks](https://jupyter.org) as well:
26 |
27 | 
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 |
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 |
52 | """
53 |
54 | EXPECTED_SVG_XML_MULTIPLE_NODES = """
55 |
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 |
--------------------------------------------------------------------------------