From 43f4ff5056dfed2d888a6aa0df8607a09dd89be8 Mon Sep 17 00:00:00 2001 From: nbro <9349000+nbro@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:20:45 +0100 Subject: [PATCH] Initial commit --- .github/workflows/release.yml | 62 ++ .github/workflows/tests.yml | 46 + .gitignore | 14 + LICENSE.md | 21 + Makefile | 58 ++ README.md | 52 + andz/__init__.py | 4 + andz/algorithms/README.md | 228 +++++ andz/algorithms/__init__.py | 4 + andz/algorithms/crypto/README.md | 59 ++ andz/algorithms/crypto/__init__.py | 4 + andz/algorithms/crypto/caesar.py | 169 ++++ .../algorithms/crypto/images/block-cipher.png | Bin 0 -> 318620 bytes .../crypto/images/cbc-decryption.png | Bin 0 -> 87470 bytes .../crypto/images/cbc-encryption.png | Bin 0 -> 87559 bytes andz/algorithms/crypto/one_time_pad.py | 188 ++++ andz/algorithms/dac/README.md | 24 + andz/algorithms/dac/__init__.py | 4 + andz/algorithms/dac/binary_search.py | 145 +++ andz/algorithms/dac/find_extrema.py | 95 ++ andz/algorithms/dac/find_peak.py | 70 ++ andz/algorithms/dac/select.py | 48 + andz/algorithms/dp/README.md | 7 + andz/algorithms/dp/change_making.py | 235 +++++ andz/algorithms/dp/fibonacci.py | 137 +++ andz/algorithms/matching/__init__.py | 4 + andz/algorithms/matching/gale_shapley.py | 334 +++++++ andz/algorithms/numerical/README.md | 25 + andz/algorithms/numerical/barycentric.py | 93 ++ andz/algorithms/numerical/gradient_descent.py | 50 + andz/algorithms/numerical/horner.py | 153 +++ andz/algorithms/numerical/neville.py | 232 +++++ andz/algorithms/numerical/newton.py | 109 +++ andz/algorithms/ode/__init__.py | 4 + andz/algorithms/ode/forward_euler.py | 163 ++++ andz/algorithms/recursion/README.md | 7 + andz/algorithms/recursion/__init__.py | 4 + andz/algorithms/recursion/ackermann.py | 53 ++ andz/algorithms/recursion/count.py | 35 + andz/algorithms/recursion/factorial.py | 94 ++ andz/algorithms/recursion/hanoi.py | 83 ++ andz/algorithms/recursion/is_sorted.py | 77 ++ andz/algorithms/recursion/make_decimal.py | 86 ++ andz/algorithms/recursion/palindrome.py | 62 ++ andz/algorithms/recursion/power.py | 29 + andz/algorithms/recursion/reverse.py | 38 + andz/algorithms/sorting/README.md | 44 + andz/algorithms/sorting/__init__.py | 4 + .../algorithms/sorting/comparison/__init__.py | 4 + .../sorting/comparison/bubble_sort.py | 62 ++ .../sorting/comparison/heap_sort.py | 99 ++ .../sorting/comparison/insertion_sort.py | 60 ++ .../sorting/comparison/merge_sort.py | 341 +++++++ .../sorting/comparison/quick_sort.py | 109 +++ .../sorting/comparison/selection_sort.py | 48 + andz/algorithms/sorting/integer/__init__.py | 4 + .../sorting/integer/counting_sort.py | 474 ++++++++++ andz/algorithms/sorting/integer/radix_sort.py | 395 ++++++++ andz/ds/BST.py | 893 ++++++++++++++++++ andz/ds/BinaryHeap.py | 255 +++++ andz/ds/DisjointSets.py | 50 + andz/ds/DisjointSetsForest.py | 296 ++++++ andz/ds/HashTable.py | 46 + andz/ds/LinearProbingHashTable.py | 308 ++++++ andz/ds/MaxHeap.py | 114 +++ andz/ds/MinHeap.py | 131 +++ andz/ds/MinMaxHeap.py | 367 +++++++ andz/ds/Queue.py | 85 ++ andz/ds/RBT.py | 669 +++++++++++++ andz/ds/README.md | 66 ++ andz/ds/Stack.py | 121 +++ andz/ds/TST.py | 641 +++++++++++++ andz/ds/__init__.py | 4 + docs/big_o.md | 56 ++ docs/contributors.md | 3 + docs/general.md | 160 ++++ docs/history.md | 7 + docs/how_to_develop.md | 37 + docs/how_to_release.md | 19 + docs/objectives.md | 69 ++ docs/resources.md | 20 + docs/typical_se_and_python_issues.md | 61 ++ docs/warnings.md | 8 + pyproject.toml | 67 ++ setup.py | 9 + tests/README.md | 3 + tests/__init__.py | 4 + tests/algorithms/__init__.py | 4 + tests/algorithms/crypto/__init__.py | 4 + tests/algorithms/crypto/test_caesar.py | 86 ++ tests/algorithms/crypto/test_one_time_pad.py | 49 + tests/algorithms/crypto/util.py | 17 + tests/algorithms/dac/__init__.py | 4 + tests/algorithms/dac/test_binary_search.py | 85 ++ tests/algorithms/dac/test_find_extrema.py | 55 ++ tests/algorithms/dac/test_find_peak.py | 60 ++ tests/algorithms/dac/test_select.py | 56 ++ tests/algorithms/dp/__init__.py | 4 + tests/algorithms/dp/test_change_making.py | 58 ++ tests/algorithms/dp/test_fibonacci.py | 72 ++ tests/algorithms/matching/__init__.py | 4 + .../algorithms/matching/test_gale_shapley.py | 63 ++ tests/algorithms/numerical/__init__.py | 4 + .../polynomial_interpolation_tests.py | 69 ++ .../algorithms/numerical/test_barycentric.py | 50 + .../numerical/test_gradient_descent.py | 40 + tests/algorithms/numerical/test_horner.py | 38 + tests/algorithms/numerical/test_neville.py | 28 + tests/algorithms/numerical/test_newton.py | 44 + tests/algorithms/ode/README.md | 13 + tests/algorithms/ode/__init__.py | 4 + tests/algorithms/ode/test_forward_euler.py | 129 +++ tests/algorithms/recursion/__init__.py | 4 + tests/algorithms/recursion/test_ackermann.py | 53 ++ tests/algorithms/recursion/test_count.py | 41 + tests/algorithms/recursion/test_factorial.py | 62 ++ tests/algorithms/recursion/test_hanoi.py | 45 + tests/algorithms/recursion/test_is_sorted.py | 145 +++ .../algorithms/recursion/test_make_decimal.py | 63 ++ tests/algorithms/recursion/test_palindrome.py | 65 ++ tests/algorithms/recursion/test_power.py | 58 ++ tests/algorithms/recursion/test_reverse.py | 50 + tests/algorithms/sorting/__init__.py | 4 + tests/algorithms/sorting/base_tests.py | 69 ++ .../algorithms/sorting/comparison/__init__.py | 4 + .../sorting/comparison/test_bubble_sort.py | 29 + .../sorting/comparison/test_heap_sort.py | 28 + .../sorting/comparison/test_insertion_sort.py | 30 + .../sorting/comparison/test_merge_sort.py | 29 + .../sorting/comparison/test_quick_sort.py | 28 + .../sorting/comparison/test_selection_sort.py | 28 + tests/algorithms/sorting/integer/__init__.py | 4 + .../sorting/integer/test_counting_sort.py | 127 +++ .../sorting/integer/test_radix_sort.py | 40 + tests/ds/__init__.py | 4 + tests/ds/test_BST.py | 451 +++++++++ tests/ds/test_DisjointSetsForest.py | 168 ++++ tests/ds/test_LinearProbingHashTable.py | 240 +++++ tests/ds/test_MaxHeap.py | 182 ++++ tests/ds/test_MinHeap.py | 182 ++++ tests/ds/test_MinMaxHeap.py | 230 +++++ tests/ds/test_Queue.py | 91 ++ tests/ds/test_RBT.py | 37 + tests/ds/test_Stack.py | 84 ++ tests/ds/test_TST.py | 500 ++++++++++ 145 files changed, 13837 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 Makefile create mode 100644 README.md create mode 100755 andz/__init__.py create mode 100644 andz/algorithms/README.md create mode 100755 andz/algorithms/__init__.py create mode 100644 andz/algorithms/crypto/README.md create mode 100755 andz/algorithms/crypto/__init__.py create mode 100755 andz/algorithms/crypto/caesar.py create mode 100644 andz/algorithms/crypto/images/block-cipher.png create mode 100644 andz/algorithms/crypto/images/cbc-decryption.png create mode 100644 andz/algorithms/crypto/images/cbc-encryption.png create mode 100755 andz/algorithms/crypto/one_time_pad.py create mode 100644 andz/algorithms/dac/README.md create mode 100755 andz/algorithms/dac/__init__.py create mode 100755 andz/algorithms/dac/binary_search.py create mode 100755 andz/algorithms/dac/find_extrema.py create mode 100755 andz/algorithms/dac/find_peak.py create mode 100755 andz/algorithms/dac/select.py create mode 100644 andz/algorithms/dp/README.md create mode 100755 andz/algorithms/dp/change_making.py create mode 100755 andz/algorithms/dp/fibonacci.py create mode 100644 andz/algorithms/matching/__init__.py create mode 100644 andz/algorithms/matching/gale_shapley.py create mode 100644 andz/algorithms/numerical/README.md create mode 100644 andz/algorithms/numerical/barycentric.py create mode 100644 andz/algorithms/numerical/gradient_descent.py create mode 100644 andz/algorithms/numerical/horner.py create mode 100644 andz/algorithms/numerical/neville.py create mode 100644 andz/algorithms/numerical/newton.py create mode 100755 andz/algorithms/ode/__init__.py create mode 100755 andz/algorithms/ode/forward_euler.py create mode 100644 andz/algorithms/recursion/README.md create mode 100755 andz/algorithms/recursion/__init__.py create mode 100644 andz/algorithms/recursion/ackermann.py create mode 100755 andz/algorithms/recursion/count.py create mode 100755 andz/algorithms/recursion/factorial.py create mode 100644 andz/algorithms/recursion/hanoi.py create mode 100644 andz/algorithms/recursion/is_sorted.py create mode 100755 andz/algorithms/recursion/make_decimal.py create mode 100755 andz/algorithms/recursion/palindrome.py create mode 100755 andz/algorithms/recursion/power.py create mode 100755 andz/algorithms/recursion/reverse.py create mode 100644 andz/algorithms/sorting/README.md create mode 100755 andz/algorithms/sorting/__init__.py create mode 100755 andz/algorithms/sorting/comparison/__init__.py create mode 100755 andz/algorithms/sorting/comparison/bubble_sort.py create mode 100755 andz/algorithms/sorting/comparison/heap_sort.py create mode 100755 andz/algorithms/sorting/comparison/insertion_sort.py create mode 100755 andz/algorithms/sorting/comparison/merge_sort.py create mode 100755 andz/algorithms/sorting/comparison/quick_sort.py create mode 100755 andz/algorithms/sorting/comparison/selection_sort.py create mode 100755 andz/algorithms/sorting/integer/__init__.py create mode 100644 andz/algorithms/sorting/integer/counting_sort.py create mode 100644 andz/algorithms/sorting/integer/radix_sort.py create mode 100755 andz/ds/BST.py create mode 100755 andz/ds/BinaryHeap.py create mode 100644 andz/ds/DisjointSets.py create mode 100644 andz/ds/DisjointSetsForest.py create mode 100644 andz/ds/HashTable.py create mode 100644 andz/ds/LinearProbingHashTable.py create mode 100644 andz/ds/MaxHeap.py create mode 100755 andz/ds/MinHeap.py create mode 100644 andz/ds/MinMaxHeap.py create mode 100755 andz/ds/Queue.py create mode 100755 andz/ds/RBT.py create mode 100644 andz/ds/README.md create mode 100755 andz/ds/Stack.py create mode 100644 andz/ds/TST.py create mode 100755 andz/ds/__init__.py create mode 100644 docs/big_o.md create mode 100644 docs/contributors.md create mode 100644 docs/general.md create mode 100644 docs/history.md create mode 100644 docs/how_to_develop.md create mode 100644 docs/how_to_release.md create mode 100644 docs/objectives.md create mode 100644 docs/resources.md create mode 100644 docs/typical_se_and_python_issues.md create mode 100644 docs/warnings.md create mode 100644 pyproject.toml create mode 100755 setup.py create mode 100644 tests/README.md create mode 100755 tests/__init__.py create mode 100755 tests/algorithms/__init__.py create mode 100755 tests/algorithms/crypto/__init__.py create mode 100755 tests/algorithms/crypto/test_caesar.py create mode 100755 tests/algorithms/crypto/test_one_time_pad.py create mode 100755 tests/algorithms/crypto/util.py create mode 100755 tests/algorithms/dac/__init__.py create mode 100644 tests/algorithms/dac/test_binary_search.py create mode 100644 tests/algorithms/dac/test_find_extrema.py create mode 100644 tests/algorithms/dac/test_find_peak.py create mode 100644 tests/algorithms/dac/test_select.py create mode 100644 tests/algorithms/dp/__init__.py create mode 100644 tests/algorithms/dp/test_change_making.py create mode 100644 tests/algorithms/dp/test_fibonacci.py create mode 100644 tests/algorithms/matching/__init__.py create mode 100644 tests/algorithms/matching/test_gale_shapley.py create mode 100755 tests/algorithms/numerical/__init__.py create mode 100644 tests/algorithms/numerical/polynomial_interpolation_tests.py create mode 100644 tests/algorithms/numerical/test_barycentric.py create mode 100644 tests/algorithms/numerical/test_gradient_descent.py create mode 100644 tests/algorithms/numerical/test_horner.py create mode 100644 tests/algorithms/numerical/test_neville.py create mode 100644 tests/algorithms/numerical/test_newton.py create mode 100644 tests/algorithms/ode/README.md create mode 100755 tests/algorithms/ode/__init__.py create mode 100644 tests/algorithms/ode/test_forward_euler.py create mode 100755 tests/algorithms/recursion/__init__.py create mode 100644 tests/algorithms/recursion/test_ackermann.py create mode 100644 tests/algorithms/recursion/test_count.py create mode 100644 tests/algorithms/recursion/test_factorial.py create mode 100644 tests/algorithms/recursion/test_hanoi.py create mode 100644 tests/algorithms/recursion/test_is_sorted.py create mode 100644 tests/algorithms/recursion/test_make_decimal.py create mode 100644 tests/algorithms/recursion/test_palindrome.py create mode 100644 tests/algorithms/recursion/test_power.py create mode 100644 tests/algorithms/recursion/test_reverse.py create mode 100755 tests/algorithms/sorting/__init__.py create mode 100644 tests/algorithms/sorting/base_tests.py create mode 100755 tests/algorithms/sorting/comparison/__init__.py create mode 100755 tests/algorithms/sorting/comparison/test_bubble_sort.py create mode 100644 tests/algorithms/sorting/comparison/test_heap_sort.py create mode 100644 tests/algorithms/sorting/comparison/test_insertion_sort.py create mode 100644 tests/algorithms/sorting/comparison/test_merge_sort.py create mode 100644 tests/algorithms/sorting/comparison/test_quick_sort.py create mode 100644 tests/algorithms/sorting/comparison/test_selection_sort.py create mode 100755 tests/algorithms/sorting/integer/__init__.py create mode 100644 tests/algorithms/sorting/integer/test_counting_sort.py create mode 100644 tests/algorithms/sorting/integer/test_radix_sort.py create mode 100755 tests/ds/__init__.py create mode 100755 tests/ds/test_BST.py create mode 100755 tests/ds/test_DisjointSetsForest.py create mode 100755 tests/ds/test_LinearProbingHashTable.py create mode 100755 tests/ds/test_MaxHeap.py create mode 100755 tests/ds/test_MinHeap.py create mode 100755 tests/ds/test_MinMaxHeap.py create mode 100644 tests/ds/test_Queue.py create mode 100755 tests/ds/test_RBT.py create mode 100644 tests/ds/test_Stack.py create mode 100644 tests/ds/test_TST.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ed1d124d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,62 @@ +name: Release + +on: + push: + tags: + - '*.*.*' + +jobs: + + tests: + uses: ./.github/workflows/tests.yml + + release: + name: Release + runs-on: ubuntu-latest + needs: [tests] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Poetry + run: curl -sSL https://install.python-poetry.org | python - -y + + - name: Update PATH + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Check package version and tag are equal + run: | + if [[ ${{ github.ref_name }} != "$(poetry version --short)" ]]; then + echo "Tag = ${{ github.ref_name }} != $(poetry version --short)" + exit 1 + fi + + - name: Build package + run: poetry build + + - name: Check if pre-release version + id: check_version + run: | + [[ "$(poetry version --short)" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || echo pre_release=true >> $GITHUB_OUTPUT + + - name: Create GitHub release + uses: ncipollo/release-action@v1 + with: + artifacts: "dist/*" + token: ${{ secrets.GITHUB_TOKEN }} + allowUpdates: true + replacesArtifacts: true + updateOnlyUnreleased: true + generateReleaseNotes: true + prerelease: ${{ steps.check_version.outputs.pre_release == 'true' }} + + - name: Publish to PyPI + env: + POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} + run: poetry publish diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..145f5316 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,46 @@ +name: Tests + +run-name: ${{ github.event.head_commit.message }} + +on: + workflow_call: + + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + + tests: + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest] + python-version: ["3.9", "3.10", "3.11", "3.12"] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry + run: curl -sSL https://install.python-poetry.org | python - -y + + - name: Update PATH + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Install dependencies + run: make dev + + - name: Run checks + run: make check_format + + - name: Run tests + run: make test diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..abe9acb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +__pycache__/ +__pypackages__/ +build/ +dist/ +.mypy_cache/ +.pytest_cache +.coverage +.env +env/ +venv/ +.idea/ +poetry.lock +.python-version +CHANGELOG.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..ec02cce6 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015 Nelson Brochado (aka nbro) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ac0b1c7f --- /dev/null +++ b/Makefile @@ -0,0 +1,58 @@ +POETRY := $(shell command -v poetry 2> /dev/null) + +.PHONY: help +help: ## Show this help + @egrep -h '\s##\s' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m %-30s\033[0m %s\n", $$1, $$2}' + +.DEFAULT_GOAL := help + +.PHONY: poetry_installed +poetry_installed: +ifndef POETRY + @echo "Poetry does not seem to be installed, but it's required." + @echo "You can install it by typing: make install_poetry" + @echo "For more details about how you can install poetry, see https://python-poetry.org/" + @exit 1 +endif + +.PHONY: install_poetry +install_poetry: ## Install poetry +ifndef POETRY + curl -sSL https://install.python-poetry.org | python3 - +endif + +.PHONY: dev +dev: poetry_installed ## Setup the development environment + poetry --version + poetry run python --version + poetry check || poetry update + poetry install --all-extras + poetry env info + poetry show + +.PHONY: test +test: poetry_installed ## Run the tests + poetry run coverage run -m pytest +# poetry run coverage run --source=. -m unittest discover -s tests -v + poetry run coverage report + +.PHONY: format +format: poetry_installed ## Format the code + poetry run isort andz tests + poetry run black andz tests + +.PHONY: check_format +check_format: poetry_installed ## Check if the code is formatted + poetry run isort --check --diff andz tests + poetry run black --check --diff andz tests + +.PHONY: check_types +check_types: poetry_installed ## Run type-checks + poetry run mypy andz + +.PHONY: check_style +check_style: poetry_installed ## Run style checks + poetry run pylint andz tests + +.PHONY: check +check: check_format check_types check_style ## Run all quality assurance checks diff --git a/README.md b/README.md new file mode 100644 index 00000000..7ee63703 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# Algorithms and Data Structures (andz) + +[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/) +[![](https://img.shields.io/badge/stability-experimental-red.svg)](http://www.engr.sjsu.edu/fayad/SoftwareStability/) +[![Packagist](https://img.shields.io/packagist/l/doctrine/orm.svg?maxAge=2592000)](./LICENSE.md) +[![Tests](https://github.com/nbro/andz/actions/workflows/tests.yml/badge.svg)](https://github.com/nbro/andz/actions/workflows/tests.yml) + +## Introduction + +`andz` stands for **a**lgorithms a**n**d **d**ata structure**z**. + +> The `s` was replaced with `z` because +> there was already a dummy package called `ands` on PyPI. + +In this package, you can find some of the most common algorithms and +data structures studied in Computer Science, +such as quick-sort or binary-search trees. +The algorithms are divided into main categories, +such as sorting algorithms or dynamic programming algorithms, but note that +some algorithms and DS can fall into multiple categories . + +> The current main goal of this project is for me to learn more about +new algorithms and data structures, but I hope these implementations +can also be useful to anyone interested in them. + +## Development + +I use + +- [poetry](https://python-poetry.org/) for development +- the [`Makefile`](./Makefile) to declare common commands +- [`pyenv`](https://github.com/pyenv/pyenv) to manage different Python versions locally +- GitHub Actions for CI/CD + +For more info about how to develop, +see [`./docs/how_to_develop.md`](docs/how_to_develop.md). + +## How to install? + +``` +pip install andz +``` + +## Documentation + +Most modules and functions have been thoroughly documented, +so the best way to learn about the algorithms and data structures is +to read the source code and the related docstrings and comments. + +You can find more documentation under [`docs`](./docs). +There you will find the history of the project, +development conventions that should be adapted, etc. diff --git a/andz/__init__.py b/andz/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/andz/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/andz/algorithms/README.md b/andz/algorithms/README.md new file mode 100644 index 00000000..19f648f7 --- /dev/null +++ b/andz/algorithms/README.md @@ -0,0 +1,228 @@ +# Algorithms + +The subpackage that contains all implemented algorithms in `andz`. + +A few interesting algorithms which may be implemented in the future. It excludes the sorting algorithms, which you can find under [`sorting/README.md`](./sorting/README.md). + +## Combinatorial Algorithms + +### Pseudo-random Number Generators + +- [Mersenne Twister](https://en.wikipedia.org/wiki/Mersenne_Twister) + +## Cryptography + +### Asymmetric Public Key Encryption + +- [RSA](https://en.wikipedia.org/wiki/RSA_(cryptosystem)) (Rivest–Shamir–Adleman) + +## Geometry + +### [Line-Segment Intersection](https://en.wikipedia.org/wiki/Line_segment_intersection) + +- [Bentley–Ottmann algorithm](https://en.wikipedia.org/wiki/Bentley%E2%80%93Ottmann_algorithm) + +### Convex Hull + +- [Graham's scan](https://en.wikipedia.org/wiki/Graham_scan) +- [Gift wrapping](https://en.wikipedia.org/wiki/Gift_wrapping_algorithm) (or Jarvis march) +- [Chan's algorithm](https://en.wikipedia.org/wiki/Chan%27s_algorithm) +- [Kirkpatrick–Seidel algorithm](https://en.wikipedia.org/wiki/Kirkpatrick%E2%80%93Seidel_algorithm) +- [Quickhull](https://en.wikipedia.org/wiki/Quickhull) +- Divide and conquer + +## Graph Algorithms + +### Search + +- [Breadth-first search](https://en.wikipedia.org/wiki/Breadth-first_search) +- [Depth-first search](https://en.wikipedia.org/wiki/Depth-first_search) +- [Iterative deepening depth-first search](https://en.wikipedia.org/wiki/Iterative_deepening_depth-first_search) +- [Best-first Search](https://en.wikipedia.org/wiki/Best-first_search) +- Uniform-Cost Search +- [A* search algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm) +- [Beam search](https://en.wikipedia.org/wiki/Beam_search) +- [Monte Carlo tree search](https://en.wikipedia.org/wiki/Monte_Carlo_tree_search) (MCTS) + +### Shortest Path + +- [Dijkstra's algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) +- [Bellman–Ford algorithm](https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm) +- [Floyd–Warshall algorithm](https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm) + +### Minimum Spanning Tree + +- [Kruskal's algorithm](https://en.wikipedia.org/wiki/Kruskal%27s_algorithm) +- [Prim's algorithm](https://en.wikipedia.org/wiki/Prim%27s_algorithm) +- [Reverse-delete algorithm](https://en.wikipedia.org/wiki/Reverse-delete_algorithm) + +### Strongly Connected Components + +- [Kosaraju's algorithm](https://en.wikipedia.org/wiki/Kosaraju%27s_algorithm) +- [Tarjan's algorithm](https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm) + +### Network Theory + +#### Network analysis + +- [PageRank](https://en.wikipedia.org/wiki/PageRank) + +##### Flow Networks + +- [Edmonds–Karp algorithm](https://en.wikipedia.org/wiki/Edmonds%E2%80%93Karp_algorithm) + +### Partitioning + +- [Kernighan–Lin algorithm](https://en.wikipedia.org/wiki/Kernighan%E2%80%93Lin_algorithm) +- [Inertial Partitioning Algorithm](http://people.eecs.berkeley.edu/~demmel/cs267/lecture18/lecture18.html) +- [Spectral Bisection Algorithm](http://people.eecs.berkeley.edu/~demmel/cs267/lecture18/lecture18.html) + +### Travelling Salesman Problem (TSP) + +- [Nearest neighbour algorithm](https://en.wikipedia.org/wiki/Nearest_neighbour_algorithm) +- [Christofides algorithm](https://en.wikipedia.org/wiki/Christofides_algorithm) + +## Number Theory + +### Multiplication + +- [Karatsuba algorithm](https://en.wikipedia.org/wiki/Karatsuba_algorithm) + +### Greatest Common Divisor + +- [Euclidean algorithm](https://en.wikipedia.org/wiki/Euclidean_algorithm) (or Euclid's algorithm) + +### Primality Tests + +- [AKS primality test](https://en.wikipedia.org/wiki/AKS_primality_test) +- [Miller–Rabin primality test](https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test) +- [Sieve of Eratosthenes](https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes) +- [Sieve of Atkin](https://en.wikipedia.org/wiki/Sieve_of_Atkin) + +## Numerical algorithms + +### Differential Equations + +- [Runge–Kutta methods](https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods) + +### Interpolation + +- [Divided Differences](https://en.wikipedia.org/wiki/Divided_differences) +- [De Casteljau's algorithm](https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm) +- [De Boor algorithm](https://en.wikipedia.org/wiki/De_Boor%27s_algorithm) + +### Linear algebra + +#### Linear System of Equations + +- [Jacobi method](https://en.wikipedia.org/wiki/Jacobi_method) +- [Conjugate gradient method](https://en.wikipedia.org/wiki/Conjugate_gradient_method) +- [Guassian elimination](https://en.wikipedia.org/wiki/Gaussian_elimination) +- [Guassian elimination with pivoting](https://en.wikipedia.org/wiki/Pivot_element) +- [Gauss–Seidel method](https://en.wikipedia.org/wiki/Gauss%E2%80%93Seidel_method) +- [LU decomposition](https://en.wikipedia.org/wiki/LU_decomposition) (or factorization) +- [Cholesky decomposition](https://en.wikipedia.org/wiki/Cholesky_decomposition) + +#### Sparse Matrix Algorithms + +- [Cuthill–McKee algorithm](https://en.wikipedia.org/wiki/Cuthill%E2%80%93McKee_algorithm) + +#### Matrices + +##### Multiplication + +- [Strassen algorithm](https://en.wikipedia.org/wiki/Strassen_algorithm) +- [Cannon's algorithm](https://en.wikipedia.org/wiki/Cannon%27s_algorithm) +- [Coppersmith–Winograd algorithm](https://en.wikipedia.org/wiki/Coppersmith%E2%80%93Winograd_algorithm) +- [Block matrix multiplication](https://en.wikipedia.org/wiki/Block_matrix#Block_matrix_multiplication) + +##### Eigenvalues + +- [Rayleigh quotient iteration](https://en.wikipedia.org/wiki/Rayleigh_quotient_iteration) +- [Power Method](https://en.wikipedia.org/wiki/Power_iteration) +- [QR algorithm](https://en.wikipedia.org/wiki/QR_algorithm) +- [Inverse iteration](https://en.wikipedia.org/wiki/Inverse_iteration) + +#### Orthogonalization + +- [Gram–Schmidt process](https://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process) + +### Roots Finding + +- [Bisection method](https://en.wikipedia.org/wiki/Bisection_method) +- [Secant method](https://en.wikipedia.org/wiki/Secant_method) +- [Halley's method](https://en.wikipedia.org/wiki/Halley%27s_method) + +## Optimization Algorithms + +### Meta-heuristics + +- [Simulated annealing](https://en.wikipedia.org/wiki/Simulated_annealing) + +### 2-player games + +- [Alpha-beta pruning](https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning) +- [Min-max](https://en.wikipedia.org/wiki/Minimax#Minimax_algorithm_with_alternate_moves) + +### Evolutionary computation + +#### Genetic Algorithms + +- [Fitness proportionate selection](https://en.wikipedia.org/wiki/Fitness_proportionate_selection) +- [Truncation selection](https://en.wikipedia.org/wiki/Truncation_selection) +- [Tournament selection](https://en.wikipedia.org/wiki/Tournament_selection) + +#### Swarm Intelligence + +- [Ant colony optimization](https://en.wikipedia.org/wiki/Ant_colony_optimization_algorithms) +- [Bees algorithm](https://en.wikipedia.org/wiki/Bees_algorithm) +- [Particle swarm optimization](https://en.wikipedia.org/wiki/Particle_swarm_optimization) + +### Non-linear Least Squares + +- [Gauss-Newton algorithm](https://en.wikipedia.org/wiki/Gauss%E2%80%93Newton_algorithm) + +## Machine Learning + +### Unsupervised + +- [k-means](https://en.wikipedia.org/wiki/K-means_clustering) + +## Supervised + +- [k-nearest neighbors algorithm](https://www.youtube.com/watch?v=UqYde-LULfs) + +## Reinforcement + +- Tabular Q-Learning +- Tabular SARSA + +## Programming Language Theory + +### Parsing + +- [CYK algorithm](https://en.wikipedia.org/wiki/CYK_algorithm) +- [LL parser](https://en.wikipedia.org/wiki/LL_parser) +- [LR parser](https://en.wikipedia.org/wiki/LR_parser) +- [Earley parser](https://en.wikipedia.org/wiki/Earley_parser) +- [Recursive descent parser](https://en.wikipedia.org/wiki/Recursive_descent_parser) + +## Sequence Algorithms + +### Approximate Sequence Matching + +#### String Metrics + +- [Levenshtein edistance](https://en.wikipedia.org/wiki/Levenshtein_distance) (or edit distance) +- [Hamming distance](https://en.wikipedia.org/wiki/Hamming_distance) + +### Sequence Permutations + +- [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) + +---- + +An article listing other possibly interesting algorithms can be found at the +following URL [https://en.wikipedia.org/wiki/List\_of\_algorithms](https://en.wikipedia.org/wiki/List_of_algorithms). + +--- \ No newline at end of file diff --git a/andz/algorithms/__init__.py b/andz/algorithms/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/andz/algorithms/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/andz/algorithms/crypto/README.md b/andz/algorithms/crypto/README.md new file mode 100644 index 00000000..e8f1d0a2 --- /dev/null +++ b/andz/algorithms/crypto/README.md @@ -0,0 +1,59 @@ +# Cryptography + +Nowadays, there are two main classes of symmetric encryption techniques (i.e. ciphers where both keys of the sender and receiver are the same): _stream ciphers_ and _block ciphers_. We can also distinguish between "symmetric private-key cryptography" (i.e. where keys are private but equal for both communicating parties) and "asymmetric public-private key cryptography" (i.e. where a key is public and the other is private). + +## Stream Ciphers + +One symbol in the plain text is convert into a symbol of cipher text. Examples of stream ciphers, shift ciphers, substitution ciphers, one-time pad. + +## Block Ciphers + +The message to be encrypted is processed in blocks of k bits. For example, if k = 64, then the message is broken into 64-bit blocks, and each block is encrypted independently. To encode a block, the cipher uses a one-to-one mapping to map the k-bit block of cleartext to a k-bit block of ciphertext + + + + +## Cipher Block Chaining + +Idea: use previous blocks in the encryption of the current block. Encryption is not just based on the current block, but on the entire message up to the current block. + +### Encryption + + + +### Decryption + + + + +CBC solve the problem of any two equal blocks within a message being encrypted in the same way (as in a normal block cipher). So, in this kind of cryptography system, equal blocks within a message are encrypted in a different, but identical messages still produce the same final cipher text. We need to add randomness to this protocol to make it more secure... + +## What is Secrecy? + +A scheme is secret if we can learn nothing about the original from the cipher text. + +More formally, given a set of keys $K$, for two messages $m_1 \neq m_2$, for any ciphertext $c$, then we have $$P_{k \in K} \left[ e(m_1, k) = c\right]=P_{k \in K} \left[ e(m_2, k) = c\right]$$ + +In other words, given $c$, every plain text is equiprobable. + +The one-time pad algorithm provides perfect secracy, but it has a drawback: the key used in the encryption and decryption of the algorithm must be as big as the message itself. + + +## TODO + +- A block cipher algorithm + - a cipher block chaining +- A substitution cipher +- RSA + + +## Reading and watching Suggestions + +- [Journey into cryptography](https://www.khanacademy.org/computing/computer-science/cryptography), a course by Khan Academy + +- [Cryptography I](https://www.coursera.org/course/crypto), a course by Coursera/Stanford + +- [Cryptography](https://en.wikipedia.org/wiki/Cryptography), an article by Wikipedia + +- [Computer Networking: A Top-Down Approach](http://www.amazon.com/Computer-Networking-Top-Down-Approach-Edition/dp/0132856204), a book by Kurose and Ross (6th ed.) + diff --git a/andz/algorithms/crypto/__init__.py b/andz/algorithms/crypto/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/andz/algorithms/crypto/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/andz/algorithms/crypto/caesar.py b/andz/algorithms/crypto/caesar.py new file mode 100755 index 00000000..4700832c --- /dev/null +++ b/andz/algorithms/crypto/caesar.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 04/08/2015 + +Updated: 18/09/2017 + +# Description + +Caesar cipher (also known as "shift" or "substitution" cipher) is one of the +simplest forms of encryption, where each letter in the original message (called +the "plaintext") is replaced with a letter corresponding to a certain number of +letters up or down in the alphabet. + +What does "certain number of letters up or down in the alphabet" actually mean? +Essentially we need to map letters of our alphabet to numbers. For example, we +may map the English alphabet to the numbers from 0 to 25 (for lower case +letters, for simplicity). In this way, after the shifts, the original message is +not understandable at first glance, unless you know how you shifted the letters +of your message. + +## Example + +For example, suppose we want to encrypt the plaintext m = "abc". Suppose further +that our alphabet is the English alphabet (of lower case letters from 'a' to +'z'). So we have an alphabet of 26 symbols or, more commonly known, letters. + +The general formula of the caesar cipher goes like this: + + e(x) := (x + k) mod 26 + +where x is a numerical representation of a character, k is non-negative number +and mod is the modulo function, i.e. a function that returns the remainder of +the division between (x + k) and 26. In practice, this allows us to have numbers +always smaller than 26. + +This function e(x) is applied to all integer representations of the characters +of a certain message. The integer representations may depend on the programming +language, environment, etc. + +Lets suppose that we have a map like this: 'a' maps to 0, 'b' to 1, and so on +until 'z', which maps to 25. Let's call this function that maps the characters +to integers g and h the one that maps integers to characters. + +Now, suppose, as an example, that k = 3, and let's try to apply the function +e(x) to all characters of the message m = "abc" previously mentioned. Let's +start by converting the characters to their integer representation, just to have +a clear visualization. + + x1 = g('a') = 0 + x2 = g('b') = 1 + x3 = g('c') = 2 + +so, let's apply + + e(x1) = (x1 + k) mod 26 + e(0) = (0 + 3) mod 26 + e(0) = 3 mod 26 = 3 + +we do the same for the other characters and we obtain; + + e(1) = (1 + 3) mod 26 + e(1) = 4 mod 26 = 4 + +and + + e(2) = (2 + 3) mod 26 + e(2) = 5 mod 26 = 5 + +Now we convert the resulting numbers to their corresponding characters, that is + + h(e(0) = 3) = 'd' + h(e(1) = 4) = 'e' + h(e(2) = 5) = 'f' + +Thus the plaintext m = "abc" has been converted to "def". + +The decryption phase works in the reverse direction, i.e. instead of using k, we +use -k, that is the general decryption formula can be described as follows + + d(x) := (x - k) mod 26 + +where x is in this case a integer representation of a character, not from the +plaintext, but from an encrypted text using the previously described Caesar +encryption algorithm. + +# Implementation + +The following implementation accepts messages of characters whose value returned +by the ord function is between 0 and 2^16 - 1. + +# Comments + +Caesar cipher is not a good cryptographic algorithm, so you should never use it +to encrypt your messages! + +# TODO + +- Add complexity analysis + +# References + +- https://learncryptography.com/classical-encryption/caesar-cipher +- https://en.wikipedia.org/wiki/Ciphertext +- https://en.wikipedia.org/wiki/Plaintext +""" + +from random import choice + +__all__ = [ + "encrypt", + "decrypt", + "encrypt_with_multiple_keys", + "decrypt_with_multiple_keys", +] + +# A number which conventionally represents the maximum number that the function +# ord can return, since it returns the Unicode code point for a one-character +# strings (assuming that 16-bit Unicode system). +MAX_MAPPED_INT = 2**16 - 1 + + +def _move_char(c: str, k: int) -> str: + return chr(ord(c) + k) + + +def encrypt(plaintext: str, k: int) -> str: + """Given a string plaintext and a non-negative key k, it returns the + encrypted version of plaintext with the key k using the Caesar cipher + algorithm, over an alphabet of possible maximum value MAX_MAPPED_INT.""" + return "".join(_move_char(c, k) for c in plaintext) + + +def decrypt(ciphertext: str, k: int) -> str: + """Reverts the operation performed by encrypt.""" + return "".join(_move_char(c, -k) for c in ciphertext) + + +# Example of poly-alphabetic encryption + + +def encrypt_with_multiple_keys(plaintext: str, keys: list) -> tuple: + """Given a message plaintext and a set keys, it encrypts each symbol of + plaintext with a random key from keys. + + The random pattern of keys chosen is the second item of the tuple + returned.""" + keys_used = [] + ciphertext = [] + + for c in plaintext: + k = choice(keys) + keys_used.append(k) + ciphertext.append(_move_char(c, k)) + + return "".join(ciphertext), keys_used + + +def decrypt_with_multiple_keys(ciphertext: str, keys_used: list) -> str: + """Reverts the operation performed by encrypt_with_multiple_keys. + + Assumes keys_used is the list of keys used to encrypt a certain plaintext, + such that len(ciphertext) == len(plaintext).""" + return "".join(_move_char(ciphertext[i], -k) for i, k in enumerate(keys_used)) diff --git a/andz/algorithms/crypto/images/block-cipher.png b/andz/algorithms/crypto/images/block-cipher.png new file mode 100644 index 0000000000000000000000000000000000000000..7d7b8e43493dd22c36aac27a8ed1cf4726043ef1 GIT binary patch literal 318620 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∋nF#Djr3C{{&hzHaMW0Cuo3BQia>MX*}8PVf*+na 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΁GO@%nT8%WCisq+AN;92jKP2P!nS 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 literal 0 HcmV?d00001 diff --git a/andz/algorithms/crypto/images/cbc-encryption.png b/andz/algorithms/crypto/images/cbc-encryption.png new file mode 100644 index 0000000000000000000000000000000000000000..2c2506e22d1ea733b03814e376d05b04df65ee5a GIT binary patch literal 87559 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%0Ai!t_OB;J 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 1, + 'b' -> 2, + 'c' -> 3, + ... + 'z' -> 26 + +Let's call this mapping function h. Let's call the reversing function f. + +The general one-time pad algorithm proceeds as follows. + + let c be an empty string + (c will represent the ciphertext at the end of the algorithm) + + iterate through message m: + + let n be the current iteration + let i me the nth character of m + let j me the nth character of k + let h(i) be the integer representation of i + let h(j) be the integer representation of j + + x = XOR(h(i), h(j)) + y = f(x) + + c = c + y + +Thus the one-time pad algorithm to encrypt the message m = "hi" with the +randomly generated key k = "ab" proceeds as follows. + + + c = "" + + Let n = 1. + + i = 'h' + j = 'a' + h(i) = 8 + h(j) = 1 + + In binary, h(i) = 8 is 1000 and h(j) = 1 is 0001. + + x = XOR(1000, 0001) = 1001, which is 9 in decimal. + y = f(1001) = 'i' + + c = '' + 'i' = "i" + + Let n = 2. + + i = 'i' + j = 'b' + h(i) = 9 + h(j) = 2 + + In binary, h(i) = 9 is 1001 and h(j) = 2 is 0010. + + x = XOR(1001, 0010) = 1011, which is 11 in decimal. + y = f(1011) = 'k' + + c = 'i' + 'k' = "ik" + +So the encrypted text c = "ik". + +To decrypt we simply apply the one-time pad with the same key but to c = "ik". + +### How does the XOR operation work in general? + +Given two binary strings A and B (over a binary alphabet consisting of the +symbols 0 and 1) then the XOR operation works as follows + + +---+---+---------+ + | A | B | A XOR B | + +---+---+---------+ + | 0 | 0 | 0 | + | 1 | 1 | 0 | + | 1 | 0 | 1 | + | 0 | 1 | 1 | + +---+---+---------+ + +In other words, the output is 1 only when the two inputs are different. + +### Why to decrypt we use the XOR on the ciphertext (with the same secret key) +and the result is the plain text? + +In short, it's because XOR is its own inverse! + +#### Example + +Suppose we have two binary strings X=101 and K=001, where K is the key. Let's +apply the XOR operation on this two strings. + + C = X XOR K = 101 + 001 XOR + ------- + 100 + + X = C XOR K = 100 + 001 XOR + ------- + 101 + +This works in general because, at position i of the strings: + +- If you have two 0s, the result will be 0, which XORed with 0, gives again 0. + +- If you have two 1s, the result will be 0, which XORed with 1, gives 1. + +- If you have message 0 and key 1, the the result will be 1, which XORed with 1, +gives 0. + +- Similarly, if you have message 1 and key 0, the the result will be 1, which +XORed with 0, gives 1. + +## Notes + +- one-time pad provides "perfect" secrecy +- one-time pad requires a key of the same length of the plaintext +- one-time pad may be impractical for messages greater than a certain length + +# TODO + +- Implement OTP using module arithmetic (i.e. modular addition for encryption +and modular subtraction for decryption). + +- Add complexity analysis. + +# References + +- https://learncryptography.com/classical-encryption/one-time-pad +- https://www.khanacademy.org/computing/computer-science/cryptography/crypt/v/one-time-pad +- http://python-reference.readthedocs.io/en/latest/docs/operators/bitwise_XOR.html +- https://en.wikipedia.org/wiki/One-time_pad +- http://crypto.stackexchange.com/questions/59/taking-advantage-of-one-time-pad-key-reuse +- http://crypto.stackexchange.com/questions/41798/one-time-pad-xor-question +- http://crypto.stackexchange.com/questions/33065/is-all-of-encryption-based-on-xor/ +""" + +__all__ = ["encrypt", "decrypt"] + + +def encrypt(plaintext: str, key: str) -> str: + """Encrypts plaintext using key according to the one-time-pad algorithm.""" + return "".join(chr(ord(p) ^ ord(k)) for (p, k) in zip(plaintext, key)) + + +def decrypt(ciphertext: str, key: str) -> str: + """Decrypts ciphertext using key according to the one-time-pad algorithm.""" + return encrypt(ciphertext, key) diff --git a/andz/algorithms/dac/README.md b/andz/algorithms/dac/README.md new file mode 100644 index 00000000..c1516897 --- /dev/null +++ b/andz/algorithms/dac/README.md @@ -0,0 +1,24 @@ +# [Divide and conquer algorithms](https://en.wikipedia.org/wiki/Divide_and_conquer_algorithm) + +A divide and conquer algorithm works by _recursively_ breaking down a problem into +two or more _sub-problems_ of the same or related type, until these become simple +enough to be solved directly. + +The solutions to the sub-problems are then combined to give a solution to the +original problem. + +The correctness of a divide and conquer algorithm is usually proved by +_mathematical induction_, and its computational cost is often determined by +solving _recurrence relations_. + +## Important concepts + +- [Recursion](https://en.wikipedia.org/wiki/Recursion_(computer_science)) +- Sub-problems +- [Mathematical induction](https://en.wikipedia.org/wiki/Mathematical_induction) +- [Recurrence relations](https://en.wikipedia.org/wiki/Recurrence_relation) +- [Master theorem](https://en.wikipedia.org/wiki/Master_theorem) + +### Resources + +- [https://brilliant.org/wiki/master-theorem/](https://brilliant.org/wiki/master-theorem/) \ No newline at end of file diff --git a/andz/algorithms/dac/__init__.py b/andz/algorithms/dac/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/andz/algorithms/dac/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/andz/algorithms/dac/binary_search.py b/andz/algorithms/dac/binary_search.py new file mode 100755 index 00000000..a871ca6f --- /dev/null +++ b/andz/algorithms/dac/binary_search.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 23/08/2015 + +Updated: 18/09/2017 + +# Description + +Binary search (or less commonly known as "half-interval search" or "logarithmic +search") is a "divide and conquer" search algorithm that operates on a sorted +list. + +Binary search compares the target value to the middle element of the list. If +they are unequal, the half in which the target cannot lie is eliminated and the +search continues on the remaining half until it is successful or the remaining +half is empty. + +## Example + +Suppose we have the following sorted list A and we want to find the element +k = 3. + + +-------------------+ + | 3 | 5 | 6 | 7 | 9 | + +-------------------+ + +Then the binary-search algorithm proceeds as follows. We look at the element in +the middle of A, that is, in our case, it's 6. We check if k == 6. Since k == 3, +then k != 6. Since the list is sorted, then k can only be on the left side of 6, +thus we now check in the left sub-list A1 of A + + +-------+ + | 3 | 5 | + +-------+ + +We again look at the middle element of the new list A1. Since this list A1 +contains an even number of elements, we can decide arbitrarily if the middle is +3 or 5 (in our case). Let's decide that the middle element is 3. We compare it +with k and we find out that it's 3, thus we have just found our target. + +## Note + +Depending on the application, we can decide to return either the index in the +list of the target element, or we can simply return true or false to indicate +respectively that the target is or not in our list. If we decide to return an +index, if the target element is not found, we can return for example -1. +""" + +from andz.algorithms.recursion.is_sorted import pythonic_is_sorted + +__all__ = [ + "linear_search", + "binary_search_iteratively", + "binary_search_recursively_in_place", + "binary_search_recursively_not_in_place", +] + + +def linear_search(ls: list, item: object) -> bool: + """Searches for item in the list ls. + + It returns the index such that ls[index] == item, if item is in the list ls, + otherwise it returns -1. + + Time complexity: O(n), where n is the size of ls.""" + assert pythonic_is_sorted(ls) + for index, e in enumerate(ls): + if e == item: + return index + return -1 + + +def binary_search_recursively_not_in_place(ls: list, item: object) -> bool: + """Recursively binary-searches item in the list ls, which is assumed to be + sorted in increasing order. + + It returns true if item is in ls, false otherwise. + + Note: this algorithm uses the slice operator, which creates a sub-lists. + Slicing is an operation that runs in O(k) time.""" + assert pythonic_is_sorted(ls) + if len(ls) == 0: # basis + return False + mid = len(ls) // 2 + if ls[mid] == item: + return True + if ls[mid] < item: + return binary_search_recursively_not_in_place(ls[mid + 1 :], item) + return binary_search_recursively_not_in_place(ls[0:mid], item) + + +def _binary_search_recursively_in_place( + ls: list, item: object, start: int, end: int +) -> bool: + if end < start: + return -1 + mid = (start + end) // 2 + if ls[mid] == item: + return mid + if ls[mid] < item: + return _binary_search_recursively_in_place(ls, item, mid + 1, end) + return _binary_search_recursively_in_place(ls, item, start, mid - 1) + + +def binary_search_recursively_in_place(ls: list, item: object) -> bool: + """Recursively binary-searches item in the list ls, assuming that ls is + sorted in increasing order. + + It returns the index such that ls[index] == item, if item is in the list ls, + otherwise it returns -1. + + This algorithm, as opposed to binary_search_recursively_not_in_place, does + not create sub-lists during the recursion process.""" + assert pythonic_is_sorted(ls) + return _binary_search_recursively_in_place(ls, item, 0, len(ls) - 1) + + +def binary_search_iteratively(ls: list, item: object) -> bool: + """Iteratively binary-searches for item in the ls, which is assumed to be + sorted in increasing order. + + It returns the index such that ls[index] == item, if item is in the list ls, + otherwise it returns -1. + + Time complexity: O(log(n)).""" + assert pythonic_is_sorted(ls) + if len(ls) == 0: + return -1 + start = 0 + end = len(ls) - 1 + while start <= end: + mid = (start + end) // 2 + if ls[mid] == item: + return mid + if ls[mid] < item: # search on the right + start = mid + 1 + else: # search on the left + end = mid - 1 + return -1 diff --git a/andz/algorithms/dac/find_extrema.py b/andz/algorithms/dac/find_extrema.py new file mode 100755 index 00000000..1e8f5b55 --- /dev/null +++ b/andz/algorithms/dac/find_extrema.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 23/08/2015 + +Updated: 18/09/2017 + +# Description + +Finding the maximum (or minimum) of a list of numbers (or, in general, +comparable objects) using the "divide and conquer" strategy. +""" + +__all__ = [ + "find_extremum_not_in_place", + "find_extremum_in_place", + "find_max", + "find_min", +] + + +def find_extremum_not_in_place(ls: list, _find_max: bool = True) -> object: + """Finds (not in-place) the maximum (or minimum) element in the list ls. + + It finds the maximum if _find_max is set to true, it finds the minimum + otherwise.""" + if len(ls) == 0: + return + if len(ls) == 1: + return ls[0] + if len(ls) == 2: + if _find_max: + return ls[0] if ls[0] > ls[1] else ls[1] + return ls[0] if ls[0] < ls[1] else ls[1] + + mid = len(ls) // 2 + m1 = find_extremum_not_in_place(ls[0:mid], _find_max) + m2 = find_extremum_not_in_place(ls[mid:], _find_max) + + if _find_max: + return m1 if m1 > m2 else m2 + return m1 if m1 < m2 else m2 + + +def _find_extremum_in_place( + ls: list, start: int, end: int, _find_max: bool = True +) -> object: + if (end - start) < 0: + return + if (end - start) == 0: + return ls[start] + if (end - start) == 1: + if _find_max: + return ls[start] if ls[start] > ls[end] else ls[end] + return ls[start] if ls[start] < ls[end] else ls[end] # find min + + mid = (start + end) // 2 + assert start <= mid <= end + m1 = _find_extremum_in_place(ls, start, mid - 1, _find_max) + m2 = _find_extremum_in_place(ls, mid, end, _find_max) + + if _find_max: + return m1 if m1 > m2 else m2 + return m1 if m1 < m2 else m2 # find min + + +def find_extremum_in_place(ls: list, _find_max: bool = True) -> object: + """Finds (in place) the maximum (or minimum) element in the list ls. + + It finds the maximum if _find_max is set to true, it finds the minimum + otherwise.""" + return _find_extremum_in_place(ls, 0, len(ls) - 1, _find_max) + + +def find_max(ls: list) -> object: + """ + Find the maximum element in ls using a divide-and-conquer strategy. + """ + m = find_extremum_in_place(ls) + assert m == find_extremum_not_in_place(ls) + return m + + +def find_min(ls: list) -> object: + """ + Find the minimum element in ls using a divide-and-conquer strategy. + """ + m = find_extremum_in_place(ls, False) + assert m == find_extremum_not_in_place(ls, False) + return m diff --git a/andz/algorithms/dac/find_peak.py b/andz/algorithms/dac/find_peak.py new file mode 100755 index 00000000..a9565a94 --- /dev/null +++ b/andz/algorithms/dac/find_peak.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 15/08/2015 + +Updated: 07/03/2018 + +# Description + +Finds a peak in a list A of comparable objects. A peak A[i] satisfies the +following condition: + + A[i - 1] <= A[i] >= A[i + 1] + +for i=1...len(A) - 2. In other words, A[i] is a peak if it is not smaller than +its neighbors. + +The two algorithms to find the peak below can return different correct answers, +because they operate differently. find_peak_linearly proceeds linearly through +the input list ls, whereas find_peak uses a divide and conquer strategy. + +# TODO + +- Complexity analysis of find_peak. + +# References + +- https://www.youtube.com/watch?v=HtSuA80QTyo&list=PLUl4u3cNGP61Oq3tWYp6V_F-5jb5L2iHb&spfreload=10 +""" + +__all__ = ["find_peak", "find_peak_linearly"] + + +def find_peak_linearly(ls: list) -> int: + """Finds the index of the first peak in ls. + + If there's no peak or the list is empty, -1 is returned. + + Time complexity: O(n), where len(ls) == n.""" + for i in range(1, len(ls) - 1): + if ls[i - 1] <= ls[i] >= ls[i + 1]: + return i + return -1 + + +def _find_peak(ls: list, i: int, j: int) -> int: + """Auxiliary in-place algorithm to find_peak.""" + m = (i + j) // 2 + if 0 < m < len(ls) - 1: + if ls[m - 1] <= ls[m] >= ls[m + 1]: + return m + if ls[m - 1] > ls[m]: + return _find_peak(ls, i, m - 1) + if ls[m] < ls[m + 1]: + return _find_peak(ls, m + 1, j) + # TODO: what if it reaches this part? Should we handle it? I don't remember anymore. + else: + return -1 + + +def find_peak(ls: list) -> int: + """Returns the index of a peak in ls, using the divide-and-conquer strategy. + + If there's no peak or the list is empty, -1 is returned.""" + return _find_peak(ls, 0, len(ls) - 1) diff --git a/andz/algorithms/dac/select.py b/andz/algorithms/dac/select.py new file mode 100755 index 00000000..9ece2646 --- /dev/null +++ b/andz/algorithms/dac/select.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 18/08/2015 + +Updated: 10/03/2017 + +# Description + +Find an element x in a list, such that at most k elements of the list are less +than or equal to x. + +# TODO + +- Add complexity analysis +- Add _select_in_place +""" + +from andz.algorithms.sorting.comparison.quick_sort import partition + +__all__ = ["select"] + + +def _select_not_in_place(ls: list, k: int) -> object: + """Find an element x in ls, such that at most k elements of ls are less than + x.""" + p = partition(ls, 0, len(ls) - 1) # p := pivot's index + if p == k: + return ls[p] + if p > k: + return _select_not_in_place(ls[0:p], k) + return _select_not_in_place(ls[p + 1 :], k - p - 1) # p < k + + +def select(ls: list, k: int) -> object: + """Returns an element x from ls such that at most k from ls are less than x. + + If k >= len(ls) or k < 0, ValueError is raised. + If the list ls is empty, None is returned. + If k == 0, it means that x is the smallest element in ls.""" + if k < 0 or k >= len(ls): + raise ValueError("k < 0 or k >= len(ls)") + return _select_not_in_place(ls, k) diff --git a/andz/algorithms/dp/README.md b/andz/algorithms/dp/README.md new file mode 100644 index 00000000..25ad4530 --- /dev/null +++ b/andz/algorithms/dp/README.md @@ -0,0 +1,7 @@ +# [Dynamic Programming](https://en.wikipedia.org/wiki/Dynamic_programming) + +## TODO + +- [ ] Text justification +- [ ] Counting Boolean Parenthesization +- [ ] Two-CNF-SAT diff --git a/andz/algorithms/dp/change_making.py b/andz/algorithms/dp/change_making.py new file mode 100755 index 00000000..d0984f33 --- /dev/null +++ b/andz/algorithms/dp/change_making.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 23/08/2015 + +Updated: 24/04/2022 + +# Description + +Here's the change-making problem. A cashier has a number of coins of different +denominations at her disposal and wishes to make a selection, using the +fewest number of coins, to meet a given total. + +It is assumed that + +- the number of coins available in each denomination is not limited, +- each denomination can be represented by a non-negative integer number, +- the total is also a non-negative integer +- we always have the denomination (or coin) 1 + +This problem is a special case of the integer knapsack problem, where each +denomination also corresponds to its weight (or value). + +## Linear Programming Formulation + +The change-making problem is a constrained minimization problem, so we can +formulate it as a linear programming problem. + +Suppose that an unlimited number of coins of denominations c₁, c₂, ..., cᵢ are +made available. So, we have i different denominations (or types of coins) and +we have an unlimited number of denomination c₁, denomination c₂, etc. For +convenience, without loss of generality, these may be ordered so that +c₁ < c₂ < ... < cᵢ. So, for example, c₁ could be 1 cent, c₂ could be 5 cents, +and cᵢ could be 2 euros. + +Assuming that xⱼ coins of denomination cⱼ are selected to meet a total C, the +linear programming problem to be solved is then + + minimize Z = sum(x₁, x₂, ..., xᵢ) + + subject to ∑ xⱼ * cⱼ = C, + xⱼ >= 0, + xⱼ is a non-negative integer, and + C is a non-negative integer + +## Dynamic Programming Solution + +This problem can be solved using dynamic programming. The recursive formulation +of the change-making problem is + + fᵤ(z) = min(xᵤ + fᵤ₋₁(z - xᵤ * cᵤ)), + +where xᵤ is allowed to range over the values 0, 1, 2, ..., ⌈z / cᵤ⌉, where +⌈z / cᵤ⌉ is the greatest integer smaller than or equal to z / cᵤ. For u = 1, +f₁(z) = ⌈z / c₁⌉. So, u is the number of stages, in the multi-stage decision +process, such that, in turn, u = 1, 2, ..., i, where i is the number of +different denominations (or types of coin). + +At each stage u, z is ranged from 0 to C in incremental steps (of 1), where C +is the total sum to which the change must be totaled. + +# Notes + +In the functions below, we use a slightly different notation, because the code +style and the mathematical formulations are usually not 100% compatible and +because it is cumbersome to write mathematical formulations in the doc-strings. + +In the formulation above, C (or the parameter n in the implementations) can be +changed exactly because we assume the existence of the denomination 1, even +though it may not be contained in the original list of coins. + +The total C (or n in the implementations) is assumed to be a non-negative +integer and the available coins (or denominations) are all assumed to also be +non-negative integers. + +Furthermore, there are other ways of solving this problem. For example, we can +also use a greedy strategy. However, the greedy strategy is not guaranteed to +compute the optimal solution (for all inputs). + +# TODO + +- Show that this problem exhibits optimal sub-structure and contains +overlapping sub-problems. + +- Add recursive change_making (for comparison with the dynamic programming +solution). + +- Use a 1-dimensional list (instead of a 2d one) to implement the solution, if +possible. + +# References + +- http://web.archive.org/web/20181108224810/http://www.dis.uniroma1.it/~bonifaci/algo/doc/COIN.pdf +- https://en.wikipedia.org/wiki/Change-making_problem +- http://www.cs.toronto.edu/~yilan/TA/364/week3_solution.pdf +""" + +__all__ = ["change_making", "extended_change_making"] + + +def _get_change_making_matrix(c: int, n: int) -> list: + """Returns a list of c + 1 lists of size n + 1. Each of the c + 1 inner + lists contains zeros, except for the first one, where each element is + initialized to its index, because we assume that the first coin is 1 and + it is always available, so that there's always a way to total n. Note that + the list of coins given may already contain 1 as one of its denominations, + but we do not know this, in general.""" + m = [[0 for _ in range(n + 1)] for _ in range(c + 1)] + + for i in range(n + 1): + # m[0], the first list of m, is associated with the usage of the + # denomination 1, which we assume is always available. So, using + # denomination 1, we can total i using i 1s. + m[0][i] = i + + return m + + +def _get_sets_of_coins_matrix(c: int, n: int) -> list: + m = [[[] for _ in range(n + 1)] for _ in range(c + 1)] + + for i in range(n + 1): + for _ in range(i): + m[0][i].append(1) + + for j in range(c + 1): + m[j][0] = [] + + return m + + +def _pre_conditions(coins: list, n: int) -> None: + assert isinstance(coins, list) or isinstance(coins, tuple) + assert isinstance(n, int) + assert all(isinstance(c, int) for c in coins) + + if n < 0: + raise ValueError("n must be non-negative.") + for c in coins: + if c < 0: + raise ValueError("All denominations must be non-negative.") + if len(coins) == 0: + raise ValueError("No coins available.") + + +def change_making(coins: list, n: int) -> int: + """Returns the minimum number of coins needed to obtain n, which the total + sum to which the change must be totaled (and it is C in the module's + doc-strings above). + + Note that, even though the list (or tuple) coins may not contain the + denomination 1, this function assumes that 1 is always available, so that + we can always total n. + + Time complexity: O((len(coins) + 1) * (n + 1)).""" + _pre_conditions(coins, n) + + m = _get_change_making_matrix(len(coins), n) + + for c in range(1, len(coins) + 1): + + for z in range(1, n + 1): + + if coins[c - 1] == z: + m[c][z] = 1 + + elif coins[c - 1] > z: + m[c][z] = m[c - 1][z] + + else: + m[c][z] = min(m[c - 1][z], 1 + m[c][z - coins[c - 1]]) + + # At this point, m[c][z] represents the minimum number of coins needed to + # obtain (at most) z using the first c coins. + return m[-1][-1] + + +def extended_change_making(coins: list, n: int) -> list: + """Returns a list of integers representing the coins which total n, such + that the size of the returned list is minimized. + + Note that, even though the list (or tuple) coins may not contain the + denomination 1, this function assumes that 1 is always available, so that + we can always total n. + + Time complexity: O((len(coins) + 1) * (n + 1)).""" + _pre_conditions(coins, n) + + m = _get_change_making_matrix(len(coins), n) + + # Matrix used to keep track of which coins are used. + p = _get_sets_of_coins_matrix(len(coins), n) + + for c in range(1, len(coins) + 1): + + # In this module's doc-strings, z ranges from 0 to C. However, because + # of implementation details (see _get_change_making_matrix), here we + # range from 1 to n (or C). + for z in range(1, n + 1): + + # Just use the coin coins[c - 1]. + if coins[c - 1] == z: + m[c][z] = 1 + p[c][z].append(coins[c - 1]) + + # coins[c - 1] cannot be included. We use the previous solution for + # for totaling z, excluding coins[c - 1]. + elif coins[c - 1] > z: + m[c][z] = m[c - 1][z] + p[c][z] = p[c - 1][z] + + # We can use coins[c - 1]. We need to decide which one of the + # following solutions is the best: + # + # 1. Using the previous solution for totaling z (without using + # coins[c - 1]). + # + # 2. Using coins[c - 1] + the optimal solution for totaling + # z - coins[c - 1]. + else: + if m[c - 1][z] < 1 + m[c][z - coins[c - 1]]: + p[c][z] = p[c - 1][z] + m[c][z] = m[c - 1][z] + else: + p[c][z] = [coins[c - 1]] + p[c][z - coins[c - 1]] + m[c][z] = 1 + m[c][z - coins[c - 1]] + + assert sum(p[-1][-1]) == n + + return p[-1][-1] diff --git a/andz/algorithms/dp/fibonacci.py b/andz/algorithms/dp/fibonacci.py new file mode 100755 index 00000000..76e3875b --- /dev/null +++ b/andz/algorithms/dp/fibonacci.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 20/07/2015 + +Updated: 29/03/2022 + +# Description + +In this file, you can find some functions that return the nth fibonacci number, +but they do it in different ways, which has also an impact on the performance +and asymptotic complexity of the same algorithms. + +The Fibonacci numbers is an infinite sequence of numbers, where the next +element of the sequence is constructed by summing the previous two elements of +the same. + +The first two elements are (usually) 0 and 1, so the next element is 1, so the +sequence is now {0, 1, 1}. We then add 1 + 1 = 2 to obtain the 4th element of +the sequence, which is now {0, 1, 1, 2}, and so on. + +The nth Fibonacci number can thus be computed in a recursive way. First, the +base cases are when n = 0 and n = 1, so fib(0) = 0 and fib(1) = 1. The +inductive case is fib(n) = fib(n - 1) + fib(n - 2). + +It turns out that, if we compute the nth Fibonacci number in this way, we would +repeat some computations. For example, to compute fib(5), you would need to +compute fib(4) and fib(3). To compute fib(4), we would need to compute fib(3) +and fib(2). So, we would compute fib(3) twice. + +To solve this problem, once we compute fib(3), we can solve the result. This is +called memoization. + +The time complexity of memoized_fibonacci (below) is linear because we need +to solve all fib(i), from i=0 to i=n, and each of these sub-problems takes +constant time. + +## References + +- "Lecture 19: Dynamic Programming I: Fibonacci, Shortest Paths" +(https://www.youtube.com/watch?v=OQ5jsbhAv_M&ab_channel=MITOpenCourseWare) +- https://www.youtube.com/watch?v=P8Xa2BitN3I&ab_channel=HackerRank + +## TODO + +- Write a Fibonacci function for negative numbers. +""" + +from typing import Union + +__all__ = ["recursive_fibonacci", "memoized_fibonacci", "bottom_up_fibonacci"] + + +def _check_input(n: int): + if not isinstance(n, int): + raise TypeError("n should be an int") + if n < 0: + raise ValueError("n should be >= 0") + + +def recursive_fibonacci(n: int) -> int: + """Returns the nth fibonacci number using a recursive approach. + + Time complexity: O(2ⁿ).""" + _check_input(n) + if n == 0: + return 0 + elif n == 1: + return 1 + else: + return recursive_fibonacci(n - 1) + recursive_fibonacci(n - 2) + + +def _memoized_fibonacci_aux(n: int, memo: dict) -> int: + """Auxiliary function of memoized_fibonacci.""" + if n == 0 or n == 1: + return n + if n not in memo: + memo[n] = _memoized_fibonacci_aux(n - 1, memo) + _memoized_fibonacci_aux( + n - 2, memo + ) + return memo[n] + + +def memoized_fibonacci(n: int) -> int: + """Returns the nth fibonacci number using recursion and a technique called + "memoization". + + Time complexity: O(n).""" + _check_input(n) + memo = {} + return _memoized_fibonacci_aux(n, memo) + + +def bottom_up_fibonacci(n: int, return_seq: bool = False) -> Union[int, list]: + """Returns the nth fibonacci number if return_seq=False, else it returns a + list containing the sequence of Fibonacci numbers from i=0 to i=n. + + For example, suppose return_seq == True and n == 5, then this function + returns [0, 1, 1, 2, 3, 5]. If return_seq == False, it returns simply 5. + + Note: indices start from 0 (not from 1). + + This function uses a dynamic programing "bottom up" approach: we start by + finding the optimal solution to smaller sub-problems, and from there, we + build the optimal solution to the initial problem. + + Time complexity: O(n).""" + _check_input(n) + assert isinstance(return_seq, bool) + if n == 0: + return n if not return_seq else [n] + if n == 1: + return n if not return_seq else [0, n] + + # If we don't need to return the list of numbers, we only need to save the + # last 2 values, so that would be constant space. + fib = [0] * (n + 1) + fib[0] = 0 + fib[1] = 1 + + for i in range(2, n + 1): + fib[i] = fib[i - 1] + fib[i - 2] + + return fib[-1] if not return_seq else fib + + +if __name__ == "__main__": + for f in range(10): + print(recursive_fibonacci(f)) + print(memoized_fibonacci(f)) + print(bottom_up_fibonacci(f, True)) diff --git a/andz/algorithms/matching/__init__.py b/andz/algorithms/matching/__init__.py new file mode 100644 index 00000000..4aded1cb --- /dev/null +++ b/andz/algorithms/matching/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a python package. diff --git a/andz/algorithms/matching/gale_shapley.py b/andz/algorithms/matching/gale_shapley.py new file mode 100644 index 00000000..82d6fcc2 --- /dev/null +++ b/andz/algorithms/matching/gale_shapley.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 19/09/2017 + +Updated: 29/09/2017 + +# Description + +The Gale-Shapley algorithm for the stable matching problem, which is a discrete +problem. + +Given n men and n women and a list of preferences for each man and woman, +regarding who they want to stay with. + +A "perfect matching" is an one-to-one assignment of each man to exactly one +woman, so that no man or woman remains unmatched (or "alone"). + +An unstable match occurs when man M prefers woman W and woman W prefers man M, +but man M is matched with another woman W' and woman W is with another man M'. + +A "stable matching" is a perfect matching with no unstable pairs. + +Stable matching problem: find a stable matching (if one exists). + +Clearly, domains could be different. Instead of women and men we could have +for example people and servers, medical school graduates and hospitals, intern +to companies, etc. + +How do find matches so that no men or women remain alone and no unstable match +exists? + +We can use the Gale-Shapley algorithm (proposed 1962) to solve this problem, +whose pseudo-code is as follows: + +function GALE_SHAPLEY: + Initialize all m ∈ M and w ∈ W to free + while ∃ free man m who still has a woman w to propose to: + w = first woman on m’s list to whom m has not yet proposed + if w is free: + (m, w) become engaged + else some pair (m', w) already exists: + if w prefers m to m': + m' becomes free + (m, w) become engaged + else: + (m', w) remain engaged + +## Examples + +Suppose we have the following preferences lists for men. + +| | 1st | 2nd | 3rd | +|--------|--------|--------|-------| +| Xavier | Amy | Bertha | Clare | +| Yancey | Bertha | Amy | Clare | +| Zeus | Amy | Bertha | Clare | + +And the following one for women. + +| | 1st | 2nd | 3rd | +|--------|--------|--------|------| +| Amy | Yancey | Xavier | Zeus | +| Bertha | Xavier | Yancey | Zeus | +| Clare | Xavier | Yancey | Zeus | + +Then assignments Xavier to Clare, Yancey to Bertha and Zeus to Amy are unstable, +because Bertha prefers Xavier to Yancey and Xavier prefers Bertha to Clare. + +The assignments Xavier to Amy, Yancey to Bertha and Zeus to Clare are stable. + +## Notes + +1. Men propose to women in decreasing order of preference. +2. Once a woman is matched, she never becomes unmatched: she only "trades up." + +## Complexity analysis of the Gale-Shapley algorithm + +Gale-Shapley terminates with a stable matching after at most n² iterations of +the while loop. In particular, n * (n - 1) + 1 proposals may be required. + +### Algorithm terminates after at most n² iterations of while loop. + +#### Proof + +- There are n² pairs (m, w). + +- At each iteration of the while loop, one man proposes to one woman. + +- Once a man proposes to a woman, he will never propose to her again (note 1). +Thus, a man does at most n proposals. + +- Thus, after at most n² iterations, no one is left to propose to (algorithm +must terminate). + +### All men and women get matched (we have a perfect matching) + +- Suppose (by contradiction) that there is a man, Z, who is not matched upon +termination of algorithm. + +- Then there must be a woman, say A, who is not matched upon termination. +Remember there are n men and n women! + +- Then, by note 2, A was never proposed to. + +- But, Z proposes to everyone, since he ends up unmatched. Thus, he must have +proposed to A, a contradiction. + +### No unstable pairs (stable matching) + +Suppose we have the following pairs (Xavier-Clare), (Yancey-Bertha) and +(Zeus-Amy). And suppose Xavier prefers Bertha to Clare and Bertha prefers Xavier +to Yancey, i.e. Xavier and Bertha would hook up with each other after the given +assignments. So we have an unstable pair in a Gale-Shapley matching S. + +Then there are two possible cases: + +1. Xavier never proposed to Bertha. + => Xavier prefers his partner to Bertha in S. + => S is stable. + +2. Xavier proposed to Bertha. + => Bertha rejected Xavier (right away or later). + Remember: women only trade up. + => Bertha prefers her current partner to Xavier. + => S is stable. + +In both cases 1 and 2, we reach a contradiction. + +▪ + +## How to implement the Gale-Shapley algorithm so that its complexity is O(n²)? + +Since there are at most n² iterations, each iteration should take constant time. + +We denote men and women from 0 to n - 1. + +We maintain two lists wife[m] and husband[w]. wife[m] or husband[w] is None if m +does still not have a woman and w does still not have a husband, respectively. +Initially, all wife[i] and husband[i], for i = 0, ..., n - 1, is None. + +For each man, maintain a list of women, ordered by preference. + +Maintain a list count[m] that counts the number of proposals made by man m. + +Idea: for each woman, create the inverse of her preference list. For example, +if the preference list of woman w is: + + 0 1 2 <- preferences + [2, 0, 1] <- men + + +where 2 is w's most preferred man and 1 the least preferred. Then we build the +following inverse list + + 0 1 2 <- men + [2, 1, 0] <- preferences + +where the number 0 represents the highest preference and the number 2 the +smallest one. + +To build the inverse preference list, it takes n time. + +Suppose we have an n x n matrix, where each row i represents the preferences +list of man (or woman) i. Then, we can invert those preferences lists as follows: + + inverses := empty n x n matrix + for i = 0 to n - 1: + for p = 0 to n - 1: + inverses[preferences[i][p]] = p + +### Conclusions + +We have n men + n women = 2 * n. But we also have as input the preferences lists +of men and women. Each of them occupies n² space. So, the input is N = 2 * n². +It actually follows that the Gale-Shapley algorithm is a O(N) algorithm, i.e. a +linear-time algorithm, where N is the size of the input. + +## Further Notes + +- In practical applications, the input size may be linear as the preference list +may be limited to a constant (say 5 < n), where n is the number of men (or +women). + +- With the previous restriction, the algorithm may fail to find a stable +matching. + +- In practice, a "reasonably" stable matching is sufficient. + +- The previous algorithm assumed we have the same number of men as women. + +## Understanding the Solution produced by Gale-Shapley algorithm. + +TODO + +# TODO + +- is_stable function +- Implement the GaleShapley algorithm for the Hospitals-Students matching +problem. + +# References + +- Slides of prof. E. Papadopoulou for her course "Algorithms & Complexity" at +USI, fall 2017, master in AI. + +- https://en.wikipedia.org/wiki/Stable_marriage_problem +""" + +__all__ = ["gale_shapley"] + + +def _validate_inputs(men_preferences: list, women_preferences: list, n: int): + if len(men_preferences) != len(women_preferences): + raise ValueError("Preferences lists should be of the same size.") + + for m, w in zip(men_preferences, women_preferences): + if len(m) != len(set(m)) or len(w) != len(set(w)): + raise ValueError("A preference list has duplicate entries.") + if len(m) != n or len(w) != n: + raise ValueError("Preferences matrix should be n x n.") + + possible_values = set(range(n)) + for p1, p2 in zip(m, w): + if p1 not in possible_values or p2 not in possible_values: + raise ValueError("Preferences must be in range [0, n - 1].") + + +def _build_inverses(women_preferences: list) -> list: + """Builds the inverse matrix of the preferences matrix for women, according + to the algorithm described in the doc-strings above of this module. + + Time complexity: Θ(n²).""" + n = len(women_preferences) + inverses = [[None for _ in range(n)] for _ in range(n)] + for w in range(n): + for p in range(n): # p for preference. + inverses[w][women_preferences[w][p]] = p + return inverses + + +def gale_shapley(men_preferences: list, women_preferences: list) -> list: + """Suppose we have n = len(men_preferences) = len(women_preferences) men and + women. We number men and women from 0 to n - 1. + + Time complexity: O(n²), where n = # of men = # of women, or O(N), where N is + the number of preference lists, i.e. N = n². In other words, this is a + linear-time algorithm in terms of the size of the input.""" + n = len(men_preferences) + + _validate_inputs(men_preferences, women_preferences, n) + + # To keep track of wives of men. So, wife[m] is the wife of m. + wife = [None] * n + + # To keep track of husbands of women. So, husband[w] is the husband of w. + husband = [None] * n + + inverses = _build_inverses(women_preferences) + + # To keep track of the number of proposals made by men. So, count[m] is the + # number of proposals of man m. + count = [0] * n + + def next_man() -> int: + """Returns the index or number of the next man without a woman, or None + if there is not such man.""" + for i, w in enumerate(wife): + if w is None: + return i + + def hook_up_with(m: int, w: int) -> None: + # Assign m to be the current partner of w. + husband[w] = m + + # Assign w to be the current partner of m. + wife[m] = w + + def go_forward(m: int) -> None: + """Makes m man go forward and forget about its current preference, i.e. + m now goes forward to his next preference.""" + count[m] += 1 + assert 0 < count[m] < n + + def make_alone(o: int) -> None: + wife[o] = None + go_forward(o) + + def prefers(w: int, m: int, o: int) -> bool: + """Returns true if w prefers m over o.""" + assert m != o + return inverses[w][m] < inverses[w][o] + + m = next_man() + + while m is not None: + # If there's still a man m without a woman. + # This while loop takes at most n² iterations. + + # All of the following operations take O(1) time. + + # Look up the next preferred woman for m. + w = men_preferences[m][count[m]] + + # If w does not have a partner. + if husband[w] is None: + hook_up_with(m, w) + else: # w is already matched with some man. + + # Get the current partner of w. + o = husband[w] + assert wife[o] == w + + # If w prefers m over o, then make m the new partner of w and make + # o alone. + if prefers(w, m, o): + hook_up_with(m, w) + make_alone(o) + else: + go_forward(m) + + m = next_man() + + # Assert that at the end of the while loop all men and women have a partner. + assert all(x is not None for x in wife) + assert all(x is not None for x in husband) + + return wife, husband diff --git a/andz/algorithms/numerical/README.md b/andz/algorithms/numerical/README.md new file mode 100644 index 00000000..63f196c5 --- /dev/null +++ b/andz/algorithms/numerical/README.md @@ -0,0 +1,25 @@ +# Numerical Algorithms + +## [Numerical Stability](http://mathworld.wolfram.com/NumericalStability.html) + +Numerical stability refers to how a malformed input affects the execution of an +algorithm. In a numerically stable algorithm, errors in the input lessen in +significance as the algorithm executes, having little effect on the final +output. + +On the other hand, in a numerically unstable algorithm, errors in the input +cause a considerably larger error in the final output. + +## [Interpolation](https://en.wikipedia.org/wiki/Interpolation#Example) + +- [Linear interpolation](https://en.wikipedia.org/wiki/Linear_interpolation) + +- [Polynomial interpolation](https://en.wikipedia.org/wiki/Polynomial_interpolation) + + Polynomial interpolation is the interpolation of a given data set by a +[polynomial](https://en.wikipedia.org/wiki/Polynomial): given some points, find +a polynomial which goes exactly through these points. + + - [Lagrange form](https://en.wikipedia.org/wiki/Lagrange_polynomial) + + - [Newton polynomial](https://en.wikipedia.org/wiki/Newton_polynomial) \ No newline at end of file diff --git a/andz/algorithms/numerical/barycentric.py b/andz/algorithms/numerical/barycentric.py new file mode 100644 index 00000000..117ed997 --- /dev/null +++ b/andz/algorithms/numerical/barycentric.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 09/10/2017 + +Updated: 02/04/2018 + +# Description + +Given a set P = {(x₁, y₁), ..., (xᵢ, yᵢ)} of 2-dimensional points, then the +so-called problem of "polynomial interpolation" consists in finding the +polynomial of smallest degree which goes through these points, that is, a +polynomial which "interpolates" these points. + +# References + +- Dr. prof. Kai Hormann's notes for the Numerical Algorithms course, fall, 2017. +- https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.barycentric_interpolate.html +- https://en.wikipedia.org/wiki/Lagrange_polynomial +""" + +__all__ = ["barycentric", "compute_weights"] + + +def compute_weights(xs: list) -> list: + """Computes and returns the weights (as a list) used in the barycentric form + of the Lagrange polynomial. + + This function avoids checking if the input list xs is well-formed for + performance reasons. + + Time complexity: O(n²).""" + n = len(xs) + ws = [1] * n + for i in range(n): + for j in range(n): + if j != i: + ws[i] *= 1 / (xs[i] - xs[j]) + return ws + + +def barycentric(xs: list, ys: list, x0: float, ws: list = None) -> float: + """Evaluates, at x coordinate x0, the polynomial that interpolates 2d points + (xs[i], ys[i]), for i=0, ..., len(xs) - 1 == len(ys) - 1. In other words, + this function returns the y value corresponding to the x coordinate x0 of + the polynomial which interpolates the points (xs[i], ys[i]). + + For reasons of numerical stability, this function does not compute the + coefficients of the polynomial. + + This function uses a "barycentric interpolation" method that treats the + problem as a special case of rational function interpolation. This algorithm + is quite stable, numerically, but even in a world of exact computation, + unless the x coordinates are chosen very carefully, polynomial interpolation + itself is a very ill-conditioned process due to the Runge phenomenon. + + The construction of the interpolation weights is a relatively slow process: + it takes O(n²) time. If you want to call this many times with the same x + coordinates (but possibly varying the corresponding y values or x0), you can + first calculate the weights, using e.g. the function compute_weights in this + same module, and then pass them as the parameter ws. If ws is None, then the + weights are computed by this function at every call. If ws is not None, it + should be a list of the same length as xs and ys and should clearly + represent the weights as computed by the function compute_weights in this + same module. + + Time complexity: O(n²), if ws is None, else O(n).""" + if len(xs) != len(ys): + raise ValueError("Lists xs and ys have different lengths.") + + if ws is None: + ws = compute_weights(xs) + else: + if len(xs) != len(ws): + raise ValueError("Lists xs and ws have different lengths.") + + n = 0 # Numerator + d = 0 # Denominator: sum of all weights. + + for i in range(len(xs)): + if x0 == xs[i]: + return ys[i] + else: + w = ws[i] / (x0 - xs[i]) + n += w * ys[i] + d += w + return n / d diff --git a/andz/algorithms/numerical/gradient_descent.py b/andz/algorithms/numerical/gradient_descent.py new file mode 100644 index 00000000..a2fca0c2 --- /dev/null +++ b/andz/algorithms/numerical/gradient_descent.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 14/10/2017 + +Updated: 26/10/2017 + +# Description + +An implementation of the gradient descent method for finding local minima of +single-variable functions. + +# References + +- https://en.wikipedia.org/wiki/Gradient_descent +""" + +__all__ = ["gradient_descent"] + + +def gradient_descent( + x0: float, + df: callable, + step_size: float = 0.01, + max_iter: int = 100, + tol: float = 1e-6, +): + """Finds a local minimum of a function whose derivative is df starting from + an initial guess x0 using a step size = step_size.""" + if not callable(df): + raise TypeError("df must be a callable object.") + + x = x0 + + for i in range(max_iter): + x_next = x - step_size * df(x) # Gradient descent step. + + if abs(x_next - x) < tol * abs(x_next): + x = x_next + break + + x = x_next + + return x diff --git a/andz/algorithms/numerical/horner.py b/andz/algorithms/numerical/horner.py new file mode 100644 index 00000000..f44f7dc3 --- /dev/null +++ b/andz/algorithms/numerical/horner.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 30/09/2017 + +Updated: 30/09/2017 + +# Description + +## Polynomials + +The most common way of expressing a polynomial p: R → R of degree at most u is +to use the monomial basis {1, x, x², ..., xᵘ} and to write p as + + p(x) = aᵤ * xᵘ + aᵤ₋₁ * xᵘ⁻¹ + ... + a₁ * x + a₀ = ∑ᵢ₌₀ᶦ⁼ᵘ aᵢ * xᶦ + +with coefficients a₀, a₁, ..., aᵤ₋₁, aᵤ ∈ R. Using this representation, one can +show that: + + p'(x) = u * aᵤ * xᵘ⁻¹ + (u − 1) * aᵤ₋₁ * xᵘ⁻² + ... + 2 * a₂ * x + a₁ = + = ∑ᵢ₌₀ᶦ⁼ᵘ⁻¹ (i + 1) * aᵢ₊₁ * xᶦ, + +and + + p⁽ᶦ⁾(0) = i! * aᵢ, for i = 0, ..., u, + +and + + p⁽ᵘ⁺¹⁾(x) = 0. + +## Horner's method to compute polynomials + +Horner's method (a.k.a. Horner scheme or Horner's rule) is an algorithm for +calculating polynomials. It consists of transforming the monomial form of p into +a computationally efficient form. + +Suppose we want to evaluate the polynomial p at a specific value of x, say x₀. + +We now transform the monomial (usual) form of p into an equivalent form, which +allows us to efficiently evaluate p at x₀: + + p(x) = aᵤ * xᵘ + aᵤ₋₁ * xᵘ⁻¹ + ... + a₁ * x + a₀ <=> + p(x) = (aᵤ * xᵘ⁻¹ + aᵤ₋₁ * xᵘ⁻² + ... + a₁) * x + a₀ <=> + p(x) = ((aᵤ * xᵘ⁻² + aᵤ₋₁ * xᵘ⁻³ + ... + a₂) * x + a₁) * x + a₀ <=> + +If we continue this process, we end up with the following formula: + + p(x) = (((aᵤ₋₁ + aᵤ * x) * x + ... + a₂) * x + a₁) * x + a₀ + +We now calculate p at x₀ by replacing x with x₀ in the general form + + p(x₀) = (((aᵤ₋₁ + aᵤ * x₀) * x₀ + ... + a₂) * x₀ + a₁) * x₀ + a₀ + +### Why would this allow us to evaluate p at x₀ efficiently? + +If, for simplicity, we perform the following changes of variables + + bᵤ := aᵤ + bᵤ₋₁ := aᵤ₋₁ + bᵤ * x₀ + . + . + . + b₀ := a₀ + b₁ * x₀ + +And replace these new variables (or alias) in the evaluation of p at x₀, that is + + p(x₀) = (((aᵤ₋₁ + bᵤ * x₀) * x₀ + ... + a₂) * x₀ + a₁) * x₀ + a₀ <=> + p(x₀) = (((bᵤ₋₁) * x₀ + ... + a₂) * x₀ + a₁) * x₀ + a₀ <=> + . + . + . + p(x₀) = a₀ + b₁ * x₀ <=> + p(x₀) = b₀ + +We see that we end up, at the end, to discover that the result of p(x₀) is b₀. + +### How many changes of variables do we perform? + +This can easily be seen from the subscripts of the variables b. We have u +changes of variables, where u is the original degree of the polynomial p. + +### In each change of variable, how many additions and multiplications do we +perform? + +Excluding bᵤ := aᵤ, which we assume to be a constant-time operation, all other u +changes of variables perform one addition and one multiplication. + +### How many operations have we performed in total? + +So, we have u additions and u multiplications, plus a constant-time operation. + +### Notes + +- When evaluating p(x₀) with the changes of variables, we are only performing +the operations in the changes of variables. + +### Optimality of Horner's method + +Horner's method is optimal, in the sense that any algorithm to evaluate an +arbitrary polynomial must use at least as many operations. + +## Computing polynomials by implementing Horner's method + +p(x₀), a u-degree polynomial, can computed efficiently using Horner's scheme, in +O(u) operations, as follows + + function HORNER({a₀, a₁, ..., aᵤ₋₁, aᵤ}, x₀): + p := aᵤ + for i from u − 1 to 0 by −1 do: + p := p * x₀ + aᵢ + return p + +From the previous pseudo-code, we can easily see that this is a O(u) algorithm, +since we have u iterations of the for loop and provided that multiplications and +additions can be performed in O(1), w.r.t. u. + +### Notes + +- HORNER basically implements the changes of variables explained above. + +# TODO + +- Add example of how Horner's method works in practice. +- Implement the slightly optimized version using explicit fused +Multiply–accumulate operation. + +# References + +- Dr. prof. Kai Hormann's notes for the Numerical Algorithms course, fall, 2017. +- https://en.wikipedia.org/wiki/Horner%27s_method +""" + +__all__ = ["horner"] + + +def horner(x0: float, coefficients: list) -> float: + """A function that implements the Horner's method for evaluating a + polynomial, with coefficients, at x = x0. + + Time complexity: O(n), where n = len(coefficients).""" + assert isinstance(coefficients, list) + assert all(isinstance(x, float) or isinstance(x, int) for x in coefficients) + assert isinstance(x0, float) or isinstance(x0, int) + p = 0 + for c in reversed(coefficients): + p = p * x0 + c + return p diff --git a/andz/algorithms/numerical/neville.py b/andz/algorithms/numerical/neville.py new file mode 100644 index 00000000..cabaac27 --- /dev/null +++ b/andz/algorithms/numerical/neville.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 30/09/2017 + +Updated: 08/10/2017 + +# Description + +## Polynomial curves + +In the context of curve design, which in turn is a key ingredient of +Computer-Aided Design (CAD) and used in the ship-building, aircraft, and +automobile industry, we are interested in polynomial curves P : R → Rᵈ in d +dimensions, where d is usually 2 or 3, although the theory works for arbitrary +d. + +Using the monomial basis, a polynomial curve P of degree at most u can be +written as + + P(t) = Aᵤ * tᵘ + Aᵤ₋₁ * tᵘ⁻¹ + ... + A₁ * t + A₀ = (1) + = ∑ᵢ₌₀ᶦ⁼ᵘ Aᵢ * tᶦ, + +where the coefficients A₀, A₁, ..., Aᵤ ∈ Rᵈ are now d-dimensional points (or +vectors, if you prefer). + +### Find point in a polynomial curve using Horner's method + +P(t) can be computed efficiently using Horner's scheme (or method). + +## Find interpolating polynomial curve of a set of points + +Often we are interested in finding an interpolating polynomial curve. That is, +we are given u + 1 interpolation points P₀, P₁, ..., Pᵤ and parameter values t₀, +t₁, ..., tᵤ, and want to determine the polynomial curve P of degree at most u +which satisfies + + P(tᵢ) = Pᵢ + +for i = 0, ..., u. + +Using (1), these u + 1 conditions can be written as the linear system: + + V * A = P (2) + +where + + A = (A₀, A₁, ..., Aᵤ)ᵀ ∈ R⁽ᵘ⁺¹⁾ˣ¹ + +is the column vector of unknown coefficients, + + P = (P₀, P₁, ..., Pᵤ)ᵀ ∈ R⁽ᵘ⁺¹⁾ˣ¹ + +is the column vector of interpolation points, and + + | 1 | t₀ | t₀² | ... | t₀ᵘ | + | 1 | t₁ | t₁² | ... | t₁ᵘ | + | . | . | . | . | . | +V = | . | . | . | . | . | ∈ R⁽ᵘ⁺¹⁾ˣ⁽ᵘ⁺¹⁾ + | . | . | . | . | . | + | 1 | tᵤ | tᵤ² | ... | tᵤᵘ | + +is the Vandermonde matrix. + +In general, solving a linear system like (2) has complexity O(n³), but the +special structure of the Vandermonde matrix can be used to design an O(n²) +algorithm (EXPLAIN FURTHER!!!). + +## Neville's algorithm + +The interpolation problem can be handled in a more direct way. + +We can actually compute the interpolating polynomial P(t) at some t ∈ R without +explicitly determining the coefficients Aᵢ of the monomial representation by +using Neville's algorithm, which is based on the following observation: + +The constant polynomial curves (of degree 0) + + Qᵢ⁰(t) = Pᵢ, for i = 0 , ..., u, + +surely interpolate the given interpolations points Pᵢ at tᵢ, one at a time, and +they can be used to define polynomial curves of degree 1 (i.e., lines), + + Qᵢ¹(t) = (tᵢ₊₁ - t) / (tᵢ₊₁ - tᵢ) * Qᵢ⁰(t) + + (t - tᵢ) / (tᵢ₊₁ - tᵢ) * Qᵢ₊₁⁰(t) + +for i = 0 , ..., u. + +It should immediately be clear that Qᵢ¹(t) is a linear combination of Qᵢ⁰(t) and +Qᵢ₊₁⁰(t) where the first weight is α = (tᵢ₊₁ - t) / (tᵢ₊₁ - tᵢ) and the second +weight is β = (t - tᵢ) / (tᵢ₊₁ - tᵢ). So, the previous expression can be +written in a more compact way as follows: + + Qᵢ¹(t) = α * Qᵢ⁰(t) + β * Qᵢ₊₁⁰(t) + +Moreover, we also have + + α + β = (tᵢ₊₁ - t) / (tᵢ₊₁ - tᵢ) + (t - tᵢ) / (tᵢ₊₁ - tᵢ) <=> + α + β = ((tᵢ₊₁ - t) + (t - tᵢ)) / (tᵢ₊₁ - tᵢ) <=> + = (tᵢ₊₁ - tᵢ) / (tᵢ₊₁ - tᵢ) + = 1 + +Hence Qᵢ¹(t) is also an affine combination. + +Qᵢ¹(t) basically interpolates the points Pᵢ and Pᵢ₊₁ at tᵢ and tᵢ₊₁, +respectively. + +### Recursion in Neville's algorithm + +This construction of polynomial curves to interpolate points can be applied +recursively, for any number of points. + +The general formula for a polynomial of degree j, that is Qᵢʲ, which +interpolates points Pᵢ, ...,Pᵢ₊ⱼ at tᵢ, ..., tᵢ₊ⱼ, looks like + + Qᵢʲ(t) = (tᵢ₊ⱼ - t) / (tᵢ₊ⱼ - tᵢ) * Qᵢʲ⁻¹(t) + (3) + (t - tᵢ) / (tᵢ₊ⱼ - tᵢ) * Qᵢ₊₁ʲ⁻¹(t) + +for i = 0, ..., u - j and j = 1, ..., u. Remember: j is the degree of the +polynomial. + +We have + + α = (tᵢ₊ⱼ - t) / (tᵢ₊ⱼ - tᵢ) + +and + + β = (t - tᵢ) / (tᵢ₊ⱼ - tᵢ) + +So, (3) can be written more compactly as follows + + Qᵢʲ(t) = α * Qᵢʲ⁻¹(t) + β * Qᵢ₊₁ʲ⁻¹(t) + +Clearly, α + β = 1, so this is still an affine combination. + +In particular, Qᵤ⁰ is the interpolating polynomial curve that we were looking +for. + +### Schematic interpretation of Neville's algorithm + +Suppose u = 1. Then, we can represent graphically (as a tree or pyramid) the +previously mentioned recursive procedure (Neville's algorithm) as follows: + + Q₀¹(t) + ↗ ↖ + (t₁ - t) / (t₁ - t₀) ↗ ↖ (t - t₀) / (t₁ - t₀) + ↗ ↖ + Q₀⁰(t) = P₀ Q₁⁰(t) = P₁ + +As we can see, the labels attached to the arrows correspond to the weights of +the affine combinations (3). + +### Pseudo-code of Neville's algorithm + + function NEVILLE({P₁, ..., Pᵤ}): + for i from 0 to u by 1 do: + Qᵢ = Pᵢ + + for j from 1 to u by 1 do: + for i from 0 to u − j by 1 do: + Qᵢ = ((tᵢ₊ⱼ - t) * Qᵢ + (t - tᵢ) * Qᵢ₊₁) / (tᵢ₊ⱼ - tᵢ) + + return Q₀ + +#### Analysis of Neville's algorithm + +The previous algorithm takes O(n²) operations. It is clearly not as efficient as +Horner's method (but it solves the interpolation problem "on-the-fly"). + +### Notes + +- The main idea of Neville's algorithm is to approximate the value of a +polynomial at a particular point without having to first find all of the +coefficients of the polynomial. + +# References + +- Dr. prof. Kai Hormann's notes for the Numerical Algorithms course, fall, 2017. +- https://en.wikiversity.org/wiki/Numerical_Analysis/Neville%27s_algorithm_examples +- https://en.wikipedia.org/wiki/Vandermonde_matrix +- https://www.cse.wustl.edu/~furukawa/cse452/slides/16_interpolation.pdf +- http://people.math.sfu.ca/~kam33/teaching/316-10.09/neville.pdf +- https://mail.scipy.org/pipermail/scipy-user/2003-August/001864.html +- https://people.clas.ufl.edu/maia/files/Lecture3.1.pdf +""" + +__all__ = ["neville"] + + +def neville(xs: list, ys: list, x0: float) -> float: + """Given n points xs[i], for i = 0, ..., n - 1, ys[i] = f(xs[i]), where f is + some function. + + Neville's algorithm approximates the value of a polynomial q (of degree + n - 1) at a particular point x0, i.e. it approximates q(x0), without having + to first find all the coefficients of the polynomial associated with a + monomial basis. + + This polynomial q interpolates the n points xs[i] of f, that is, + f(xs[i]) = ys[i] = q(xs[i]), for i = 0, ..., n - 1. So, at these points, the + error of the approximation of the function f using the polynomial q is zero. + However, at other points k of the domain of f, q(k) may be very different + from f(k), that is the error of the approximation of the function using the + polynomial varies from point to point of the domain of f. We define the + error as e(k) = f(k) – q(k). In general, we are most interested in the + maximum of |e| over the domain of f. This maximum is called "error bound". + + Time complexity: O(n²).""" + if len(xs) != len(ys): + raise ValueError("Lists xs and ys have different lengths.") + + n = len(xs) + q = n * [0] + + for j in range(n): + for i in range(n - j): + if j == 0: # Base case: Qᵢ = Pᵢ + q[i] = ys[i] + else: + # Qᵢʲ(x0) = (tᵢ₊ⱼ - x0) / (tᵢ₊ⱼ - tᵢ) * Qᵢʲ⁻¹(x0) + + # (x0 - tᵢ) / (tᵢ₊ⱼ - tᵢ) * Qᵢ₊₁ʲ⁻¹(x0) + q[i] = ((x0 - xs[i + j]) * q[i] + (xs[i] - x0) * q[i + 1]) / ( + xs[i] - xs[i + j] + ) + + return q[0] # Q₀ diff --git a/andz/algorithms/numerical/newton.py b/andz/algorithms/numerical/newton.py new file mode 100644 index 00000000..8765716c --- /dev/null +++ b/andz/algorithms/numerical/newton.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 23/09/2017 + +Updated: 23/09/2017 + +# Description + +Newton's method is an iterative method that can be used to calculate, e.g., (an +approximation of) the square root of a number or the reciprocal of a number. In +general, it used to find the root of a function. If a problem can be formulated +as a root-finding problem, then Newton's method can potentially be used. + +Newton's method, also called the Newton–Raphson method, usually converges much +faster than the linearly convergent methods. + +To find a root of f(x) = 0, a starting guess x0 is given, and the tangent line +to the function f at x0 is drawn. The tangent line will approximately follow the +function down to the x-axis toward the root. The intersection point of the line +with the x-axis is an approximate root, but probably not exact if f curves. +Therefore, this step is iterated. + +The tangent line at x0 has slope given by the derivative f'(x0). One point on +the tangent line is (x0, f(x0)). The point-slope formula for the equation of a +line is y − f(x0) = f'(x0)(x − x0), so that looking for the intersection point +of the tangent line with the x-axis is the same as substituting y = 0 in the +line: + + f'(x0)(x − x0) = 0 - f(x0) <=> + f'(x0)(x - x0) = -f(x0) <=> + x - x0 = -(f(x0) / f'(x0)) <=> + x = x0 - (f(x0) / f'(x0)) + +Solving for x gives an approximation for the root, which we call x1. Next, the +entire process is repeated, beginning with x1, to produce x2, and so on, +yielding the following iterative formula: + + x0 = initial guess + xᵢ₊₁ = xᵢ - (f(xᵢ) / f'(xᵢ)), for i = 0, 1, 2, ... + +## Examples + +### Example 1 + +Find the Newton's method formula for the equation f(x) = x³ + x − 1 = 0. We find +f'(x) = 3x² + 1. Then we apply the formula above to this f, so we obtain + + xᵢ₊₁ = xᵢ - (xᵢ³ + xᵢ − 1 / 3xᵢ² + 1) <=> + xᵢ₊₁ = xᵢ - (xᵢ³ + xᵢ − 1 / 3xᵢ² + 1) <=> + xᵢ₊₁ = ((3xᵢ² + 1)xᵢ - (xᵢ³ + xᵢ − 1)) / (3xᵢ² + 1) <=> + xᵢ₊₁ = (3xᵢ³ + xᵢ - xᵢ³ - xᵢ + 1) / (3xᵢ² + 1) <=> + xᵢ₊₁ = (2xᵢ³ + 1) / (3xᵢ² + 1) + +# TODO + +- Analysis of the convergence of Newton's method. + +# References + +- https://en.wikipedia.org/wiki/Newton%27s_method +- Dr. prof. Kai Hormann's notes for the Numerical Algorithms course, fall, 2017. +- Chapter 1 of "Numerical Analysis" (2nd ed.) by Sauer. +""" + +__all__ = ["newton"] + + +def newton( + x0: float, + f: callable, + df: callable, + max_iter: int = 20, + tolerance: float = 1e-6, + epsilon: float = 1e-12, +) -> float: + """Returns an approximation of the closest root to x0 of f, provided that x0 + is "close enough" to one of the roots of f. + + It returns a "garbage" value if either f does not have roots or Newton's + method does not converge, given the initial guess x0. + + If f or df are not callable, TypeError is raised.""" + if not callable(f) or not callable(df): + raise TypeError("f and df must be callable objects.") + x = x0 + for _ in range(max_iter): + + # We don't want to divide by a too small number. + if abs(df(x)) < epsilon: + break + + # Newton's computation. + x_next = x - f(x) / df(x) + + # If the relative error is smaller than a certain tolerance, we exit the + # loop and return x_next. + if abs(x_next - x) < tolerance * abs(x_next): + x = x_next + break + + x = x_next + return x diff --git a/andz/algorithms/ode/__init__.py b/andz/algorithms/ode/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/andz/algorithms/ode/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/andz/algorithms/ode/forward_euler.py b/andz/algorithms/ode/forward_euler.py new file mode 100755 index 00000000..ba326562 --- /dev/null +++ b/andz/algorithms/ode/forward_euler.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 10/09/2016 + +Updated: 07/03/2018 + +# Description + +The forward Euler's method is the easiest method for approximately solving an +initial value ODE problem. In practice, it's used as a vehicle for studying +several important and basic notions on numerical ODE methods. + +## Euler's method derivation + +We first consider finding an approximate solution for a scalar initial value ODE +problem at equidistant abscissae. Thus, we define the points: + + t₀ = a + + tᵢ = a + i * h, + +where h = (b - a) / N is the step size, for i = 0, 1, 2, ... N. t here is called +an "independent variable", a and b are respectively the beginning and end of the +interval at which we're trying to numerical approximate y(t). + +We denote the approximate solution to y(tᵢ) by yᵢ. +Note: in general, the step size h could be variable, i.e. we could have a hᵢ, +but we keep it constant in this explanation and implementation. + +Consider the following "forward difference" formula: + + y'(tᵢ) = (y(tᵢ₊₁) - y(tᵢ)) / h - h / 2 * y''(Ζᵢ) + +By the ODE, y'(tᵢ) = f(tᵢ, y(tᵢ)). So, we manipulate the expression above to +have y(tᵢ₊₁) isolated in one side. We start by multiplying both sides by h: + + h * y'(tᵢ) = y(tᵢ₊₁) - y(tᵢ) - (h² / 2 * y''(Ζᵢ)) + +or, if we rearrange the entries, + + h * y'(tᵢ) + y(tᵢ) + (h² / 2 * y''(Ζᵢ)) = y(tᵢ₊₁) + +we further rearrange the entries so that what we're looking for is on the left +side of the equals: + + y(tᵢ₊₁) = y(tᵢ) + h * y'(tᵢ) + (h² / 2 * y''(Ζᵢ)) + +and we replace y'(tᵢ) by f(tᵢ, y(tᵢ)) + + y(tᵢ₊₁) = y(tᵢ) + h * f(tᵢ, y(tᵢ)) + (h² / 2 * y''(Ζᵢ)) + +dropping the truncation term, i.e. (h² / 2 * y''(Ζᵢ)), we obtain the forward +Euler method, which defines the approximate solution (yᵢ){ᵢ₌₀}^{N} by: + + y₀ = c + +where c is the initial value. + + y(tᵢ₊₁) = y(tᵢ) + h * f(tᵢ, y(tᵢ)) + +for i = 0, ..., N - 1. + +This simple formula allow us to march forward into t. + +### Notes + +In the following implementation, we assume that f and all other parameters are +specified. + +## Explicit vs Implicit methods + +What happens if we replace the forward difference formula by the backward +formula + + y'(tᵢ₊₁) ~ (y(tᵢ₊₁) - y(tᵢ)) / h + +and this leads similarly to the backward Euler method: + + y₀ = c + yᵢ₊₁ = yᵢ + h * f(tᵢ₊₁, yᵢ₊₁) + +for i = 0, ..., N - 1. + +There's actually a big difference between this new method and the previous one. +This one to calculate yᵢ₊₁ depends implicitly on yᵢ₊₁ itself. + +In general, if a method to calculate yᵢ₊₁ depends implicitly on yᵢ₊₁, it's +called an implicit method, whereas the forward method is considered a explicit +method. + +# References + +- First Course in Numerical Methods, chapter 16, by Uri M. Ascher and C. Greif +- https://www.khanacademy.org/math/ap-calculus-bc/diff-equations-bc/eulers-method-bc/v/ + eulers-method-program-code +""" + +__all__ = ["forward_euler", "forward_euler_approx"] + +from numpy import arange, zeros + + +def forward_euler(a: float, b: float, n: int, c: float, f: callable) -> tuple: + """Forward Euler method, with y = f(x, y), with initial value c, and range + [a, b]. n is the number of times to split the range [a, b], and is thus used + to calculate the step size h. + + It returns a tuple, whose first element is the array of abscissas, i.e. the + values of t during the iterations, and the second element is the array of + ordinates, i.e. the values of y during the iterations.""" + if a is None or b is None or n is None or c is None: + raise ValueError("a, b, n and c must not be None.") + if b < a: + raise ValueError("b < a, but it should be a <= b.") + if not callable(f): + raise TypeError("f should be a callable object.") + + h = (b - a) / n + + # t is an array of abscissas. + t = arange(a, b, h) + + # y is an array of ordinates. + y = zeros(n) + y[0] = c + + for i in range(n - 1): + y[i + 1] = y[i] + h * f(t[i], y[i]) + + return t, y + + +def forward_euler_approx(a: float, b: float, n: int, c: float, f: callable) -> float: + """Forward Euler method, with y = f(x, y), with initial value c, and range + [a, b]. n is the number of times to split the range [a, b], and is thus used + to calculate the step size h. + + It returns just y[b]. + + Use this function in case space requirements are a must.""" + + if a is None or b is None or n is None or c is None: + raise ValueError("a, b, n and c must not be None.") + if b < a: + raise ValueError("b < a, but it should be a <= b") + if not callable(f): + raise TypeError("f should be a callable object") + + t = a + y = c + h = (b - a) / n + + for _ in range(n - 1): + y += h * f(t, y) + t += h + + return y diff --git a/andz/algorithms/recursion/README.md b/andz/algorithms/recursion/README.md new file mode 100644 index 00000000..62087c61 --- /dev/null +++ b/andz/algorithms/recursion/README.md @@ -0,0 +1,7 @@ +# [Recursion](https://en.wikipedia.org/wiki/Recursion_(computer_science)) + +A few simple examples of recursive algorithms. + +## Resources + +- [https://www.khanacademy.org/computing/computer-science/algorithms/recursive-algorithms/a/recursion](https://www.khanacademy.org/computing/computer-science/algorithms/recursive-algorithms/a/recursion) \ No newline at end of file diff --git a/andz/algorithms/recursion/__init__.py b/andz/algorithms/recursion/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/andz/algorithms/recursion/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/andz/algorithms/recursion/ackermann.py b/andz/algorithms/recursion/ackermann.py new file mode 100644 index 00000000..4bd75338 --- /dev/null +++ b/andz/algorithms/recursion/ackermann.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 22/02/2016 + +Updated: 19/09/2017 + +# Description + +The Ackermann function is the simplest example of a well defined total function. +Total function means that it's defined for all possible inputs. The function is +computable, but not primitive recursive. A primitive recursive function is a +function that can be implemented using only "for" loops, i.e. loops that have a +fixed number of iterations. A computable function is a function that can be +implemented using "while" loops. Note: "do" loops are a particular case of while +loops. + +It grows faster than an exponential function, or even a multiple exponential +function. + +# References + +- http://mathworld.wolfram.com/AckermannFunction.html +- http://math.stackexchange.com/questions/75296/ + what-is-the-difference-between-total-recursive-and-primitive-recursive-functions +- https://en.wikipedia.org/wiki/Ackermann_function +""" + +__all__ = ["ackermann"] + + +def ackermann(m: int, n: int) -> int: + """ + Compute the Ackermann function. + + The Ackermann function is a total computable function that is not primitive recursive. + + See + + - https://en.wikipedia.org/wiki/Computable_function + - https://en.wikipedia.org/wiki/Primitive_recursive_function + """ + assert m >= 0 and n >= 0 + if m == 0: + return n + 1 + if n == 0: + return ackermann(m - 1, 1) + return ackermann(m - 1, ackermann(m, n - 1)) diff --git a/andz/algorithms/recursion/count.py b/andz/algorithms/recursion/count.py new file mode 100755 index 00000000..4a8de242 --- /dev/null +++ b/andz/algorithms/recursion/count.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 04/08/2015 + +Updated: 19/09/2017 + +# Description + +A very simple example of how to count the number occurrences of a certain object +o in a list ls. + +You should not use recursion in general for doing this task: for example, in +Python the stack limit is quite small: 1000. +""" + +__all__ = ["count"] + + +def _count(elem: object, ls, index: int) -> int: + if index < len(ls): + if ls[index] == elem: + return 1 + _count(elem, ls, index + 1) + return _count(elem, ls, index + 1) + return 0 + + +def count(elem: object, ls) -> int: + """Counts how many times elem appears in the list or tuple ls.""" + return _count(elem, ls, 0) diff --git a/andz/algorithms/recursion/factorial.py b/andz/algorithms/recursion/factorial.py new file mode 100755 index 00000000..f0693bd1 --- /dev/null +++ b/andz/algorithms/recursion/factorial.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 17/07/2015 + +Updated: 19/09/2017 + +# Description + +The factorial of a number n is defined recursively as follows: + + fact(n): + # Assume n is int and n >= 0 + if n == 0 or n == 1: + return 1 + else: + return n * fact(n - 1) # n * (n - 1)! + +# References + +- http://www.math.uah.edu/stat/foundations/Structures.html#com2 +""" + +__all__ = ["factorial", "iterative_factorial", "smallest_geq", "multiple_factorial"] + + +def factorial(n: int) -> int: + """Returns the factorial of n, which is calculated recursively, as it's + usually defined mathematically.""" + assert n >= 0 + if n == 0: + return 1 + if n == 1 or n == 2: # pylint: disable=consider-using-in + return n + return n * factorial(n - 1) + + +def iterative_factorial(n: int) -> int: + """Returns the factorial of n, which is calculated iteratively. + + This is just for comparison with the recursive implementation. + + Since the "factorial" is a primitive recursive function, it can be + implemented iteratively. + + Proof that factorial is a primitive recursive function: + https://proofwiki.org/wiki/Factorial_is_Primitive_Recursive. + + A primitive recursive function is a recursive function which can be + implemented with "for" loops. + See: http://mathworld.wolfram.com/PrimitiveRecursiveFunction.html.""" + assert n >= 0 + if n == 0 or n == 1: # pylint: disable=consider-using-in + return 1 + f = 1 + for i in range(2, n + 1): + f *= i + return f + + +def smallest_geq(x: int) -> int: + """Returns the smallest number n such that n! >= x. + + "geq" stands for "greater or equal".""" + assert x >= 0 + + n = 0 + while iterative_factorial(n) < x: + n += 1 + + return n + + +def _multiple_factorial(n: int, i: int, a: list) -> list: + if i <= n: + a.append(factorial(i)) + _multiple_factorial(n, i + 1, a) + return a + + +def multiple_factorial(n: int) -> list: + """Returns a list L of factorials from 0 to n, that is: + L[0] := 0! + L[1] := 1! + ... + L[n] := n! + If n is a negative number, returns an empty list.""" + assert n >= 0 + return _multiple_factorial(n, 0, []) diff --git a/andz/algorithms/recursion/hanoi.py b/andz/algorithms/recursion/hanoi.py new file mode 100644 index 00000000..48e3dec1 --- /dev/null +++ b/andz/algorithms/recursion/hanoi.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 27/02/2016 + +Updated: 19/09/2017 + +# Description + +Towers of Hanoi is a mathematical game. It consists of 3 rods, and a number of +disks of different sizes, which can slide onto any rod. The game starts with the +disks in a neat stack in ascending order of size on one rod, the smallest at the +top, thus making a conical shape. + +The objective of the game is to move the entire stack to another rod, obeying +the following rules: + + 1. Only 1 disk can be moved at a time. + + 2. Each move consists of taking the upper disk from one of the stacks and + placing it on top of another stack, i.e. a disk can only be moved if it is + the uppermost disk on its stack. + + 3. No disk may be placed on top of a smaller disk. + +With 3 disks, the game can be solved with at least 7 moves (best case). + +The minimum number of moves required to solve a tower of hanoi game +is 2^n - 1, where n is the number of disks. + +For simplicity, in the following algorithm +the source (='A'), auxiliary (='B') and destination (='C') rodes are fixed, and +therefore the algorithm always shows the steps to go from 'A' to 'C'. + +# References + +- https://en.wikipedia.org/wiki/Tower_of_Hanoi +- http://www.cut-the-knot.org/recurrence/hanoi.shtml +- http://stackoverflow.com/questions/105838/real-world-examples-of-recursion +""" + +__all__ = ["hanoi"] + + +def _hanoi(n: int, ls: list, src="A", aux="B", dst="C") -> list: + """Recursively solve the Towers of Hanoi game for n disks. + + The smallest disk, which is the topmost one at the beginning, is called 1, + and the largest one is called n. + + src is the start rod where all disks are set in a neat stack in ascending + order. + + aux is the third rod. + + dst is similarly the destination rod.""" + if n > 0: + _hanoi(n - 1, ls, src, dst, aux) + ls.append((n, src, dst)) + _hanoi(n - 1, ls, aux, src, dst) + return ls + + +def hanoi(n: int) -> list: + """Returns a list L of tuples each of them representing a move to be done, + for n number of disks and 3 rods. + + L[i] must be done before L[i + 1], for all i. + L[i][0] := the disk number (or id). + Numbers start from 1 and go up to n. + L[i][1] := the source rod from which to move L[i][0]. + L[i][2] := the destination rod to which to move L[i][0]. + + The disk with the smallest radius (at the top) is the disk number 1, + its successor in terms or radius' size is disk number 2, and so on. + So the largest disk is disk number n.""" + assert n >= 0 + return _hanoi(n, []) diff --git a/andz/algorithms/recursion/is_sorted.py b/andz/algorithms/recursion/is_sorted.py new file mode 100644 index 00000000..c4b16461 --- /dev/null +++ b/andz/algorithms/recursion/is_sorted.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 21/01/2017 + +Updated: 19/09/2017 + +# Description + +is_sorted checks if a list or tuple contains elements in sorted order by using +recursion. This algorithm can potentially be modified to work with other +collections. The other versions are here just for comparison. +""" + +import operator + +__all__ = ["is_sorted", "iterative_is_sorted", "pythonic_is_sorted"] + + +def _is_sorted(a: list, i: int, op) -> bool: + """i is used to index the two adjacent elements of a. + + op can either be >, if a should be in ascending order, or <, if a should be + in descending order.""" + # If i is the last index, there's nothing more to check, thus the list is + # sorted. + if i == len(a) - 1: + return True + if op(a[i], a[i + 1]): + return False + return _is_sorted(a, i + 1, op) + + +def is_sorted(a: list, rev=False) -> bool: + """Checks recursively if a is sorted. + + If rev is true, this function checks if a is sorted in descending order, + else if it's sorted in ascending order.""" + if len(a) < 2: + return True + + op = operator.gt + if rev: + op = operator.lt + + return _is_sorted(a, 0, op) + + +def iterative_is_sorted(a, rev=False) -> bool: + """Iterative alternative to is_sorted. + + Time complexity: O(n).""" + if len(a) < 2: + return True + + op = operator.gt + if rev: + op = operator.lt + + for i in range(len(a) - 1): + if op(a[i], a[i + 1]): + return False + + return True + + +def pythonic_is_sorted(a, rev=False) -> bool: + """Checking if a is sorted in a shorter way by using the all function.""" + op = operator.le + if rev: + op = operator.ge + return all(op(a[i], a[i + 1]) for i in range(len(a) - 1)) diff --git a/andz/algorithms/recursion/make_decimal.py b/andz/algorithms/recursion/make_decimal.py new file mode 100755 index 00000000..fa89f215 --- /dev/null +++ b/andz/algorithms/recursion/make_decimal.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 08/08/2015 + +Updated: 19/01/2017 + +# Description + +Converts a number in a certain base in the range [2, 36] to a decimal number. +Bases greater than 10 take in order as digits the letters of the English +alphabet. For example, a number system with base 11, would take 'a' as the 11th +digit. +""" + +from string import ascii_lowercase, digits + +__all__ = ["make_decimal", "ALPHA_NUMERIC_ALPHABET"] + + +def _build_alpha_numeric_alphabet() -> dict: + """Returns a dictionary whose keys are all nine digits from 0 to 9 and the + 26 letters of the English alphabet. + + The values of the numbers are the numbers themselves. The values of the + letters are 10 for 'a', 11 for 'b', and so on until 36 for 'z'.""" + alphabet = {} + for i, char in enumerate(ascii_lowercase): + # Letters of the alphabet start after digit 9. + alphabet[char] = i + 10 + for i, char in enumerate(digits): + alphabet[char] = i + return alphabet + + +ALPHA_NUMERIC_ALPHABET = _build_alpha_numeric_alphabet() + + +def _make_decimal(n: str, b: int, pos: int) -> int: + """Suppose we have a number n=xyz (represented as string) in base b. + + The algorithm of converting it to a decimal representation is as follows. + + b⁰ * decimal_value(z) + b¹ * decimal_value(y) + b² * decimal_value(x), + + where decimal_value(z) is the decimal value of the digit z. + + For example, suppose "ef2" is an hexadecimal number that we want to convert + to decimal, which should yield 3826. + + 16⁰ * decimal_value(2) + + 16¹ * decimal_value(f) + + 16² * decimal_value(e) = + 1 * 2 + 16 * 15 + 256 * 14 = + 2 + 240 + 3584 = + 3826 + + Note: in any number n=xyz in any base b, z is in the "ones" position (has + the smaller "value"), y is in the "tens" position and x is in the "hundreds" + position (has the greatest "value"). + + See: http://www.math.com/school/subject1/lessons/S1U1L1GL.html.""" + if len(n) == 0: + return 0 + last = b**pos * ALPHA_NUMERIC_ALPHABET[n[-1]] + return _make_decimal(n[:-1], b, pos + 1) + last + + +def make_decimal(n: str, base: int) -> int: + """n is a number in any base in the range [2, 36]. base is the base in which + n is currently represented. + + Assumes n only contains digits in the range 0..9 and letters of the English + alphabet. + + Returns the decimal representation of n (as a int).""" + if not n: + raise ValueError("n cannot be an empty string or None") + if base > 36 or base < 2: + raise ValueError("not base >= 2 and base <= 36") + return _make_decimal(n, base, 0) diff --git a/andz/algorithms/recursion/palindrome.py b/andz/algorithms/recursion/palindrome.py new file mode 100755 index 00000000..1126a3ee --- /dev/null +++ b/andz/algorithms/recursion/palindrome.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 01/08/2015 + +Updated: 15/03/2022 + +# Description + +Checking recursively if a string is a palindrome, which is a string that reads +the same way forward and backward. For example, "anna" is a palindrome, whereas +"prime" is not. + +# TODO + +- This does not just apply to strings, but other sequences as well. + +# References + +- https://en.wikipedia.org/wiki/Palindrome +""" + +__all__ = ["is_palindrome", "iterative_is_palindrome"] + + +def _is_palindrome_aux(s: str, l: int, r: int) -> bool: + """l is the index that indexes s from the left and, similarly, r indexes it + from the right.""" + if l >= r: + return True + if s[l] == s[r]: + return _is_palindrome_aux(s, l + 1, r - 1) + return False + + +def is_palindrome(s: str) -> bool: + """Returns true if the string s is a palindrome, false otherwise.""" + if len(s) <= 1: + return True + return _is_palindrome_aux(s, 0, len(s) - 1) + + +# TODO: create another version that does not use s[::-1]. +def iterative_is_palindrome(s: str) -> bool: + """ + Check if s is a palindrome by reversing s with s[::-1]. + """ + return s == s[::-1] + + +# pylint: disable=missing-function-docstring +def test1(): + print(iterative_is_palindrome("")) + + +if __name__ == "__main__": + test1() diff --git a/andz/algorithms/recursion/power.py b/andz/algorithms/recursion/power.py new file mode 100755 index 00000000..91fe8ef6 --- /dev/null +++ b/andz/algorithms/recursion/power.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 25/07/2015 + +Updated: 19/09/2017 + +# Description + +Raising an integer a to the k >= 0 using recursion, i.e., aᵏ = b. +""" + +__all__ = ["power"] + + +def power(base: int, p: int) -> int: + """Assumes inputs are integers and that the power p >= 0. + + Base case: a⁰ = 1. + Recursive step: aⁿ⁺¹ = aⁿ * a.""" + assert p >= 0 + if p == 0: + return 1 + return base * power(base, p - 1) diff --git a/andz/algorithms/recursion/reverse.py b/andz/algorithms/recursion/reverse.py new file mode 100755 index 00000000..fa357200 --- /dev/null +++ b/andz/algorithms/recursion/reverse.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 22/07/2015 + +Updated: 15/03/2022 + +# Description + +Reverses in-place the elements of a list using recursion. + +This method could also be adapted to work with other mutable collections. + +# TODO + +- Implement the iterative versions with slicing, i.e. s[::-1], and without it. +""" + +__all__ = ["reverse"] + + +def _reverse_aux(ls: list, i: int, j: int) -> list: + if (j - i) >= 1: + ls[j], ls[i] = ls[i], ls[j] + _reverse_aux(ls, i + 1, j - 1) + return ls + + +def reverse(ls: list) -> list: + """Returns the reverse of the list ls using recursion.""" + if len(ls) < 2: + return ls + return _reverse_aux(ls, 0, len(ls) - 1) diff --git a/andz/algorithms/sorting/README.md b/andz/algorithms/sorting/README.md new file mode 100644 index 00000000..cd4139e2 --- /dev/null +++ b/andz/algorithms/sorting/README.md @@ -0,0 +1,44 @@ +# [Sorting Algorithms](https://en.wikipedia.org/wiki/Sorting_algorithm) + +Algorithms to sort sequences. + +The algorithms under [`comparison`](./comparison) are comparison sorting algorithms, while the algorithms under [`integer`](./integer) are integer sorting algorithms, which can beat the `Ω(n * log(n))` lower bound of comparison sorting algorithms. + +## Properties + +There are different properties of sorting algorithms that one might be interested in. + +- Time complexity +- Space complexity +- Stability +- In-place vs not-in-place +- Comparison sorting vs integer sorting +- Recursive +- Method: insertion, merging, exchange, selection, etc. + +## TODO + +### Comparison sorting + +- In-place merge sort +- Intro sort +- Block sort +- Tim sort +- Cubesort +- Shell sort +- Exchange sort +- Binary Tree sort +- Cycle sort +- Library sort +- Patience sort +- Smooth sort +- Strand sort +- Tournament sort +- Cocktail shaker sort +- Comb sort +- Gnome sort +- Odd-even sort + +### Non-comparison sorting + +- Bucket sort \ No newline at end of file diff --git a/andz/algorithms/sorting/__init__.py b/andz/algorithms/sorting/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/andz/algorithms/sorting/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/andz/algorithms/sorting/comparison/__init__.py b/andz/algorithms/sorting/comparison/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/andz/algorithms/sorting/comparison/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/andz/algorithms/sorting/comparison/bubble_sort.py b/andz/algorithms/sorting/comparison/bubble_sort.py new file mode 100755 index 00000000..538844ed --- /dev/null +++ b/andz/algorithms/sorting/comparison/bubble_sort.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 20/07/2015 + +Updated: 19/09/2017 + +# Description + +Bubble-sort is an algorithm which is used to sort N elements that are given in a +memory. For example, an array or list with N number of elements. Bubble-sort +compares all the element one by one and sort them based on their values. + +It is called bubble-sort, because with each iteration the smaller element in the +list bubbles up towards the first place, just like a water bubble rises up to +the water surface. + +Sorting takes place by stepping through all the data items one-by-one in pairs +and comparing adjacent data items and swapping each pair that is out of order. + +# TODO + +- Add ASCII animation of a sorting example using bubble-sort. + +# References + +- http://www.studytonight.com/data-structures/bubble-sort +- http://en.wikipedia.org/wiki/Bubble_sort +- http://interactivepython.org/runestone/static/pythonds/SortSearch/TheBubbleSort.html +- http://stackoverflow.com/questions/29555839/how-to-calculate-bubble-sort-time-complexity +""" + +__all__ = ["bubble_sort"] + + +def bubble_sort(ls: list) -> None: + """Bubble-sort in-place sorting algorithm. + + Time complexity + + +------+----------+----------+ + | Best | Average | Worst | + +------+----------+----------+ + | O(n) | O(n²) | O(n²) | + +------+----------+----------+ + + Note: best case is O(n) when ls is already sorted, and thus no swap occurs. + + Space complexity: O(1). + + Note: space complexity is O(1), but not considering memory for original + list ls.""" + assert isinstance(ls, list) + for i in range(len(ls) - 1): + for j in range(len(ls) - 1 - i): + if ls[j] > ls[j + 1]: + ls[j], ls[j + 1] = ls[j + 1], ls[j] diff --git a/andz/algorithms/sorting/comparison/heap_sort.py b/andz/algorithms/sorting/comparison/heap_sort.py new file mode 100755 index 00000000..d54b5819 --- /dev/null +++ b/andz/algorithms/sorting/comparison/heap_sort.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 09/09/2015 + +Updated: 19/09/2017 + +# Description + +Heap-sort is one of the best sorting methods being in-place and with no +quadratic worst-case scenarios. Heap-sort algorithm is divided into two basic +parts: + + 1. Creating a heap from a (possibly) unsorted list, then + + 2. a sorted list is created by repeatedly removing the largest/smallest + element from the heap, and inserting it into the list. + The heap is reconstructed after each removal. + +Heap-sort is somehow slower in practice on most machines than a well-implemented +quick-sort, but it has the advantage of a more favorable worst-case O(n * log n) +runtime. Heap-sort is an in-place algorithm, but it is not a stable sort. + +# TODO + +- Add ASCII animation of a sorting example using heap-sort! + +# References + +- https://en.wikipedia.org/wiki/Binary_heap +- https://en.wikipedia.org/wiki/Heapsort +- http://video.mit.edu/watch/introduction-to-algorithms-lecture-4-heaps-and-heap-sort-14154/ +- http://www.studytonight.com/data-structures/heap-sort +- https://en.wikipedia.org/wiki/Sorting_algorithm#Stability +""" + +__all__ = ["heap_sort", "build_max_heap", "max_heapify"] + + +def max_heapify(ls: list, heap_size: int, i: int) -> None: + """This operation is also sometimes called "push down", "shift_down" or + "bubble_down". + + Time complexity: O(log(n)).""" + m = i + left = 2 * i + 1 + right = 2 * i + 2 + if left < heap_size and ls[left] > ls[m]: + m = left + if right < heap_size and ls[right] > ls[m]: + m = right + if i != m: + ls[i], ls[m] = ls[m], ls[i] + max_heapify(ls, heap_size, m) + + +def build_max_heap(ls: list) -> None: + """Converts a list ls, which can be thought as a binary tree (not a + binary-search tree!) with n = len(ls) nodes, to a list representing a + max-heap by repeatedly using max_heapify in a bottom up manner. + + It is based on the observation that the list of elements indexed by + floor(n/2) + 1, floor(n/2) + 2, ..., n are all leaves for the tree + (assuming that indices start at 1), thus each is a 1-element heap. + + It runs max_heapify on each of the remaining tree nodes. + + For more info see: https://en.wikipedia.org/wiki/Binary_heap#Building_a_heap + + This algorithm initially proposed by Robert W. Floyd as an improvement to + the sub-optimal algorithm to build heaps proposed by the inventor of + max-heap and of the heap data structure, that is J. Williams. + + Time complexity: O(n).""" + for i in range(len(ls) // 2, -1, -1): + max_heapify(ls, len(ls), i) + + +def heap_sort(ls: list) -> None: + """Heap-sort in-place sorting algorithm. + + Time complexity + + +-------------+-------------+-------------+ + | Best | Average | Worst | + +-------------+-------------+-------------+ + | O(n*log(n)) | O(n*log(n)) | O(n*log(n)) | + +-------------+-------------+-------------+ + + Space complexity: O(1).""" + build_max_heap(ls) + for i in range(len(ls) - 1, 0, -1): + ls[i], ls[0] = ls[0], ls[i] + max_heapify(ls, i, 0) diff --git a/andz/algorithms/sorting/comparison/insertion_sort.py b/andz/algorithms/sorting/comparison/insertion_sort.py new file mode 100755 index 00000000..4808f858 --- /dev/null +++ b/andz/algorithms/sorting/comparison/insertion_sort.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 23/07/2015 + +Updated: 09/03/2022 + +# Description + +It is a simple sorting algorithm which sorts a list by shifting elements one by +one. + +## Properties + +- comparison-based sorting algorithm +- stable +- in-place +- efficient for "small" lists +- adaptive (reduces the number of comparison if the list is already sorted) +- Better than bubble-sort for almost-ordered lists, even though both have +a best-case time complexity of O(n). +- Online (can sort a list as it receives it) + +# TODO + +- Add ASCII animation of a sorting example using insertion-sort! +- Add key parameter to sort by key + +# References + +- http://en.wikipedia.org/wiki/Insertion_sort +- https://runestone.academy/ns/books/published/pythonds/SortSearch/TheInsertionSort.html +- http://www.studytonight.com/data-structures/insertion-sorting +""" + +__all__ = ["insertion_sort"] + + +def insertion_sort(ls: list) -> None: + """Insertion-sort in-place sorting algorithm. + + Time complexity + + +------+----------+----------+ + | Best | Average | Worst | + +------+----------+----------+ + | O(n) | O(n²) | O(n²) | + +------+----------+----------+ + + Space complexity: O(1).""" + for i in range(1, len(ls)): + n = i + while n > 0 and ls[n] < ls[n - 1]: + ls[n], ls[n - 1] = ls[n - 1], ls[n] + n -= 1 diff --git a/andz/algorithms/sorting/comparison/merge_sort.py b/andz/algorithms/sorting/comparison/merge_sort.py new file mode 100755 index 00000000..e2c00d25 --- /dev/null +++ b/andz/algorithms/sorting/comparison/merge_sort.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 09/09/2015 + +Updated: 09/03/2022 + +# Description + +Merge-sort is a sorting algorithm which follows the divide-and-conquer +strategy. + +In merge-sort the unsorted list is divided into N sub-lists, each having one +element. A list of one element is by definition sorted. And merging two sorted +lists of one element is easy, since you just need to understand which of the 2 +elements from the 2 different lists is greater or smaller. So, once all +1-element lists have been merged in 2-elements lists, we do the same +recursively until we have just 1 list. + +## Properties + +- Comparison-based sorting algorithm +- Divide-and-conquer algorithm +- Stable +- Not-in-place +- n*log(n) algorithm + +## Example + +Suppose we have initially the following list L of numbers: + + +-------------------+ + | 5 | 6 | 2 | 9 | 1 | + +-------------------+ + +Then we divide this list into 2 sub-lists as follows. Let's call this first +sub-list L1: + + +-----------+ + | 5 | 6 | 2 | + +-----------+ + +and this second let's call it L2 + + +-------+ + | 9 | 1 | + +-------+ + +Then we keep diving these two lists in half to obtain sub-lists of at most 1 +element. Let's first divide L1 into other two smaller sub-lists. The first +let's call it L11 and the second L12. + + +-------+ + | 5 | 6 | + +-------+ + +and + + +---+ + | 2 | + +---+ + +Now we divide L2 in two sub-lists. The first we call it L21 and the second L22. + + + +---+ + | 9 | + +---+ + +and + + +---+ + | 1 | + +---+ + +Now, apart from L11, all lists have already just one element. So, let's further +divide L11 in two smaller sub-lists, named respectively L111 and L112. + + +---+ + | 5 | + +---+ + +and + + + +---+ + | 6 | + +---+ + +Now, all lists have 1 element and, by definition, they are sorted. + +Let's start by merging L111 and L112 into a new list M: + + +-------+ + | 5 | 6 | + +-------+ + +Note that this new list M is the same as L11, because L11 was already sorted! + +We then merge M and L12 to obtain M1 as follows: + + +-----------+ + | 2 | 5 | 6 | + +-----------+ + +We then merge L21 and L22 as follows into a new list called M2 as follows: + + +-------+ + | 1 | 9 | + +-------+ + +Now we have two lists which are sorted which we can merge in linear time to +obtain the final result: + + +-------------------+ + | 1 | 2 | 5 | 6 | 9 | + +-------------------+ + +### The merge procedure + +Suppose we have two sorted lists (the ones from the merge of the example above) +A and B: + + +-----------+ + | 2 | 5 | 6 | + +-----------+ + +and + + +-------+ + | 1 | 9 | + +-------+ + +The merge procedure then works in general as follows. We create a new empty list +C, which will contain the final merged and sorted list. + + +---+ + | | + +---+ + +We then iterate through both lists using for each of them different indices, i +for the first one and j for the second. So the situation looks as follows: + + +-----------+ + | 2 | 5 | 6 | + +-----------+ + ^ + | + + i = 0 + +and + + +-------+ + | 1 | 9 | + +-------+ + ^ + | + + j = 0 + +We then compare the elements at i and j from both lists and take the smallest +one. In our case, the smallest one is the one at j = 0 from list B. We take it +from B and put it in C, which now looks as follows: + + +---+ + | 1 | + +---+ + +Then we increment j, i.e. j = j + 1, so j == 1, that is the situation looks as +follows + + +-------+ + | 1 | 9 | + +-------+ + ^ + | + + j = 1 + +The situation for A has not changed. We compare again the elements at position +i and j from both lists. Now, element at index i from A, that is 2, is smaller +than element at index j from B, that is 9, so we add 2 two C, and the C now +looks like this: + + +-------+ + | 1 | 2 | + +-------+ + +and the situation for A now looks as follows: + + +-----------+ + | 2 | 5 | 6 | + +-----------+ + ^ + | + + i = 1 + +We do again the same thing: compare elements at indices i and j. Now, element +at index i from list A, that is A[i] == 5, is smaller than B[j] == 9, so we +add 5 to list C, and it now looks as follows: + + +-----------+ + | 1 | 2 | 5 | + +-----------+ + +and the situation for A looks as follows: + + +-----------+ + | 2 | 5 | 6 | + +-----------+ + ^ + | + + i = 2 + +Now, I think you have understood the pattern. We add afterwards 6 (from A) and +finally 9 (from B) to C to obtain (as expected): + + +-------------------+ + | 1 | 2 | 5 | 6 | 9 | + +-------------------+ + +At the end i == 3 and j == 2. + +# TODO + +- Implement merge-sort in-place version. +- I don't remember if I implemented the top-down or bottom-up approach; in any +case, implement the other version too. +- Apparently, space complexity can be improved to O(log(n)) +- Add key parameter to sort by key + +# References + +- http://www.studytonight.com/data-structures/merge-sort +- http://interactivepython.org/runestone/static/pythonds/SortSearch/TheMergeSort.html +- http://en.wikipedia.org/wiki/Merge_sort +""" + +__all__ = ["merge_sort", "merge", "merge_recursively"] + + +def merge(left: list, right: list) -> list: + """Merges 2 sorted lists (left and right) in 1 single list, which is + returned at the end. + + Time complexity: O(m), where m = len(left) + len(right).""" + mid = [] + i = 0 # Used to index the left list. + j = 0 # Used to index the right list. + + while i < len(left) and j < len(right): + if left[i] < right[j]: + mid.append(left[i]) + i += 1 + else: + mid.append(right[j]) + j += 1 + + while i < len(left): + mid.append(left[i]) + i += 1 + + while j < len(right): + mid.append(right[j]) + j += 1 + + return mid + + +def merge_recursively(left: list, right: list) -> list: + """Equivalent to merge, but using recursion and creating new sub-lists at + each recursion call. + + You should use merge instead of this function, because the space complexity + of this algorithm is higher, since it uses the slice operation, which + creates additional unnecessary lists.""" + if len(left) == 0: + return right + if len(right) == 0: + return left + if left[0] < right[0]: + return [left[0]] + merge_recursively(left[1:], right) + return [right[0]] + merge_recursively(left, right[1:]) + + +def _merge_sort_aux(ls: list) -> list: + """Not-in-place sorting algorithm. + + Splits the original list ls until we have many sub-lists of one element + (which is by the way the base case). + + Note: a list of 1 element is sorted by definition. + + Using the merge algorithm, we can easily merge two sorted lists of size 1, + to obtain a merged sorted list of size 2. We keep merging greater sorted + lists, until we obtain the final sorted list. + + Time complexity: O(n * log(n))""" + + # Base case, where ls contains either 1 or 0 items, and it is by definition + # sorted. + if len(ls) < 2: + return ls + + # Calls merge_sort on the left half part of ls. + left = merge_sort(ls[0 : len(ls) // 2]) + + # Calls merge_sort on the right half part of ls. + right = merge_sort(ls[len(ls) // 2 :]) + + # Note that in the previous 2 statements, we are creating new sub-lists + # using ls[0:len(ls)//2], for the first case, for example. + + # Returns a new sorted list composed of the items in left and right. + return merge(left, right) + + +def merge_sort(ls: list) -> list: + """Merge-sort not-in-place sorting algorithm. + + Returns a new list containing the same elements as ls but sorted in + increasing order. + + Time complexity + + +-------------+-------------+-------------+ + | Best | Average | Worst | + +-------------+-------------+-------------+ + | O(n*log(n)) | O(n*log(n)) | O(n*log(n)) | + +-------------+-------------+-------------+ + + Space complexity: O(n).""" + return _merge_sort_aux(ls) diff --git a/andz/algorithms/sorting/comparison/quick_sort.py b/andz/algorithms/sorting/comparison/quick_sort.py new file mode 100755 index 00000000..cf2e6852 --- /dev/null +++ b/andz/algorithms/sorting/comparison/quick_sort.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 09/08/2015 + +Updated: 07/03/2018 + +# Description + +Quick-sort is a sorting algorithm which uses recursion and is composed of two +different procedures. + +## Procedures of the quick-sort algorithm + +1. Partition + + All elements smaller than one element of the input list, the "pivot", are + put to the left of the pivot. + + In this partition algorithm, the pivot is chosen to be the last element of + the list. In general, we may choose another pivot, e.g. the middle element. + + We keep searching for elements less than the pivot, from the left to the + right in the range [start, end[, and we insert them at the position tracked + by the variable p. + + So, p keeps track of the position (or index) in the range [start, end[, + where all elements to the left of p are smaller than the pivot. + + Before returning this position, p, the pivot is inserted in that position. + Note: by doing this, the pivot will be already in its final sorted position. + +2. Recursive Calls + + quick_sort is called recursively on the left of the pivot and on the right + until start >= end. + +# TODO + +- Add ASCII animation of a sorting example using quick-sort! +- Improve efficiency of best case time complexity and space complexity. +- Implement 3-way partition to improve best case of time complexity of +quick-sort. + +# References + +- http://en.wikipedia.org/wiki/Quicksort +- http://interactivepython.org/runestone/static/pythonds/SortSearch/TheQuickSort.html +- http://algs4.cs.princeton.edu/23quicksort/ +""" + +__all__ = ["quick_sort", "partition"] + + +def partition(ls: list, start: int, end: int) -> int: + """Shifts all elements in ls that are less than the pivot to the left of the + position p, which is at the end returned. + + Time complexity: O(k), where k is the size of ls.""" + pivot = ls[end] # Take last element as pivot. + p = start # Pivot's index. + + for i in range(start, end): + if ls[i] <= pivot: + ls[p], ls[i] = ls[i], ls[p] + p += 1 + + # Insert the pivot at index p (the pivot's index). + ls[p], ls[end] = ls[end], ls[p] + return p + + +def _quick_sort_aux(ls: list, start: int, end: int) -> None: + """Keeps calling partition to find the pivot index p, and then calls itself + recursively on the left and right sides of the pivot index.""" + if start < end: + # Returns the pivot index after partition. + p = partition(ls, start, end) + + # Calling _quick_sort_aux on the left side of the pivot. + _quick_sort_aux(ls, start, p - 1) + + # Calling quick_sort on the right side of the pivot. + _quick_sort_aux(ls, p + 1, end) + + +def quick_sort(ls: list) -> None: + """Quick-sort in-place sorting algorithm. + + Time complexity + + +-------------+-------------+----------+ + | Best | Average | Worst | + +-------------+-------------+----------+ + | O(n*log(n)) | O(n*log(n)) | O(n²) | + +-------------+-------------+----------+ + + Note: the best case can be improved to O(n) if a 3-way partition is used and + we have equal keys. + + Space complexity: O(n). + + Note: the space complexity can be improved to O(log(n)). How???""" + _quick_sort_aux(ls, 0, len(ls) - 1) diff --git a/andz/algorithms/sorting/comparison/selection_sort.py b/andz/algorithms/sorting/comparison/selection_sort.py new file mode 100755 index 00000000..50e83cce --- /dev/null +++ b/andz/algorithms/sorting/comparison/selection_sort.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 09/09/2015 + +Updated: 07/03/2018 + +# Description + +Selection sorting is conceptually probably the simplest sorting algorithm. + +This algorithm first finds the smallest element in the list and exchanges it +with the element in the first position, then find the second smallest element +and exchange it with the element in the second position, and continues in this +way until the entire list is sorted. + +# References + +- http://www.studytonight.com/data-structures/selection-sorting +- http://en.wikipedia.org/wiki/Selection_sort +- http://interactivepython.org/runestone/static/pythonds/SortSearch/TheSelectionSort.html +""" + +__all__ = ["selection_sort"] + + +def selection_sort(ls: list) -> None: + """Selection-sort in-place sorting algorithm. + + Time complexity + + +-------+----------+----------+ + | Best | Average | Worst | + +-------+----------+----------+ + | O(n²) | O(n²) | O(n²) | + +-------+----------+----------+ + + Space complexity: O(n).""" + for i in range(len(ls) - 1): + k = i + for j in range(i + 1, len(ls)): + if ls[j] < ls[k]: + ls[k], ls[j] = ls[j], ls[k] diff --git a/andz/algorithms/sorting/integer/__init__.py b/andz/algorithms/sorting/integer/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/andz/algorithms/sorting/integer/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/andz/algorithms/sorting/integer/counting_sort.py b/andz/algorithms/sorting/integer/counting_sort.py new file mode 100644 index 00000000..81fe08e4 --- /dev/null +++ b/andz/algorithms/sorting/integer/counting_sort.py @@ -0,0 +1,474 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 03/03/2022 + +Updated: 07/03/2022 + +# Description + +Counting sort (not in-place) algorithm. + +## Short Description + +Counting sort determines, for each element in the input list x ∈ a, the number +of elements less than x. It uses this information to place element x directly +into its position in the output list. For example, if 17 elements are less +than x, then x should be placed in position 18 in the sorted list. + +Counting sort assumes that each of the n = len(a) elements in a is an +integer in the range 0 to k - 1, for some integer k, which is the largest +possible number - 1 (although some descriptions and implementations assume +that k is the largest possible) in a. However, note that these integers, +sometimes called "keys" in this context, can have other associated data +(values). + +## Long description + +More specifically, counting sort keeps an auxiliary list c with k elements, all +initialized to 0. We make one pass through the input list a, and, for each +element x ∈ a that we see, we increment c[x] by 1. After we iterate through the +n elements of a and update c, the value at index i of c corresponds to how +many times i appeared in a. + +This step takes Θ(n) time. + +Once we have c, we can construct the sorted version b of a by iterating through +c and inserting each element x ∈ a a total of c[x] times into the new list b +(or a itself). + +Specifically, we continue from the point where c is a list where c[x] refers to +how many times x appears in a. We transform c to a list where c[x] refers to +how many elements are ≤ x. We do this by iterating through c and adding the +value at the previous index to the value at the current index, since the +number of elements ≤ x is equal to the number of elements ≤ x − 1 (i.e. the +value at the previous index) plus the number of elements = x (i.e. the value +at the current index). The final result is a list c where the value of c[x] is +the number of elements ≤ x in a. + +This step takes Θ(k) time. + +We now iterate through a backwards starting from the last element of a. +For each element x we see, we check c[x] to find out how many elements are ≤ x. +From this information, we know exactly where we can put x in the sorted list b. +Once we insert x into the sorted list, we decrement c[x] so that if we see a +duplicate element, we know that we have to insert it right before the previous +x. Once we finish iterating through a, we get a sorted list b. + +Note that since we iterate through a backwards and decrement c[x] every time +we see x, we preserve the order of duplicates in a. That is, if there are two +3s in a, we map the first 3 to an index before the second 3. This is the +reason why counting sort is a stable sorting algorithm. + +This step takes Θ(n) time. + +The 2nd and 3rd loop can be combined if the keys don't have any other +associated data, because, in that case, the same numbers are really +indistinguishable, so if have, say, three 5s, we can simply put them their +correct position without caring with 5 we place before another. + +## Properties + +Time complexity: Θ(n + k + n) = Θ(2n + k) = Θ(n + k). + +If k = O(n), then the time complexity is Θ(n), so counting sort because a +linear-time sorting algorithm. Thomas H. Cormen et al. state that, in practice, +counting sort is used when k = O(n). + +Counting sort beats the lower bound of Ω(n * log(n)) because it is not a +comparison sort, instead, counting sort uses the actual values of the +elements to index into a list. + +Counting sort is stable: numbers with the same value (e.g. two 2s) appear in +the output list in the same order as they do in the input list a. So, for +example, if we have an initial list a = [1, 2, 3, 2]. The first 2 at index +x = 1 will appear before the second 2 at index x = 3 in the sorted list b. + +The stability of a sorting algorithm can be important for multiple reasons. +For example, let's say that we want to sort the array +[(1, "no"), (0, "ok"), (1, "yes")]. If we sort this array according to +the first elements of the tuples, then (1, "no") would still come before +(1, "yes") in the sorted list, i.e. the sorted list would be +[(0, "ok"), (1, "no"), (1, "yes")] and not [(0, "ok"), (1, "yes"), (1, "no")]. +If this property is important, then counting sort can be useful. + +## Example + +Initial input list a that we want to sort. + + a = [4, 1, 3, 4, 3] + +Create the counter list and initialise all elements to zero. + + c = [0, 0, 0, 0, 0] + +After the first loop, the auxiliary counter list c looks as follows. + + Number of times 4 appears in a + ^ + | + c = [0, 1, 0, 2, 2] + | + v + number of times 2 appears in a + + +So, the index i in c correspond to the number i in the sequence {0,.. k - 1}. + +After the second loop, the counter list c is + + number of elements <= to number 3 + in fact, there is one 1 and two 3s, so c[3] = 1 + 2 = 3. + ^ + | + c = [0, 1, 1, 3, 5] + | + v + number of elements <= to number 1 + in fact, there's only one 1 and no zero, so c[1] = 1 + +Final loop + + b = [None, None, None, None, None] + +Iteration x = n - 1 = 4, where n = 5. + + a = [4, 1, 3, 4, 3] + | + x + + // There are 3 numbers <= 3 + c[x] = c[3] = 3 + c = [0, 1, 1, 3, 5] + + // The position of x=3 is at c[3] - 1 = 2 + // We decrement first c[3] because the indices are shifted by 1, + // and the maximum value in c is k = n + 1. + c[3] <- c[3] - 1 = 2 + c = [0, 1, 1, 2, 5] + + b[c[3]] = b[2] <- a[x] = 3 + b = [None, None, 3, None, None] + +Iteration x = n - 2 = 3. + + a = [4, 1, 3, 4, 3] + | + x + + // There are 5 numbers <= 4 + c[x] = c[4] = 5 + c = [0, 1, 1, 2, 5] + + // The position of x=4 is at c[4] - 1 = 4 + c[4] <- c[4] - 1 = 4 + c = [0, 1, 1, 2, 4] + + b[c[4]] = b[4] <- a[x] = 4 + b = [None, None, 3, None, 4] + +Iteration x = n - 3 = 2. + + a = [4, 1, 3, 4, 3] + | + x + + c[x] = c[3] = 2 + c = [0, 1, 1, 2, 4] + + c[3] <- c[3] - 1 = 1 + c = [0, 1, 1, 1, 4] + + b[c[3]] = b[1] <- a[x] = 3 + b = [None, 3, 3, None, 4] + + +Iteration x = n - 4 = 1. + + a = [4, 1, 3, 4, 3] + | + x + + c[x] = c[1] = 1 + c = [0, 1, 1, 1, 4] + + c[1] <- c[1] - 1 = 0 + c = [0, 0, 1, 1, 4] + + b[c[1]] = b[0] <- a[x] = 1 + b = [1, 3, 3, None, 4] + +Iteration x = n - 5 = 0. + + a = [4, 1, 3, 4, 3] + | + x + + c[x] = c[4] = 4 + c = [0, 1, 1, 1, 4] + + c[4] <- c[4] - 1 = 3 + c = [0, 0, 1, 1, 3] + + b[c[4]] = b[3] <- a[x] = 4 + b = [1, 3, 3, 4, 4] + +The final sorted list b and the counter are + + b = [1, 3, 3, 4, 4] + + c = [0, 0, 1, 1, 3] + +# Applications + +- Counting sort is often used as a subroutine in radix sort. + - The stability of counting sort is important in order for radix sort to + work correctly. +- sort strings by the first (second, third, etc.) letter +- sort phone numbers by area code + +# Invention + +Counting sort was invented by Harold H. Seward in 1954 in the paper +"Information sorting in the application of electronic digital computers to +business operations", section "2.4.6 Internal Sorting by Floating Digital +Sort", according to Donald Knuth. + +# Terminology + +- Counting sort is also called "key-indexed counting" by Robert Sedgewick and +Kevin Wayne in their book "Algorithms" (4th edition), section 5.1, page 703, +but it's in the context of sorting strings. + +- k is also called "radix"; this terminology will clarify why radix-sort, +which uses counting sort as a subroutine, is called radix-sort. In Sedgewick +and Wayne's book, k is denoted by R to remind us that it refers to the radix. + +# References + +- http://opendatastructures.org/ods-java/11_2_Counting_Sort_Radix_So.html +- Chapter 8.2, "Introduction to Algorithms" (3rd edition), by CLRS. +- Section 5.1, p. 703, "Algorithms" (4th edition), by Robert Sedgewick and +Kevin Wayne +- "31 2 Key Indexed Counting" +(https://www.youtube.com/watch?v=WrPm-Eqoicg&ab_channel=ComputerScience) video +lesson on "key-indexed counting" by Robert Sedgewick. +- https://courses.csail.mit.edu/6.006/spring11/rec/rec11.pdf +- https://en.wikipedia.org/wiki/Counting_sort +- https://www.ime.usp.br/~yoshi/Sedgewick/Algs4th/Slides/51StringSorts.pdf +""" + +__all__ = ["counting_sort"] + + +def counting_sort( + a: list, k: int = None, key: callable = lambda x: x, sedgewick_wayne: bool = False +) -> list: + """Counting sort algorithm, which is a stable algorithm. So, if + a[i] == a[j] and i < j, then a[i] still appears before a[j] in the sorted + list. It's also not in-place. + + Time complexity + + +-------------+-------------+-------------+ + | Best | Average | Worst | + +-------------+-------------+-------------+ + | | Θ(n + k) | Θ(n + k) | + +-------------+-------------+-------------+ + + Space complexity: Θ(n + k).""" + assert isinstance(a, list) + assert isinstance(sedgewick_wayne, bool) + + n = len(a) + + # If a is empty, return an empty list, and it does not check the + # correctness of any other argument. + if n == 0: + return [] + + if not callable(key): + raise TypeError("key should be a function") + + try: + for x in a: + key(x) + except Exception as exc: + raise KeyError("key is not valid") from exc + + if not all(isinstance(key(x), int) for x in a): + raise TypeError("the key attribute of each element of a should be an int") + + # In Sedgewick and Wayne's book, k is denoted by R. + if not isinstance(k, int): + k = key(max(a, key=key)) + 1 + + if k < 0: + raise ValueError("k must be greater than or equal to 0") + + if not all(0 <= key(x) < k for x in a): + raise ValueError( + "the key of each element in a should be between 0 " + "(included) and k (excluded)" + ) + + # In Sedgewick and Wayne's book, b is denoted by aux. + b = [None] * n + + def sw(): + # This implementation follows closely Sedgewick and Wayne's book. + # In Sedgewick and Wayne's book, c is denoted by count. + # c[0] will always be 0. + c = [0] * (k + 1) + + # Compute frequency counts. + for x in range(n): + c[key(a[x]) + 1] += 1 + + # Transform counts to indices. + for x in range(k): + c[x + 1] += c[x] + + # Distribute the records. + for x in range(n): + b[c[key(a[x])]] = a[x] + c[key(a[x])] += 1 + + def default(): + # This implementation is based on + # http://opendatastructures.org/ods-java/11_2_Counting_Sort_Radix_So.html. + + # An auxiliary counter list of size k with counters initialized to 0. + c = [0] * k + + for x in range(n): + c[key(a[x])] += 1 + + # Now, c[x] contains the number of elements = x. + + for x in range(1, k): + c[x] += c[x - 1] + + # c[x] now contains the number of elements ≤ x. + + # We place each element a[x] into its correct sorted position in b. + # + # If all n keys are distinct, then, when we first enter the + # following line, for each key(a[x]), the value c[key(a[x])] is the + # correct final position of key(a[x]) in the output list, since there + # are c[key(a[x])] elements less than or equal to a[x]. + # + # Because the elements might not be distinct, we decrement + # c[key(a[x])] each time we place a value a[x] into the list b. + # Decrementing c[key(a[x])] causes the next input element with a value + # equal to a[x], if one exists, to go to the position immediately + # before a[x] in b. + # + # If we started the loop from 0 to n - 1, this implementation + # would still produce a sorted list, but it would not be stable. We + # can easily see this from the example given in the doc-strings above. + # However, R. Sedgewick and K. Wayne (section 5.1, p. 703, of + # "Algorithms" (4th edition)) implement it slightly differently. See + # the function sw(). + for x in range(n - 1, -1, -1): + c[key(a[x])] -= 1 + b[c[key(a[x])]] = a[x] + + if sedgewick_wayne: + sw() + else: + default() + + return b + + +# pylint: disable=missing-function-docstring +def example1(): + a: list = [] + print(counting_sort(a)) + + a = [0] + print(counting_sort(a)) + a = [10, 2] + print(counting_sort(a)) + a = [4, 1, 3, 4, 3] + print(counting_sort(a)) + + # It ignores the non-integer second argument and calculates it based on a. + a = [2, 12, 3] + print(counting_sort(a, None)) + + # Error, as expected. + # a = [-1, 10] + # print(counting_sort(a)) + + +# pylint: disable=missing-function-docstring +def example2(): + # Example taken from R. Sedgewick and K. Wayne's book "Algorithms" + # (4th edition), section 5.1, p. 703. + a = [ + (2, "Anderson"), + (3, "Brown"), + (3, "Davis"), + (4, "Garcia"), + (1, "Harris"), + (3, "Jackson"), + (4, "Johnson"), + (3, "Jones"), + (1, "Martin"), + (2, "Martinez"), + (2, "Miller"), + (1, "Moore"), + (2, "Robinson"), + (4, "Smith"), + (3, "Taylor"), + (4, "Thomas"), + (4, "Thompson"), + (2, "White"), + (3, "Williams"), + (4, "Wilson"), + ] + + from pprint import pprint # pylint: disable=import-outside-toplevel + + key = lambda x: x[0] # pylint: disable=unnecessary-lambda-assignment + + b = counting_sort(a, key=key) + pprint(b) + assert b is not a + + pprint(counting_sort(a, key=key)) + + +# pylint: disable=missing-function-docstring +def example3(): + a = [(1, "no"), (0, "ok"), (1, "yes")] + from pprint import pprint # pylint: disable=import-outside-toplevel + + key = lambda x: x[0] # pylint: disable=unnecessary-lambda-assignment + b = counting_sort(a, key=key, sedgewick_wayne=True) + pprint(b) + + +# pylint: disable=missing-function-docstring +def example4(): + # Raises an exception (as expected). + a = [(1, "no"), (0, "ok"), (1, "yes")] + from pprint import pprint # pylint: disable=import-outside-toplevel + + key = lambda x: x[3] # pylint: disable=unnecessary-lambda-assignment + b = counting_sort(a, key=key, sedgewick_wayne=True) + pprint(b) + + +if __name__ == "__main__": + example1() + example2() + example3() + example4() diff --git a/andz/algorithms/sorting/integer/radix_sort.py b/andz/algorithms/sorting/integer/radix_sort.py new file mode 100644 index 00000000..d7efbe22 --- /dev/null +++ b/andz/algorithms/sorting/integer/radix_sort.py @@ -0,0 +1,395 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 05/03/2022 + +Updated: 09/03/2022 + +# Description + +## Short Description + +Radix sort is a non-comparison sorting algorithm that uses the stable +counting sort as a subroutine. The stability of counting sort is essential for +radix sort to work properly. + +## Long Description + +Radix-sort assumes that the given list contains integers, each of which has +a w-bit binary representation (for example, w = 32). It sorts the list by +calling w / d times the counting sort algorithm. In the first iteration, it +sorts (with counting sort) the list of integers according to the least +significant d bits of each integer. In the next iteration, it sorts according +to the next least significant d bits. This continues until the last iteration, +where the integers are sorted according to the most significant d bits. + +If w / d is not an integer, we can increase w to d⌈w/d⌉, where ⌈⌉ is the +ceiling operation. + +## Example + +In reality, radix sort can also be applied to non-binary representations. +For example, consider 3-digit decimal numbers: for example, 356, which has 3 +digits, each of which can range from 0 to 9. Now, here's an example of how +radix sort would work on a list of 3-digit decimal numbers (this example is +taken from Figure 8.3 of CLRS book, p. 198). Here's the initial list. +list. + + 329 + 457 + 657 + 839 + 436 + 720 + 355 + +We now sort these numbers according to the least significant digit. + + 720 + 355 + 436 + 457 + 657 + 329 + 839 + | + Least significant digit + +You can see that the least significant digits, i.e. 0, 5, 6, 7, 7, 9, 9, are +sorted. + +We now sort these numbers according to the second least significant digit, so +we obtain + + 720 + 329 + 436 + 839 + 355 + 457 + 657 + | +Second least significant digit + +You can see that the 2nd least significant digits, 2, 2, 3, 3, 5, 5, 5 are +sorted. However, note that the least significant digits are no longer sorted. +Nevertheless, note that, if we consider the last 2 digits, we have a sorted +list, i.e. 20, 29, 36, 39, 55, 57, 57. + +Finally, we sort the numbers according to the most significant digit, to obtain + + 329 + 355 + 436 + 457 + 657 + 720 + 839 + | + Most significant digit + +So, the most significant digits are sorted, 3, 3, 4, 4, 6, 7, 8. + +So, in this example, we performed 3 iterations of the stable sorting algorithm, +e.g. counting sort. + +## Properties + +- Non-comparison (integer) sorting algorithm +- Stable (because of the stability of the counting sort, which is used as a +subroutine) +- Not-in-place (at least the implementation below) + +## Applications + +- Sort records of information that are keyed by multiple fields. + - Example: sort dates by three keys: year, month, and day. + +# Invention + +- According to Wikipedia, radix-sort dates back to 1887 to the work of +Herman Hollerith on tabulating machines. It came into come use in 1923 as a +way to sort punched cards. Harold H. Seward, in 1954, developed the first +memory-efficient version. + +# Terminology + +- Robert Sedgewick and Kevin Wayne in their book "Algorithms" (4th edition), +section 5.1, page 706, call a version of radix-sort applied to strings +"least-significant-digit first (LSD) string sort". There are other similar +algorithms or variations. For example, there's the most-significant-digit first +(MSD) radix sort. + +- The "radix" refers to the base of the numbers that are given as input to the +algorithm. + +# TODO + +- The code used for counting sort here is really very similar to the standalone +counting sort algorithm in the module counting_sort.py, so we might want to +reuse that code. +- Add time and space complexity +- Is it in place? + +# References + +- "Introduction to Algorithms" (3rd edition), by CLRS, chapter 8.3 +- "Algorithms in C", R. Sedgewick, chapter 10, p. 133 +- "Algorithms" (4th edition), by Robert Sedgewick and Kevin Wayne +- http://opendatastructures.org/ods-java/11_2_Counting_Sort_Radix_So.html +- https://www.youtube.com/watch?v=YXFI4osELGU +- http://www.allisons.org/ll/AlgDS/Sort/Radix/ +- https://wiki.python.org/moin/BitwiseOperators +- https://en.wikipedia.org/wiki/Radix_sort +- https://brilliant.org/wiki/radix-sort/ +""" + +__all__ = ["radix_sort"] + +import math +import sys + + +def lsd( + a: list, + w: int = None, + # length of strings; we assume fixed-length strings here + # The value of k can make a big difference in performance. If we can + # assume e.g. that we're using only ASCII chars, then we know that k + # is quite small. + k: int = sys.maxunicode, + # TODO: support a more general key + # other we may need to assume that the first parameter is always a + # string (or something that we can index) and index would be the index + key: callable = lambda string, index: ord(string[index]), +) -> list: + """The least-significant-digit first (LSD) string sort, which stably sorts + fixed-length strings. + + This implementation is based on the pseudocode and information given in + section 5.1 of the book "Algorithms" (4th edition), by Robert Sedgewick and + Kevin Wayne. + + Time complexity + + +-------------+---------------+---------------+ + | Best | Average | Worst | + +-------------+---------------+---------------+ + | | Θ(2*w(n + k)) | Θ(2*w(n + k)) | + +-------------+---------------+---------------+ + + Space complexity: Θ(n + k).""" + if len(a) == 0: + return [] + if not isinstance(w, int): + w = len(a[0]) + if w < 0: + raise ValueError("w must be >= 0") + if not all(len(x) == w for x in a): + raise ValueError( + "the length of each string in a should be equal to " "w = {}".format(w) + ) + + # TODO: check correctness of key. + # TODO: check correctness of k, maybe as an assertion, because we may + # get index out of range + # TODO: support also the sort of integers (probably we need to convert them + # to strings): see CLRS. + # TODO: this is in-place, but the algorithm below is not. + + n = len(a) + + b = [None] * n + + for d in range(w - 1, -1, -1): + # Sort by key-indexed counting on dth char. + c = [0] * (k + 1) # In the book, it's denoted by "count" + + # Compute frequency counts. + for x in range(n): + c[key(a[x], d) + 1] += 1 + + # Transform counts to indices. + for x in range(k): + c[x + 1] += c[x] + + # Distribute. + for x in range(n): + b[c[key(a[x], d)]] = a[x] + c[key(a[x], d)] += 1 + + # Copy back. + for x in range(n): + a[x] = b[x] + + +# TODO: deal with the case where w is not divisible by d; don't forget the +# test cases. +# TODO: deal with wrong values for w and d. +# TODO: this implementation of radix-sort fails to sort if e.g. we try to sort +# numbers that cannot be represented as w-bit integers. For example, if we +# assume w=8, then we assume 8-bit integers, so integers up to 2^8 = 256. In +# the tests, we need to take this into account, otherwise, the tests may fail. +def radix_sort( + a: list, + w: int = 14, # We assume 8-bit integers. + d: int = 8, # We will sort the integers 1 bit at a time +) -> list: + """Radix-sort algorithm that sorts w-bit integers with w // d rounds of + the stable counting sort. At each round, we sort the list according to d + bits. + + Time complexity + + +--------------+--------------+--------------+ + | Best | Average | Worst | + +--------------+--------------+--------------+ + | | Θ(d*(n + k)) | Θ(d*(n + k)) | + +--------------+--------------+--------------+ + + """ + assert isinstance(a, list) + assert isinstance(w, int) + assert isinstance(d, int) + + if not all(isinstance(x, int) for x in a): + raise TypeError("all elements of a should be integers") + if not all(x >= 0 for x in a): + raise ValueError("all elements be >= 0") + + if not (1 <= w <= int(math.log2(sys.maxsize)) + 1): + raise ValueError( + "w should be an integer in the range " + "[1, {}]".format(int(math.log2(sys.maxsize)) + 1) + ) + + for x in a: + if not (0 <= x < 2**w): + raise ValueError( + "element {} of 'a' cannot be represented with {} " "bits".format(x, w) + ) + + # We need to consider at least 1 and at most w digit at a time. + if not (1 <= d <= w): + raise ValueError("d should be an integer in the range [1, w]") + + if w % d != 0: + # We could also set w to int(math.floor(w / d)) * d, as suggested here + # http://opendatastructures.org/ods-java/11_2_Counting_Sort_Radix_So.html + d = 1 + + assert w % d == 0 + + # If d == 1, then 1 << d == 1 << 1 == 10 (in binary) == 2 (in decimal). + k = 1 << d + + def key(x: int, p: int) -> int: + # x is an element of a + # p is the current iteration number + # if d == 1, then p is just the current digit/bit's position/index. + + # (x >> d * p) & (k - 1) is the integer whose binary + # representation is given by bits + # (p + 1) * d - 1, ..., p * d of x, where x = a[i]. + # + # Example + # + # Let x = 3, d = 2 and p = 0. + # Then (x >> d * p) & ((1 << d) - 1) = (3 >> 2 * 0) & ((1 << 2) - 1) = + # (3 >> 0) & (4 - 1) = 3 & 3 = 3. + # + # Let x = 7, d = 2 and p = 1. + # Then (x >> d * p) & ((1 << d) - 1) = (7 >> 2 * 1) & ((1 << 2) - 1) = + # (7 >> 2) & (4 - 1) = 1 & 3 = 1. + + # If k == 2, then k - 1 == 1. + # So, x & 1 means that the output is 1 only if the last digit of the + # binary representation of x is 1, else it's 0. + # For example, let x = 3. In binary, 3 == 11, so x & 1 == 3 & 1 == + # 11 & 01 = 1. Let x = 2, then 10 & 01 == 0, where 10 is the binary + # representation of 2. In general, r can be as big as k - 1. In this + # case, given that k = 2, then as big as 1. + r = (x >> d * p) & (k - 1) + assert r in list(range(k)) + return r + + n = len(a) + + # w // d of counting sort. + for p in range(w // d): + # This block of code is an adaptation of the counting sort algorithm + # for this radix sort algorithm. + + # The auxiliary counter list of size k. + c = [0] * k + + for i in range(n): + c[key(a[i], p)] += 1 + + for i in range(1, k): + c[i] += c[i - 1] + + # The result list for iteration p. + b = [None] * n + + for i in range(n - 1, -1, -1): + c[key(a[i], p)] -= 1 + b[c[key(a[i], p)]] = a[i] + + a = b + + return b + + +# pylint: disable=missing-function-docstring +def example1(): + a: list = [] + b = radix_sort(a) + print(b) + + a = [2] + b = radix_sort(a) + print(b) + + a = [10, 2] + b = radix_sort(a) + print(b) + + # a = [10, 2, -3] + # b = radix_sort(a) + # print(b) + + +# pylint: disable=missing-function-docstring +def example2(): + # Example taken from Sedgewick and Wayne's book, p. 706. + a = [ + "4PGC938", + "2IYE230", + "3CIO720", + "1ICK750", + "1OHV845", + "4JZY524", + "1ICK750", + "3CIO720", + "1OHV845", + "1OHV845", + "2RLA629", + "2RLA629", + "3ATW723", + ] + + lsd(a, 7) + from pprint import pprint # pylint: disable=import-outside-toplevel + + pprint(a) + + +if __name__ == "__main__": + example1() + example2() diff --git a/andz/ds/BST.py b/andz/ds/BST.py new file mode 100755 index 00000000..7d059e4c --- /dev/null +++ b/andz/ds/BST.py @@ -0,0 +1,893 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 01/07/2015 + +Updated: 28/09/2017 + +# Description + +A binary search trees (BST), sometimes called ordered or sorted binary trees, +are a particular type of containers: data structures that store "items" (such as +numbers, names, etc.) in memory. They allow fast lookup, addition and removal of +items (if balanced), and can be used to implement either dynamic sets of items, +or lookup tables that allow finding an item by its key (e.g., finding the phone +number of a person by name). + +Binary search trees keep their keys in sorted order, so that lookup and other +operations can use the principle of "binary search": when looking for a key in a +tree (or a place to insert a new key), they traverse the tree from the root to a +leaf, making comparisons to keys stored in the nodes of the tree and deciding, +based on the comparison, to continue searching in the left or right subtrees. On +average, this means that each comparison allows the operations to skip about +half of the tree, so that each lookup, insertion or deletion takes time +proportional to the logarithm of the number of items stored in the tree. + +This is much better than the linear time required to find items by key in an +(unsorted) array, but slower than the corresponding operations on hash tables. + +# TODO + +- Add functions "intersection" and "union". +- Implement a recursive version of insert (OPTIONAL). + +# References + +- https://en.wikipedia.org/wiki/Binary_search_tree +- Introduction to Algorithms (3rd edition), chapter 12, by CLRS +- http://algs4.cs.princeton.edu/32bst/ +- http://www.cs.princeton.edu/courses/archive/spr04/cos226/lectures/bst.4up.pdf +- http://algs4.cs.princeton.edu/32bst/BST.java.html +- http://ocw.mit.edu/courses/electrical-engineering-and-computer-science/ + 6-006-introduction-to-algorithms-fall-2011/readings/binary-search-trees/bst.py +""" + +__all__ = ["BST", "is_bst"] + + +class _BSTNode: + """A class to represent a node for the BST class.""" + + def __init__(self, key, parent=None, left=None, right=None): + if key is None: + raise ValueError("key cannot be None") + self.key = key + self.parent = parent + self.left = left + self.right = right + + @property + def sibling(self) -> "_BSTNode": + """Return the sibling node of this node, which can of course be None.""" + if self.parent is not None: + if self.is_left_child(): + return self.parent.right + else: + return self.parent.left + + @property + def grandparent(self) -> "_BSTNode": + """Return the parent of the parent of this node.""" + if self.parent is not None: + return self.parent.parent + + @property + def uncle(self) -> "_BSTNode": + """Return the uncle node of this node. + + The uncle is the sibling of the parent of this node, if it exists. None + is returned if it doesn't exist, or the parent or grandparent of this + node is None.""" + + # Implies that also parent is not None. + if self.grandparent is not None: + if self.parent == self.grandparent.left: + return self.grandparent.right + else: # self.parent == self.grandparent.right + return self.grandparent.left + + def is_left_child(self) -> bool: + """ + Check if self is the left child of its parent. + + If self has no parent, an ``AttributeError`` is raised. + """ + if self.parent is not None: + if self.parent.left is not None: # TODO: do we need this check? + return self.parent.left == self + else: + raise AttributeError("self does not have a parent") + + def is_right_child(self) -> bool: + """ + Check if self is the right child of its parent. + + If self has no parent, an ``AttributeError`` is raised. + """ + if self.parent is not None: + if self.parent.right is not None: # TODO: do we need this check? + return self.parent.right == self + else: + raise AttributeError("self does not have a parent") + + def has_right_child(self) -> bool: + """ + Return true if self has a right child, else false. + """ + return self.right is not None + + def has_left_child(self) -> bool: + """ + Return true if self has a left child, else false. + """ + return self.left is not None + + def has_parent(self) -> bool: + """ + Return true if self has a parent, else false. + """ + return self.parent is not None + + def has_children(self) -> bool: + """Return true if self has at least one child, false otherwise.""" + return self.left is not None or self.right is not None + + def has_one_child(self) -> bool: + """Return true only if self has exactly one child, false otherwise.""" + return (self.left is not None and self.right is None) or ( + self.left is None and self.right is not None + ) + + def has_two_children(self) -> bool: + """Return true if self has exactly two children, false otherwise.""" + return self.left is not None and self.right is not None + + def count(self) -> int: + """Count the numbers of nodes under self (including self).""" + if not self.has_children(): + return 1 + else: + c = 0 + return self._count(self, c) + + def _count(self, u: "_BSTNode", c: int) -> int: + if u is None: + return c + else: + c += 1 + c = self._count(u.left, c) + c = self._count(u.right, c) + return c + + def __str__(self): + return str(self.key) + + def __repr__(self): + return self.__str__() + + +class BST: + """BST is a class that represents a binary-search tree. + + This implementation does allow duplicate elements. + + It's the responsibility of the client of this class to make sure that keys + provided to the methods of this class are comparable among them. + + In the time complexity analysis under the methods of this class, h in O(h) + means the maximum height the algorithm is going to reach. m in O(m) is the + height of the subtree rooted at the node passed as parameter.""" + + def __init__(self): + self._n = 0 + self._root = None + assert is_bst(self) + + @property + def size(self) -> int: + """Returns the total number of nodes. + + Time complexity: O(1).""" + assert is_bst(self) + if self._root is not None: + assert self._root.count() == self._n + else: + assert self._n == 0 + return self._n + + def is_empty(self) -> bool: + """Returns true if this tree has 0 nodes. + + Time complexity: O(1).""" + assert is_bst(self) + return self.size == 0 + + def clear(self) -> None: + """Removes all nodes from this tree. + + Time complexity: O(1).""" + assert is_bst(self) + self._root = None + self._n = 0 + assert is_bst(self) + + def _is_root(self, u: _BSTNode) -> bool: + """Checks if u is the same object as self.root. + + Time complexity: O(1).""" + assert is_bst(self) + if u == self._root: + if u is not None: + assert u.parent is None + return u == self._root + + def insert(self, key: object) -> None: + """Inserts key into this BST. + + Time complexity: O(h).""" + assert is_bst(self) + + if key is None: + raise ValueError("key cannot be None") + + key_node = _BSTNode(key) + + if self._root is None: + assert self._n == 0 + self._root = key_node + else: + c = self._root # c is the current node. + p = self._root.parent # Parent of c. + + while c is not None: + p = c + if key_node.key < c.key: + c = c.left + else: + c = c.right + + if key_node.key < p.key: + p.left = key_node + else: + p.right = key_node + + key_node.parent = p + + self._n += 1 + + assert is_bst(self) + + def contains(self, key: object) -> bool: + """Returns true if key is in this BST, false otherwise. + + Time complexity: O(h).""" + assert is_bst(self) + if key is None: + raise ValueError("key cannot be None") + key_node = self._search_key_iteratively(key, self._root) + assert self._search_key_recursively(key, self._root) == key_node + assert is_bst(self) + return key_node is not None + + @staticmethod + def _search_key_iteratively(key: object, u: _BSTNode) -> _BSTNode: + """Returns the _BSTNode object c such that c.key == key, or None if no + such object exists. + + Time complexity: O(m).""" + c = u # Current node. + while c is not None: + if key == c.key: + return c + elif key < c.key: + c = c.left + else: + c = c.right + + def _search_key_recursively(self, key: object, u: _BSTNode) -> _BSTNode: + """Returns the _BSTNode object c such that c.key == key, or None if no + such object exists. + + Time complexity: O(m).""" + if u is None or key == u.key: + return u + elif key < u.key: + return self._search_key_recursively(key, u.left) + else: + return self._search_key_recursively(key, u.right) + + def rank(self, key: object) -> int: + """Returns the number of keys strictly less than key. + + Time complexity: O(h).""" + assert is_bst(self) + if not self.contains(key): + raise LookupError("key was not found") + return self._rank(self._root, key, 0) + + def _rank(self, u: _BSTNode, key, r: int) -> int: + if u is None: + return r + if u.key < key: + r += 1 + r = self._rank(u.left, key, r) + r = self._rank(u.right, key, r) + return r + + def height(self) -> int: + """Returns the maximum height of this BST. + + Since this is not a balanced BST, the maximum height may vary during the + lifetime of this BST. + + Time complexity: O(h).""" + assert is_bst(self) + if self._root is None: + return 0 + else: + return self._height(self._root) + + def _height(self, u: _BSTNode) -> int: + if u is None: + return 0 + return 1 + max(self._height(u.left), self._height(u.right)) + + def minimum(self) -> object: + """Returns the minimum key in this BST, or None if this BST is empty. + + Time complexity: O(h).""" + assert is_bst(self) + if self._root is not None: + m = BST._minimum(self._root) + assert m == BST._minimum_recursively(self._root) + assert is_bst(self) + return m.key if m is not None else None + + @staticmethod + def _minimum(u: _BSTNode) -> _BSTNode: + """Returns the node with the minimum key rooted at u.""" + assert u is not None + while u.has_left_child(): + u = u.left + return u + + @staticmethod + def _minimum_recursively(u: _BSTNode) -> _BSTNode: + """Recursive version of the BST._minimum function.""" + assert u is not None + if u.has_left_child(): + u = BST._minimum_recursively(u.left) + return u + + def maximum(self) -> object: + """Returns the maximum key in this BST, or None if this BST is empty. + + Time complexity: O(h).""" + assert is_bst(self) + if self._root is not None: + m = BST._maximum(self._root) + assert m == BST._maximum_recursively(self._root) + assert is_bst(self) + return m.key if m is not None else None + + @staticmethod + def _maximum(u: _BSTNode) -> _BSTNode: + """Returns the node with the maximum key rooted at u.""" + assert u is not None + while u.has_right_child(): + u = u.right + return u + + @staticmethod + def _maximum_recursively(u: _BSTNode) -> _BSTNode: + """Recursive version of the BST._maximum function.""" + assert u is not None + if u.has_right_child(): + u = BST._maximum_recursively(u.right) + return u + + def successor(self, key: object) -> object: + """Finds the successor of key, i.e. the smallest element greater than + key, or None if key does not have a successor. + + If key has a right subtree, then the successor of key is the minimum of + that right subtree. + + Otherwise it is the first ancestor of key, lets call it A, such that key + falls in the left subtree of A. + + Time complexity: O(h).""" + assert is_bst(self) + if key is None: + raise ValueError("key cannot be None") + + key_node = self._search_key_iteratively(key, self._root) + if key_node is None: + raise LookupError("key not in this BST") + + s = BST._successor(key_node) + + assert is_bst(self) + + return s.key if s is not None else None + + @staticmethod + def _successor(u: _BSTNode) -> _BSTNode: + """Returns the _BSTNode representing the successor of u.""" + assert u is not None + + if u.has_right_child(): + return BST._minimum(u.right) + + p = u.parent + while p is not None and p.right == u: + u = p + p = u.parent + + return p + + def predecessor(self, key: object) -> object: + """Finds the predecessor of the node key, i.e. the greatest element + smaller than key, or None if key does not have a predecessor. + + Time complexity: O(h).""" + assert is_bst(self) + + if key is None: + raise ValueError("key cannot be None") + + key_node = self._search_key_iteratively(key, self._root) + if key_node is None: + raise LookupError("key not in this BST") + + p = BST._predecessor(key_node) + + assert is_bst(self) + + return p.key if p is not None else None + + @staticmethod + def _predecessor(u: _BSTNode) -> _BSTNode: + """Returns the _BSTNode representing the predecessor of u.""" + assert u is not None + + if u.has_left_child(): + return BST._maximum(u.left) + + p = u.parent + while p is not None and u == p.left: + u = p + p = u.parent + + return p + + def remove_max(self) -> None: + """Removes the greatest element from self. + + Time complexity: O(h).""" + assert is_bst(self) + + if self.is_empty(): + return + + u = self._root + + # Note that the maximum element is all the way to the right, and it + # cannot have a right child, but it can still have a left subtree. + m = BST._maximum(u) + + if m.left is not None: # m has a left subtree. + if self._is_root(m): # m is the root. + self._root = m.left + m.left.parent = None # self.root.parent = None + else: # m is NOT the root. + m.left.parent = m.parent + m.parent.right = m.left + else: # m has NO children + if self._is_root(m): + self._root = None + else: + m.parent.right = None + + self._n -= 1 + assert is_bst(self) + + def remove_min(self) -> None: + """Removes the smallest element from self. + + Time complexity: O(h).""" + assert is_bst(self) + + if self.is_empty(): + return + + u = self._root + m = BST._minimum(u) + + if m.right is not None: + if self._is_root(m): + self._root = m.right + m.right.parent = None + else: + m.right.parent = m.parent + m.parent.left = m.right + else: # m has not right subtree. + if self._is_root(m): + self._root = None + else: # m is an internal node with no right subtree. + m.parent.left = None + + self._n -= 1 + assert is_bst(self) + + def delete(self, key: object) -> None: + """Deletes key from self, if it exists. + + There are 3 cases of deletion: + + 1. key has no children, + 2. key has one subtree (or child), and + 3. key has the left and right subtrees (or children). + + Time complexity: O(h).""" + assert is_bst(self) + + if key is None: + raise ValueError("key cannot be None") + + key_node = self._search_key_iteratively(key, self._root) + if key_node is None: + raise LookupError("key not in this BST") + + self._n -= 1 + self._delete_aux(key_node) + assert is_bst(self) + + def _delete_aux(self, u: _BSTNode) -> _BSTNode: + """When deleting a node u from a BST, we have basically to consider 3 + cases: + + 1. u has no children, then we simply remove it by modifying its parent + to replace u with None. If u.parent is None, then u must be the root, + and thus we simply set the root to None. + + 2. u has just one child, but we first need to decide which one (left or + right). Then we elevate this child to u's position in the tree by + modifying u's parent to replace u by u's child. But if u's parent is + None, that means u was the root, and the new root becomes u's child. + + 3. u has two children, then we search for u's successor s, (which must + be in the u's right subtree, and it's the smallest of that subtree) + which takes u's position in the tree. The rest of the u's subtree + becomes the s's right subtree, and the u's left subtree becomes the new + s's left subtree. This case is a little bit tricky, because it matters + whether s is u's right child. + + Suppose s is the right child of u, then we replace u by s, which might + or not have a right subtree, but no left subtree. + + Suppose s is not the right child of u, in this case, we replace s by its + own right child, and then we replace u by s. + + Note that self._delete_when_two_children does not exactly do that, but + instead it simply replaces the positions of u and s, as if s was u and u + was s. + + After that, _delete is called again on u, but note that u is now in the + previous s's position, and thus u has now no left subtree, but at most a + right subtree.""" + if u.has_two_children(): + self._delete_when_two_children(u) + else: # u has at most one child. + self._delete_when_at_most_one_child(u) + + u.right = u.left = u.parent = None + return u + + def _delete_when_two_children(self, u: _BSTNode) -> None: + """Called by _delete_aux when a node has two children.""" + assert u is not None + # Replace u with its successor s. + self._switch(u, self._successor(u)) + # u has at most a right child now. + self._delete_aux(u) + + def _delete_when_at_most_one_child(self, u: _BSTNode) -> None: + """Removes u from the tree, when u has at most one child. + + This means that u could have 0 or 1 child.""" + assert u is not None + child = u.right + if u.left: + child = u.left + if not u.has_parent(): # u is the root. + self._root = child + else: # u has a parent, so it is not the root. + if u.is_left_child(): + u.parent.left = child + else: + u.parent.right = child + # child is None iff u.right and u.left are None. + if child: + child.parent = u.parent + + def _switch(self, x: _BSTNode, y: _BSTNode) -> None: + """ "Switches the roles of x and y in the tree by moving references.""" + assert x is not None and y is not None + assert x != y + + if x.parent == y: + self._switch_parent_with_child(y, x) + elif y.parent == x: + self._switch_parent_with_child(x, y) + else: + self._switch_nodes_when_not_parent_child(x, y) + + def _switch_nodes_when_not_parent_child(self, x: _BSTNode, y: _BSTNode) -> None: + """x and y are nodes in the tree that are not related by a parent-child. + + Time complexity: O(1).""" + assert x.parent != y and y.parent != x + + if not x.has_parent(): + self._root = y + if y.is_left_child(): + y.parent.left = x + else: + y.parent.right = x + elif not y.has_parent(): + self._root = x + if x.is_left_child(): + x.parent.left = y + else: + x.parent.right = y + else: # Neither x nor y are the root. + if x.is_left_child(): + if y.is_left_child(): + y.parent.left, x.parent.left = x, y + else: + y.parent.right, x.parent.left = x, y + else: + if y.is_left_child(): + y.parent.left, x.parent.right = x, y + else: + y.parent.right, x.parent.right = x, y + + y.parent, x.parent = x.parent, y.parent + x.left, y.left = y.left, x.left + x.right, y.right = y.right, x.right + + if x.left: + x.left.parent = x + if x.right: + x.right.parent = x + if y.left: + y.left.parent = y + if y.right: + y.right.parent = y + + def _switch_parent_with_child(self, p: _BSTNode, c: _BSTNode) -> None: + """Switches the roles of p and c, where p (parent) is the direct parent + of c (child).""" + assert c.parent == p + + if c.is_left_child(): + p.left = c.left + if c.left: + c.left.parent = p + + c.left = p + + c.right, p.right = p.right, c.right + if c.right: + c.right.parent = c + if p.right: + p.right.parent = p + else: + p.right = c.right + if c.right: + c.right.parent = p + + c.right = p + + c.left, p.left = p.left, c.left + if c.left: + c.left.parent = c + if p.left: + p.left.parent = p + + if p.parent: + if p.is_left_child(): + p.parent.left = c + else: + p.parent.right = c + else: # p is the root. + self._root = c + + c.parent = p.parent + p.parent = c + + def in_order_traversal(self) -> None: + """Prints the elements of the tree in increasing order. + + Time complexity: O(h).""" + assert is_bst(self) + self._in_order_traversal(self._root) + print("\n") + + def _in_order_traversal(self, u: _BSTNode, e=", ") -> None: + if u is not None: + self._in_order_traversal(u.left) + print(u, end=e) + self._in_order_traversal(u.right) + + def pre_order_traversal(self) -> None: + """Prints the keys of this tree in pre-order. + + The pre-order consists of recursively printing first a node u, then its + left child node and then its right child node. + + Time complexity: O(h).""" + assert is_bst(self) + self._pre_order_traversal(self._root) + print("\n") + + def _pre_order_traversal(self, u: _BSTNode, e=", ") -> None: + if u is not None: + print(u, end=e) + self._pre_order_traversal(u.left) + self._pre_order_traversal(u.right) + + def post_order_traversal(self) -> None: + """Prints the keys of this tree in post-order. It does the opposite of + pre_order_traversal. + + Time complexity: O(h).""" + assert is_bst(self) + self._post_order_traversal(self._root) + print("\n") + + def _post_order_traversal(self, u: _BSTNode, e=", ") -> None: + if u is not None: + self._post_order_traversal(u.left) + self._post_order_traversal(u.right) + print(u, end=e) + + def reverse_in_order_traversal(self) -> None: + """Prints the keys of this tree in decreasing order. It does the + opposite of self.in_order_traversal. + + Time complexity: O(h).""" + assert is_bst(self) + self._reverse_in_order_traversal(self._root) + print("\n") + + def _reverse_in_order_traversal(self, u: _BSTNode, e=", ") -> None: + if u is not None: + self._reverse_in_order_traversal(u.right) + print(u, end=e) + self._reverse_in_order_traversal(u.left) + + def __str__(self): + if self._root is None: + return "Nothing to print: this BST is empty." + return "\n".join(build_pretty_bst(self._root)) + "\n" + + def __repr__(self): + return self.__str__() + + +def build_pretty_bst(node: _BSTNode, only_list: bool = True): + """Pretty-prints this BST object.""" + if not isinstance(_BSTNode): + raise TypeError("node must be an instance of _BSTNode") + if not isinstance(only_list, bool): + raise TypeError("only_list must be a bool") + + if node is None: + if only_list: + return [] + else: + return [], 0, 0 + + fill = "_" + + left_lines, left_pos, left_width = build_pretty_bst(node.left) + right_lines, right_pos, right_width = build_pretty_bst(node.right) + middle = max(right_pos + left_width - left_pos + 1, len(node.key), 2) + pos = left_pos + middle // 2 + width = left_pos + middle + right_width - right_pos + + while len(left_lines) < len(right_lines): + left_lines.append(" " * left_width) + + while len(right_lines) < len(left_lines): + right_lines.append(" " * right_width) + + if ( + (middle - len(node.key)) % 2 == 1 + and node.parent is not None + and node is node.parent.left + and len(node.key) < middle + ): + node.key += fill + + node.key = node.key.center(middle, fill) + + if node.key[0] == fill: + node.key = " " + node.key[1:] + + if node.key[-1] == fill: + node.key = node.key[:-1] + " " + + lines = [ + " " * left_pos + node.key + " " * (right_width - right_pos), + " " * left_pos + + "/" + + " " * (middle - 2) + + "\\" + + " " * (right_width - right_pos), + ] + [ + left_line + " " * (width - left_width - right_width) + right_line + for left_line, right_line in zip(left_lines, right_lines) + ] + + if only_list: + return lines + else: + return lines, pos, width + + +def has_bst_property(n: _BSTNode) -> bool: + """Check if the tree under n has the binary-search tree property, i.e., for + each node u, all nodes in its left sub-tree are smaller than u, and all + nodes in its right sub-tree are greater than u. + + It also checks that parent pointers are correctly set up.""" + if n is not None: + if n.left and n.key < n.left.key: + return False + if n.right and n.key > n.right.key: + return False + + # Asserting n.left and n.right have n as parent. + if n.left: + if n.left.parent != n: + return False + if n.right: + if n.right.parent != n: + return False + + return has_bst_property(n.left) and has_bst_property(n.right) + + return True + + +def all_bst_nodes(n: _BSTNode) -> bool: + """Returns true if all nodes under n (including n) are instances of _BSTNode, + false otherwise.""" + if n is not None: + # If either n or its parent are not instances of _BSTNode. + if not isinstance(n, _BSTNode) or ( + n.parent is not None and not isinstance(n.parent, _BSTNode) + ): + return False + return all_bst_nodes(n.left) and all_bst_nodes(n.right) + return True + + +def is_bst(t: BST) -> bool: + """Returns true if t is a valid BST object, false otherwise. + + Invariant: for each node n in t, if n.left exists, then n.left <= n, and if + n.right exists, then n.right >= n.""" + if not isinstance(t, BST): + return False + if t._root and t._root.parent is not None: + return False + return all_bst_nodes(t._root) and has_bst_property(t._root) diff --git a/andz/ds/BinaryHeap.py b/andz/ds/BinaryHeap.py new file mode 100755 index 00000000..cdee83b9 --- /dev/null +++ b/andz/ds/BinaryHeap.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 01/07/2015 + +Updated: 06/04/2018 + +# Description + +Contains the abstract class BinaryHeap. + +# References + +- Slides by prof. A. Carzaniga +- Chapter 13 of Introduction to Algorithms (3rd ed.) +- http://www.math.clemson.edu/~warner/M865/HeapDelete.html +- https://docs.python.org/3/library/exceptions.html#NotImplementedError +- http://effbot.org/pyfaq/how-do-i-check-if-an-object-is-an-instance-of- + a-given-class-or-of-a-subclass-of-it.htm +- https://en.wikipedia.org/wiki/Heap_(data_structure) +- https://arxiv.org/pdf/1012.0956.pdf +- http://pymotw.com/2/heapq/ +- http://stackoverflow.com/a/29197855/3924118 +""" + +import io +import math +from abc import ABC, abstractmethod + +__all__ = ["BinaryHeap", "build_pretty_binary_heap"] + + +class BinaryHeap(ABC): + """Abstract class to represent binary heaps. + + This binary heap allows duplicates. + + It's the responsibility of the client to ensure that inserted elements are + comparable among them. + + Their order also defines their priority. + + Public interface: + + - size + - is_empty + - clear + - add + - contains + - delete + - merge + + MinHeap, MaxHeap and MinMaxHeap all derive from this class.""" + + def __init__(self, ls=None): + self.heap = [] if not isinstance(ls, list) else ls + self._build_heap() + + @property + def size(self) -> int: + """Returns the number of elements in this heap. + + Time complexity: O(1).""" + return len(self.heap) + + def is_empty(self) -> bool: + """Returns true if this heap is empty, false otherwise. + + Time complexity: O(1).""" + return self.size == 0 + + def clear(self) -> None: + """Removes all elements from this heap. + + Time complexity: O(1).""" + self.heap.clear() + + def add(self, x: object) -> None: + """Adds object x to this heap. + + This algorithm proceeds by placing x at an available leaf of this heap, + then bubbles up from there, in order to maintain the heap property. + + Time complexity: O(log n).""" + if x is None: + raise ValueError("x cannot be None") + self.heap.append(x) + if self.size > 1: + self._push_up(self.size - 1) + + def contains(self, x: object) -> bool: + """Returns true if x is in this heap, false otherwise. + + Time complexity: O(n).""" + if x is None: + raise ValueError("x cannot be None") + return self._index(x) != -1 + + def delete(self, x: object) -> None: + """Removes the first found x from this heap. + + If x is not in this heap, LookupError is raised. + + Time complexity: O(n).""" + if x is None: + raise ValueError("x cannot be None") + + i = self._index(x) + if i == -1: + raise LookupError("x not found") + + # self has at least one element. + if i == self.size - 1: + self.heap.pop() + else: + self._swap(i, self.size - 1) + self.heap.pop() + self._push_down(i) + self._push_up(i) + + def merge(self, o: "Heap") -> None: + """Merges this heap with the o heap. + + Time complexity: O(n + m).""" + self.heap += o.heap + self._build_heap() + + @abstractmethod + def _push_down(self, i: int) -> None: + """Classical "heapify" operation for heaps.""" + + @abstractmethod + def _push_up(self, i: int) -> None: + """Classical reverse-heapify operation for heaps.""" + + def _build_heap(self) -> list: + """Builds the heap data structure using Robert Floyd's heap construction + algorithm. + + Floyd's algorithm is optimal as long as complexity is expressed in terms + of sets of functions described via the asymptotic symbols O, Θ and Ω. + Indeed, its linear complexity Θ(n), both in the worst and best case, + cannot be improved as each object must be examined at least once. + + Floyd's algorithm was invented in 1964 as an improvement of the + construction phase of the classical heap-sort algorithm introduced + earlier that year by Williams J.W.J. + + Time complexity: Θ(n).""" + if self.heap: + for index in range(len(self.heap) // 2, -1, -1): + self._push_down(index) + + def _index(self, x: object) -> int: + """Returns the index of x in this heap if x is in this heap, otherwise + it returns -1. + + Time complexity: O(n).""" + for i, node in enumerate(self.heap): + if node == x: + return i + return -1 + + def _swap(self, i: int, j: int) -> None: + """Swaps elements at indexes i and j. + + Time complexity: O(1).""" + assert self._is_good_index(i) and self._is_good_index(j) + self.heap[i], self.heap[j] = self.heap[j], self.heap[i] + + def _left_index(self, i: int) -> int: + """Returns the left child's index of the node at index i, if it exists, + otherwise this function returns -1. + + Time complexity: O(1).""" + assert self._is_good_index(i) + left = i * 2 + 1 + return left if self._is_good_index(left) else -1 + + def _right_index(self, i: int) -> int: + """Returns the right child's index of the node at index i, if it exists, + otherwise this function returns -1. + + Time complexity: O(1).""" + assert self._is_good_index(i) + right = i * 2 + 2 + return right if self._is_good_index(right) else -1 + + def _parent_index(self, i: int) -> int: + """Returns the parent's index of the node at index i. + + If i = 0, then -1 is returned, because the root has no parent. + + Time complexity: O(1).""" + assert self._is_good_index(i) + return -1 if i == 0 else (i - 1) // 2 + + def _is_good_index(self, i: int) -> bool: + """Returns true if i is in the bounds of self.heap, false otherwise. + + Time complexity: O(1).""" + return not (i < 0 or i >= self.size) + + def __str__(self): + return str(self.heap) + + def __repr__(self): + return build_pretty_binary_heap(self.heap) + + +def build_pretty_binary_heap(heap: list, total_width=36, fill=" ") -> str: + """Returns a string (which can be printed) representing heap as a tree. + + To increase/decrease the horizontal space between nodes, just + increase/decrease the float number h_space. + + To increase/decrease the vertical space between nodes, just + increase/decrease the integer number v_space. + Note: v_space must be an integer. + + To change the length of the line under the heap, you can simply change the + line_length variable.""" + if not isinstance(heap, list): + raise TypeError("heap must be an list object") + if len(heap) == 0: + return "Nothing to print: heap is empty." + + output = io.StringIO() + last_row = -1 + h_space = 3.0 + v_space = 2 + + for i, heap_node in enumerate(heap): + if i != 0: + row = int(math.floor(math.log(i + 1, 2))) + else: + row = 0 + + if row != last_row: + output.write("\n" * v_space) + + columns = 2**row + column_width = int(math.floor((total_width * h_space) / columns)) + output.write(str(heap_node).center(column_width, fill)) + last_row = row + + s = output.getvalue() + "\n" + line_length = total_width + 15 + s += "-" * line_length + "\n" + return s diff --git a/andz/ds/DisjointSets.py b/andz/ds/DisjointSets.py new file mode 100644 index 00000000..021bf49e --- /dev/null +++ b/andz/ds/DisjointSets.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 08/03/2017 + +Updated: 06/04/2018 + +# Description + +Module which contains the abstract class from which DisjointSetsForest derives. + +The reason to have this abstract class is that a disjoint-sets data structure +can possibly be implemented in different ways. +""" + +from abc import ABC, abstractmethod + +__all__ = ["DisjointSets"] + + +class DisjointSets(ABC): + """ + An abstract class from which DisjointSetsForest derives. + + A DisjointSets data structure is sometimes also called DisjointSet, + UnionFind or MergeSet.""" + + @abstractmethod + def make_set(self, x) -> None: + """ + Put ``x`` in a set that contains only ``x``. + """ + + @abstractmethod + def find(self, x): + """ + Find the representative (or root) element of the set to which ``x``belongs. + """ + + @abstractmethod + def union(self, x, y): + """ + Merge the set of ``x`` with set of ``y``. + """ diff --git a/andz/ds/DisjointSetsForest.py b/andz/ds/DisjointSetsForest.py new file mode 100644 index 00000000..85f728bd --- /dev/null +++ b/andz/ds/DisjointSetsForest.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 21/02/2016 + +Updated: 20/08/2017 + +# Description + +A disjoint-set (forests) or union-find data structure is a data structure which +keeps track of a set of elements partitioned into disjoint sets. + +Each of these disjoint sets has one "representative" (or "root") element, which +we can think as the element representing the whole set. + +If the disjoint sets are implemented as trees, the representative of each of +these disjoint sets are the root of the corresponding trees. + +The usual operations supported by this data structure are: + + 1. make-set(x): creates a single-element set containing x, and x is the + representative of that set. + + 2. find(x): returns the representative of the set where the element x is. + + 3. union(x, y): unions the sets where x and y are (if they do not belong + already to the same set). + +DisjointSetsForest uses two heuristics that improve the performance with respect +to a naive implementation: + + 1. Union by rank: attach the smaller tree to the root of the larger tree + + 2. Path compression: is a way of flattening the structure of the tree whenever + find is used on it. + +These two techniques complement each other: applied together, the amortized time +per operation is only O(α(n)). + +# TODO + +- Pretty-print(x), for some element x in the disjoint-set data structure. +- Implement the version explained [here](http://algs4.cs.princeton.edu/15uf/) +- Add complexity analysis for print_set +- Deletion operation (OPTIONAL) + +# References + +- Introduction to algorithms, 3rd, by C.L.R.S., chapter 21.3 +- https://en.wikipedia.org/wiki/Disjoint-set_data_structure +- http://orionsword.no-ip.org/blog/wordpress/?p=246 +- http://stackoverflow.com/a/22945492/3924118 +- http://stackoverflow.com/q/23055236/3924118 +- https://www.cs.usfca.edu/~galles/JavascriptVisual/DisjointSets.html +""" + +from andz.ds.DisjointSets import DisjointSets + +__all__ = ["DisjointSetsForest"] + + +class _DSFNode: + """_DSFNode is the node used internally by DisjointSetsForest to represent + nodes in the disjoint trees (or sets).""" + + def __init__(self, x, rank=0): + # This attribute can contain any hashable value. + self.value = x + + # The rank of node x only changes in one specific union(x, y) case: when + # x is the representative of its set and the representative of the set + # where y resides has the same rank as x. + # + # In the DisjointSetsForest implementation below, if a situation as just + # described occurs, then the x.rank is increased by 1. + self.rank = rank + + # Reference to the representative of the set where this node resides. + # Since DisjointSetsForest actually implements a tree, self.parent is + # also the root of that tree. + self.parent = self + + # Reference used to help printing all nodes belonging to the set to + # which this node belongs in O(m) time, where m is the size of the + # mentioned set. + self.next = self + + def is_root(self) -> bool: + """A _DSFNode x is a root or representative of a set whenever its parent + pointer points to himself. Of course this is only true if x is already + in a DisjointSetsForest object.""" + return self.parent == self + + def __str__(self): + return str(self.value) + + def __repr__(self): + if self.parent == self: + return f"(value: {self.value}, rank: {self.rank}, parent: self)" + return f"(value: {self.value}, rank: {self.rank}, parent: {self.parent})" + + +class DisjointSetsForest(DisjointSets): + """Disjoint-set forests is a collection of disjoint sets. + + Two sets A and B are disjoint if they have no element in common, or, in + other words, their intersection is the empty set. + + It's called forest because it's implemented as a collection of trees, + which is a forest. + + A disjoint-set data structure can be implemented differently. + + This data structure does not allow duplicates.""" + + def __init__(self): + # Keeps tracks of the _DSNodes in this disjoint-set forests. + self._sets = {} + self._n = 0 + + def make_set(self, x: object) -> None: + """Creates a set object for x. + + If x is already in self, then ValueError is raised.""" + assert 0 <= self.sets <= self.size + if self.contains(x): + raise LookupError("x is already in self") + self._sets[x] = _DSFNode(x) + self._n += 1 + assert 0 <= self.sets <= self.size + + @property + def size(self) -> int: + """Returns the number of elements in this DisjointSetsForest.""" + return len(self._sets) + + @property + def sets(self) -> int: + """Returns the number of disjoint sets in self.""" + return self._n + + def contains(self, x: object) -> bool: + """Returns true if x is in self, false otherwise.""" + return x in self._sets + + def _find(self, x: _DSFNode) -> _DSFNode: + """Finds and returns the representative (or root) of x. + + It does that by following parent nodes until it reaches the root of the + tree (set) to which x belongs. + + It also uses a technique called "path compression", which is a way of + flattening the structure of the tree. The idea is that each node visited + on the way to a root node may as well be attached directly to the root + node: they all share the same representative. To effect this, as + self.find recursively traverses up the tree, it changes each node's + parent reference to point to the root that it found. The resulting tree + is much flatter, speeding up future operations not only on these + elements but on those referencing them, directly or indirectly. + + This algorithm does not change any ranks of the Set objects. + + Time complexity: O(α(n)), where α(n) is the inverse of the function + n = f(x) = A(x, x), and A is the extremely fast-growing Ackermann + function. Since α(n) is the inverse of this function, α(n) is less than + 5 for all remotely practical values of n. Thus, the amortized running + time per operation is effectively a small constant.""" + assert x is not None + if x.parent != x: + x.parent = self._find(x.parent) + return x.parent + + @staticmethod + def _find_iteratively(x: _DSFNode) -> _DSFNode: + """This version is just an iterative alternative to the find method.""" + assert x is not None + + y = x + + # Find the representative of the set where x resides. + while y != y.parent: + y = y.parent + + # Now y is the representative of x, but we also want to do a path + # compression, i.e. connect all nodes in the path from x to y directly + # to y. + while x != x.parent: + p = x.parent + x.parent = y + x = p + + return y + + def find(self, x: object) -> object: + """ + Find and return the representative (or root) of ``x``. + + Raise a ``LookupError`` if ``x`` does not belong to this DisjointSetsForest. + + Time complexity: O(α(n)). + """ + if not self.contains(x): + raise LookupError("x is not in self") + x_root = self._find(self._sets[x]).value + assert x_root == DisjointSetsForest._find_iteratively(self._sets[x]).value + return x_root + + def union(self, x: object, y: object) -> object: + """ + Union by rank 2 sets into one by attaching the root of one to the + root of the other. + + Return the root object representing the representative of the set + resulted from the union of the sets containing x and y. It returns None + if x and y are already in the same set. + + "Union by rank" consists of attaching the smaller tree to the root of + the larger tree. Since it is the depth of the tree that affects the + running time, the tree with smaller depth gets added under the root of + the deeper tree, which only increases the depth if the depths were + equal. In the context of this algorithm, the term "rank" is used instead + of depth, since it stops being equal to the depth if path compression is + also used. The rank is an upper bound on the height of the node. + + One-element trees are defined to have a rank of zero, and whenever two + trees of the same rank r are united, the rank of the result is r + 1. + + Time complexity: O(α(n)), where α(n) is the inverse of the function + n = f(x) = A(x, x), and A is the extremely fast-growing Ackermann + function. Since α(n) is the inverse of this function, α(n) is less than + 5 for all remotely practical values of n. Thus, the amortized running + time per operation is effectively a small constant. + """ + assert 0 <= self.sets <= self.size + + if not self.contains(x): + raise LookupError("x is not in self") + if not self.contains(y): + raise LookupError("y is not in self") + + x_node = self._sets[x] + y_node = self._sets[y] + + x_root = self._find(x_node) + y_root = self._find(y_node) + + # x and y are already joined. + if x_root == y_root: + return + + # Exchanging the next pointers of x_node and y_node. + # + # This is needed in order to print the elements of a set in O(m) time, + # where m is the size of the same set, in self.print_set. + # + # Check here: http://stackoverflow.com/a/22945492/3924118. + x_node.next, y_node.next = y_node.next, x_node.next + + self._n -= 1 + assert 0 <= self.sets <= self.size + + # x and y are not in the same set, therefore we merge them. + if x_root.rank < y_root.rank: + x_root.parent = y_root + return y_root.value + + y_root.parent = x_root + if x_root.rank == y_root.rank: + x_root.rank += 1 + return x_root.value + + def print_set(self, x: object) -> None: + """ + Print the set to which ``x`` belongs. + + If ``x`` is not in self, ``LookupError`` is raised. + """ + if not self.contains(x): + raise LookupError("x is not in self") + + x_node = self._sets[x] + y = x_node + + print(f"{x_node} -> {{{x_node}", end="") + while y.next != x_node: + print(",", y.next, end="") + y = y.next + print("}") + + def __str__(self): + return str(self._sets) diff --git a/andz/ds/HashTable.py b/andz/ds/HashTable.py new file mode 100644 index 00000000..259821c6 --- /dev/null +++ b/andz/ds/HashTable.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 13/02/2017 + +Updated: 06/04/2018 + +# Description + +Since a hash table (or map) can be implemented in many ways, mostly because +these ways reflect the way collisions are handled, where the most famous +techniques are "separate chaining" and "open addressing", it is convenient to +have this abstract class from which all implementations should derive, and they +should all implement at least two methods: put and get. + +# References + +- https://stackoverflow.com/q/13646245/3924118 +""" + +from abc import ABC, abstractmethod + +__all__ = ["HashTable"] + + +class HashTable(ABC): + """Abstract class from which classes such as LinearProbingHashTable + derive.""" + + @abstractmethod + def put(self, key: object, value: object) -> None: + """ + Add the key: value pair to the hash table. + """ + + @abstractmethod + def get(self, key: object) -> object: + """ + Get the value associated with the key. + """ diff --git a/andz/ds/LinearProbingHashTable.py b/andz/ds/LinearProbingHashTable.py new file mode 100644 index 00000000..55def89f --- /dev/null +++ b/andz/ds/LinearProbingHashTable.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 01/06/2015 + +Updated: 02/04/2018 + +# Description + +## What's a hash map (or hash table)? + +A hash map is a data structure which is used to implement the so-called +associative array, which is an abstract data type composed of a collection of +(key, value) pairs, such that each possible key appears at most once in the +collection. + +## Hash function + +To map keys to values, a hash function is used when implementing a hash map. A +hash function is any function that can be used to map data of arbitrary size to +data of fixed size. + +A perfect hash function is a function that assigns each key a unique bucket in +the the data structure, but most hash table designs employ an imperfect hash +function, which might cause hash collisions, where the hash function generates +the same index (i.e. the same position or bucket) for more than one key. Such +collisions must be resolved or accommodated in some way. + +## Resolving collisions + +There are different ways to resolve collisions, where the most famous techniques +are "separate chaining" and "open addressing". + +# TODO + +- Add complexity analysis to operations + +- No difference between non-existence of a key in the table and existence of a +key with None as associated value: maybe we want to differentiate the two cases? + +- Resizing the hash table only whenever we reach a full table may not be the +best option in terms of performance... + +- Should a client of this class be able to specify its custom hash function? + +- Size could be implemented as a counter. + +- Improve is_hash_table function + +# References + +- http://interactivepython.org/runestone/static/pythonds/SortSearch/Hashing.html +- https://stackoverflow.com/q/279539/3924118 +- https://stackoverflow.com/q/9835762/3924118 +- https://stackoverflow.com/q/1541797/3924118 +- https://en.wikipedia.org/wiki/Associative_array +- https://en.wikipedia.org/wiki/Hash_table +- https://en.wikipedia.org/wiki/Hash_function +- https://en.wikipedia.org/wiki/Linear_probing +- https://en.wikipedia.org/wiki/Open_addressing +""" + +from collections.abc import Hashable + +from tabulate import tabulate + +from andz.ds.HashTable import HashTable + +__all__ = ["LinearProbingHashTable", "has_duplicates_ignore_nones", "is_hash_table"] + + +class LinearProbingHashTable(HashTable): + """Resizable hash table which uses linear probing, which is a specific + "open addressing" technique, to resolve collisions. + + The process of resizing consists in doubling the current capacity of the + hash table each time. + + The hash function uses both the Python's built-in hash function and the % + operator. + + You can access and put an item in the hash table by using the same + convenient notation that is used by the Python's standard dict class: + + h = LinearProbingHashTable() + h[12] = 3 + print(h[12])""" + + def __init__(self, capacity: int = 11): + if not isinstance(capacity, int): + raise TypeError("capacity must be an instance of int") + if capacity < 1: + raise ValueError("capacity must be greater or equal to 1") + self._n = capacity # self._n holds the size of the buffers. + self._keys = [None] * self._n + self._values = [None] * self._n + + @property + def size(self) -> int: + """Returns the number of pairs key-value in this map.""" + assert is_hash_table(self) + return sum(k is not None for k in self._keys) + + @property + def capacity(self) -> int: + """Returns the number of allocated cells in memory.""" + assert is_hash_table(self) + return len(self._keys) + + @staticmethod + def _hash_code(key, size: int) -> int: + """Returns a hash code (an int) between 0 and size (excluded). + + size must be the size of the buffer based on which this function should + return a hash value.""" + return hash(key) % size + + @staticmethod + def _rehash(old_hash: int, size: int) -> int: + """Returns a new hash value based on the previous one called old_hash. + + size must be the size of the buffer based on which we want to have a new + hash value from the old hash value.""" + return (old_hash + 1) % size + + def put(self, key: object, value: object) -> None: + """Inserts the pair (key: value) in this map. + + If key is None, a TypeError is raised, because keys cannot be None.""" + assert is_hash_table(self) + + if key is None: + raise TypeError("key cannot be None.") + if not isinstance(key, Hashable): + raise TypeError("key must be an instance of a hashable type") + + self._put(key, value, self._n) + + assert is_hash_table(self) + + def _put(self, key: object, value: object, size: int) -> None: + """Helper method of self.put.""" + hash_value = LinearProbingHashTable._hash_code(key, size) + + # No need to allocate new space. + if self._keys[hash_value] is None: + self._keys[hash_value] = key + self._values[hash_value] = value + + # If self already contains_key key, then its value is overridden. + elif self._keys[hash_value] == key: + self._values[hash_value] = value + + # Collision: there's already a (key: value) pair at the slot dedicated + # to this (key: value) pair, according to the self._hash_code function. + # We need to _rehash, i.e. find another slot for this (key: value) pair. + else: + next_slot = LinearProbingHashTable._rehash(hash_value, size) + rehashed = False + + while self._keys[next_slot] is not None and self._keys[next_slot] != key: + + next_slot = LinearProbingHashTable._rehash(next_slot, size) + + # Allocate new buffer of length len(self.keys) * 2 + 1. + if next_slot == hash_value: + rehashed = True + + keys = self._keys + values = self._values + + new_size = len(self._keys) * 2 + 1 + self._keys = [None] * new_size + self._values = [None] * new_size + + # Rehashing and putting all elements in the new bigger + # buffer. + # + # Note: the calls to self._put in the following loop will + # never reach these statements, because there will be slots + # available, and because the way hashing and rehashing is + # currently implemented. + for k in keys: + v = LinearProbingHashTable._get(k, keys, values, self._n) + self._put(k, v, new_size) + + # After resizing the buffers, we insert the original + # (key: value) pair. + self._put(key, value, new_size) + self._n = new_size + + # We exited the loop either because we have found a free slot or a + # slot containing our key, and not after having re-sized the table. + if not rehashed: + if self._keys[next_slot] is None: + self._keys[next_slot] = key + self._values[next_slot] = value + else: + assert self._keys[next_slot] == key + self._values[next_slot] = value + + def get(self, key: object) -> object: + """Returns the value associated with key. + + If key is None, a TypeError is raised, because keys cannot be None.""" + assert is_hash_table(self) + + if key is None: + raise TypeError("key cannot be None.") + if not isinstance(key, Hashable): + raise TypeError("key must be an instance of a hashable type") + + value = LinearProbingHashTable._get(key, self._keys, self._values, self._n) + + assert is_hash_table(self) + + return value + + @staticmethod + def _get(key: object, keys: list, values: list, size: int) -> object: + """Helper method of self.get.""" + hash_value = LinearProbingHashTable._hash_code(key, size) + + data = None + stop = False + found = False + position = hash_value + + while keys[position] is not None and not found and not stop: + + if keys[position] == key: + found = True + data = values[position] + else: + # Find a new possible position by rehashing. + position = LinearProbingHashTable._rehash(position, size) + + # We are at the initial slot, and thus nothing was found. + if position == hash_value: + stop = True + + return data + + def delete(self, key: object) -> object: + """Deletes the mapping between key and its associated value. + + If there's no mapping, nothing is done.""" + assert is_hash_table(self) + + if key is None: + raise TypeError("key cannot be None.") + if not isinstance(key, Hashable): + raise TypeError("key must be an instance of a hashable type") + + try: + i = self._keys.index(key) + v = self._values[i] + self._keys[i] = self._values[i] = None + return v + except ValueError: + pass + finally: + assert is_hash_table(self) + + def show(self) -> None: + """Prints this hash table in table-like format.""" + c = 0 + data = [] + for i, _ in enumerate(self._keys): + if self._keys[i] is not None: + c += 1 + data.append([c, self._keys[i], self._values[i]]) + print(tabulate(data, headers=["#", "Keys", "Values"], tablefmt="grid")) + + def __getitem__(self, key): + return self.get(key) + + def __setitem__(self, key, value): + self.put(key, value) + + def __str__(self): + return str([(k, v) for k, v in zip(self._keys, self._values) if k is not None]) + + def __repr__(self): + return self.__str__() + + +def has_duplicates_ignore_nones(ls: list) -> bool: + """Returns true if ls does contain duplicate elements, false otherwise. + + None items in ls are not considered.""" + ls = [item for item in ls if item is not None] + return len(ls) != len(set(ls)) + + +# pylint: disable=protected-access +def is_hash_table(t: HashTable) -> bool: + """Returns true if t is a valid HashTable, false otherwise.""" + if not isinstance(t, HashTable): + return False + if len(t._keys) != len(t._values) or len(t._keys) != t._n: + return False + return not has_duplicates_ignore_nones(t._keys) diff --git a/andz/ds/MaxHeap.py b/andz/ds/MaxHeap.py new file mode 100644 index 00000000..2b03bf92 --- /dev/null +++ b/andz/ds/MaxHeap.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 15/02/2016 + +Updated: 29/09/2017 + +# Description + +Implementation of a max-heap. +See doc-strings of the module MinHeap.py. + +# References + +- https://en.wikipedia.org/wiki/Binary_heap +- Slides by prof. A. Carzaniga +- Chapter 13 of Introduction to Algorithms (3rd ed.) by CLRS +- http://www.math.clemson.edu/~warner/M865/HeapDelete.html +""" + +from andz.ds.BinaryHeap import BinaryHeap + +__all__ = ["MaxHeap", "is_max_heap"] + + +class MaxHeap(BinaryHeap): + """Sub-class of BinaryHeap, and thus provides the same public interface, + but in addition provides two more operations: + + - find_max + - remove_max""" + + def __init__(self, ls=None): + BinaryHeap.__init__(self, ls) + + def find_max(self): + """Returns the greatest element in this MaxHeap. + + Time complexity: O(1).""" + return self.heap[0] if not self.is_empty() else None + + def remove_max(self): + """Removes and returns the greatest element in this MaxHeap. + + Time complexity: O(log(n)).""" + assert is_max_heap(self) + if not self.is_empty(): + self._swap(0, self.size - 1) + m = self.heap.pop() + if not self.is_empty(): + self._push_down(0) + assert is_max_heap(self) + return m + + def _push_down(self, i: int) -> None: + """Max-heapifies this MaxHeap starting from index i. + + This operation is also called "bubble-down" or "shift-down". + + Time complexity: O(log(n)).""" + m = i + l = self._left_index(i) + r = self._right_index(i) + + if l != -1 and self.heap[l] > self.heap[m]: + m = l + if r != -1 and self.heap[r] > self.heap[m]: + m = r + + if m != i: + self._swap(m, i) + self._push_down(m) + + def _push_up(self, i: int) -> None: + """Pushes up the node at index i from this MaxHeap. + + Note: this operation only happens if the node at index i is greater than + its parent. + + This operation is also called "bubble-up" or "shift-up". + + Time complexity: O(log(n)).""" + c = i # Current index. + p = self._parent_index(i) + + if p != -1 and self.heap[c] > self.heap[p]: + c = p + + if c != i: + self._swap(c, i) + self._push_up(c) + + +# pylint: disable=protected-access +def is_max_heap(h: MaxHeap) -> bool: + """Returns true if h is a valid MaxHeap, false otherwise.""" + if not isinstance(h, MaxHeap): + return False + if h.heap: + for i, item in enumerate(h.heap): + l = h._left_index(i) + r = h._right_index(i) + if r != -1 and l == -1: + return False + if l != -1 and item < h.heap[l]: + return False + if r != -1 and item < h.heap[r]: + return False + return True diff --git a/andz/ds/MinHeap.py b/andz/ds/MinHeap.py new file mode 100755 index 00000000..44071bdc --- /dev/null +++ b/andz/ds/MinHeap.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 01/07/2015 + +Updated: 29/09/2017 + +# Description + +A binary min-heap is a data structure similar to a binary tree, where the parent +nodes are smaller or equal to their children. In addition to the previous +constraint, a binary min-heap is a complete binary tree, that is, all levels of +the tree, except possibly the deepest one are fully filled, and, if the last +level of the tree is not complete, the nodes of that level are filled from left +to right. + +A min-heap can be implemented with a classic array (or list, in Python). + +If we have a node at index i, then + +- its left child can be found at index i*2 + 1 + +- its right child is found at i*2 + 2, + +- its parent can be found at index floor((i - 1) / 2), where floor(x) truncates +x to the smallest integer. + +Note: these indexes are for 0-index based lists (or arrays). + +# References + +- https://en.wikipedia.org/wiki/Binary_heap +- Slides by prof. A. Carzaniga +- Chapter 13 of Introduction to Algorithms (3rd ed.) by CLRS +- http://www.math.clemson.edu/~warner/M865/HeapDelete.html +""" + +from andz.ds.BinaryHeap import BinaryHeap + +__all__ = ["MinHeap", "is_min_heap"] + + +class MinHeap(BinaryHeap): + """Sub-class of BinaryHeap, and thus provides the same public interface, + but in addition provides two more operations: + + - find_min + - remove_min""" + + def __init__(self, ls=None): + BinaryHeap.__init__(self, ls) + + def find_min(self): + """Returns the smallest element in this MinHeap. + + Time complexity: O(1).""" + return self.heap[0] if not self.is_empty() else None + + def remove_min(self): + """Removes and returns the smallest element in this MinHeap. + + Time complexity: O(log(n)).""" + assert is_min_heap(self) + if not self.is_empty(): + self._swap(0, self.size - 1) + m = self.heap.pop() + if not self.is_empty(): + self._push_down(0) + assert is_min_heap(self) + return m + + def _push_down(self, i: int) -> None: + """Min-heapifies this MinHeap starting from index i. + + This operation is also called "bubble-down" or "shift-down". + + Time complexity: O(log(n)).""" + m = i # Index of node with the smallest value among i and its children. + l = self._left_index(i) + r = self._right_index(i) + + if l != -1 and self.heap[l] < self.heap[m]: + m = l + if r != -1 and self.heap[r] < self.heap[m]: + m = r + + if m != i: + self._swap(m, i) + self._push_down(m) + + def _push_up(self, i: int) -> None: + """Pushes up the node at index i from this MinHeap. + + Note: this operation only happens if the node at index i is smaller than + its parent. + + This operation is also called "bubble-up" or "shift-up". + + Time complexity: O(log(n)).""" + c = i # Current index. + p = self._parent_index(i) + + if p != -1 and self.heap[c] < self.heap[p]: + c = p + + if c != i: + self._swap(c, i) + self._push_up(c) + + +# pylint: disable=protected-access +def is_min_heap(h: MinHeap) -> bool: + """Returns true if h is a valid MinHeap, false otherwise.""" + if not isinstance(h, MinHeap): + return False + if h.heap: + for i, item in enumerate(h.heap): + l = h._left_index(i) + r = h._right_index(i) + if r != -1 and l == -1: + return False + if l != -1 and item > h.heap[l]: + return False + if r != -1 and item > h.heap[r]: + return False + return True diff --git a/andz/ds/MinMaxHeap.py b/andz/ds/MinMaxHeap.py new file mode 100644 index 00000000..b9c5a8e2 --- /dev/null +++ b/andz/ds/MinMaxHeap.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 18/02/2016 + +Updated: 23/03/2024 + +# Description + +Min-max heap is a heap that supports find-min and find-max operations in +constant time. Moreover, both remove-min and remove-max are supported in +logarithmic time. It's therefore an useful data structure to represent +double-ended priority queues. + +The min-max heap ordering is the following: + + values stored at nodes on even (or min) levels are smaller than or equal to + values stored at their descendants, whereas values stored at nodes on odd + (or max) levels are greater than or equal to values stored at their + descendants. + +Even levels are 0, 2, 4, 6, etc., whereas odd levels are 1, 3, 5, 7, etc. + +# TODO + +- find-kth, i.e. find the kth smallest element in the structure, in O(1) time. +- delete-kth, i.e. delete the kth smallest element, in O(log n) time. + +# References + +- http://www.akira.ruc.dk/~keld/teaching/algoritmedesign_f03/Artikler/02/Atkinson86.pdf +- http://www.diku.dk/forskning/performance-engineering/Jesper/heaplab/heapsurvey_html/node11.html +- http://www.math.clemson.edu/~warner/M865/HeapDelete.html +""" + +import math +from logging import getLogger + +from andz.ds.BinaryHeap import BinaryHeap + +logger = getLogger(__name__) + +__all__ = ["MinMaxHeap", "is_min_max_heap"] + + +class MinMaxHeap(BinaryHeap): + """Subclass of BinaryHeap, and thus provides the same public interface, + but in addition provides four more operations: + + - find_max + - find_min + - remove_max + - remove_min""" + + def __init__(self, ls=None): + BinaryHeap.__init__(self, ls) + + def find_max(self): + """Returns the greatest element in this MinMaxHeap. + + Time complexity: O(1).""" + if not self.is_empty(): + return self.heap[self._find_max_index()] + + def find_min(self): + """Returns the smallest element in this MinMaxHeap. + + Time complexity: O(1).""" + if not self.is_empty(): + return self.heap[0] + + def remove_max(self): + """Removes and returns the greatest element in this MinMaxHeap. + + Time complexity: O(log(n)).""" + assert is_min_max_heap(self) + + if not self.is_empty(): + i = self._find_max_index() + + if i == self.size - 1: + m = self.heap.pop() + assert is_min_max_heap(self) + return m + + self._swap(i, self.size - 1) + m = self.heap.pop() + self._push_up(i) + self._push_down(i) + assert is_min_max_heap(self) + return m + + def remove_min(self): + """Removes and returns the smallest element in this MinMaxHeap. + + Time complexity: O(log(n)).""" + if not self.is_empty(): + if self.size == 1: + m = self.heap.pop() + assert is_min_max_heap(self) + return m + + self._swap(0, self.size - 1) + m = self.heap.pop() + self._push_up(0) + self._push_down(0) + assert is_min_max_heap(self) + return m + + def _push_down(self, i: int) -> None: + """This operation is also called "bubble-down" or "shift-down".""" + if self._is_on_even_level(i): + self._push_down_min(i) + else: + self._push_down_max(i) + + def _push_down_min(self, i: int) -> None: + """Helper method for self._push_down.""" + if self._has_children(i): + m = self._index_of_min(i) + + if self._is_grandchild(m, i): + if self.heap[m] < self.heap[i]: + self._swap(i, m) + + mp = self._parent_index(m) + if mp != -1 and self.heap[m] > self.heap[mp]: + self._swap(m, mp) + self._push_down_min(m) + + else: # self.heap[m] is a child of self.heap[i]. + if self.heap[m] < self.heap[i]: + self._swap(i, m) + + def _push_down_max(self, i: int) -> None: + """Helper method for self._push_down.""" + if self._has_children(i): + m = self._index_of_max(i) + + if self._is_grandchild(m, i): + if self.heap[m] > self.heap[i]: + self._swap(i, m) + + mp = self._parent_index(m) + if mp != -1 and self.heap[m] < self.heap[mp]: + self._swap(m, mp) + self._push_down_max(m) + + else: # self.heap[m] is a child of self.heap[i]. + if self.heap[m] > self.heap[i]: + self._swap(i, m) + + def _push_up(self, i: int) -> None: + """This operation is also called "bubble-up" or "shift-up".""" + p = self._parent_index(i) + + # Let x be the element at index i. + # If x has a parent at position p, we call it y. + if self._is_on_even_level(i): + if p != -1 and self.heap[i] > self.heap[p]: + # If x is greater than y, swap x with y. + # Now, x is at index p, and y at index i. + # _push_up_max from the new index of x, i.e. p. + self._swap(i, p) + self._push_up_max(p) + else: + # x does not have a parent OR x <= y. + self._push_up_min(i) + else: + # Odd or max level. + if p != -1 and self.heap[i] < self.heap[p]: + self._swap(i, p) + self._push_up_min(p) + else: + self._push_up_max(i) + + def _push_up_min(self, i: int) -> None: + """Helper method for self._push_up.""" + g = self._grandparent_index(i) + # Let x be the element at index i. + # If x has a grandparent at position g, we call it z. + + # If the z exists and x is smaller than z, swap x and z. + # Now, x is at index g and z at index i. + if g != -1 and self.heap[i] < self.heap[g]: + self._swap(i, g) + self._push_up_min(g) + + def _push_up_max(self, i: int) -> None: + """Helper method for self._push_up.""" + g = self._grandparent_index(i) + if g != -1 and self.heap[i] > self.heap[g]: + self._swap(i, g) + self._push_up_max(g) + + def _find_max_index(self) -> int: + """Returns the index of the maximum element in this MinMaxHeap. + + Time complexity: O(1).""" + if self.is_empty(): + return -1 + if self.size == 1: + return 0 + if self.size == 2: + return 1 + return 1 if self.heap[1] > self.heap[2] else 2 + + def _index_of_min(self, i: int) -> int: + """Returns the index of the smallest element among the children and + grandchildren of the node at index i. + + Time complexity: O(1).""" + m = l = self._left_index(i) + r = self._right_index(i) + + if r != -1 and self.heap[r] < self.heap[m]: + m = r + + if l != -1: + gll = self._left_index(l) + if gll != -1 and self.heap[gll] < self.heap[m]: + m = gll + glr = self._right_index(l) + if glr != -1 and self.heap[glr] < self.heap[m]: + m = glr + + if r != -1: + grl = self._left_index(r) + if grl != -1 and self.heap[grl] < self.heap[m]: + m = grl + grr = self._right_index(r) + if grr != -1 and self.heap[grr] < self.heap[m]: + m = grr + + return m + + def _index_of_max(self, i: int) -> int: + """Returns the index of the largest element among the children and + grandchildren of the node at index i. + + Time complexity: O(1).""" + m = l = self._left_index(i) + r = self._right_index(i) + + if r != -1 and self.heap[r] > self.heap[m]: + m = r + + if l != -1: + gll = self._left_index(l) + if gll != -1 and self.heap[gll] > self.heap[m]: + m = gll + glr = self._right_index(l) + if glr != -1 and self.heap[glr] > self.heap[m]: + m = glr + + if r != -1: + grl = self._left_index(r) + if grl != -1 and self.heap[grl] > self.heap[m]: + m = grl + grr = self._right_index(r) + if grr != -1 and self.heap[grr] > self.heap[m]: + m = grr + + return m + + def _has_children(self, i: int) -> bool: + """Returns true if the node at index i has at least one child, false + otherwise. + + Time complexity: O(1).""" + assert self._is_good_index(i) + return self._left_index(i) != -1 or self._right_index(i) != -1 + + def _is_child(self, c: int, i: int) -> bool: + """Returns true if c is a child of i, false otherwise. + + Time complexity: O(1).""" + assert self._is_good_index(c) and self._is_good_index(i) + return c == self._left_index(i) or c == self._right_index(i) + + def _is_grandchild(self, g: int, i: int) -> bool: + """Returns true if g is a grandchild of i, false otherwise. + + Time complexity: O(1).""" + l = self._left_index(i) + if l == -1: + assert self._right_index(i) == -1 + assert self._is_good_index(g) + return False + r = self._right_index(i) + if r == -1: + return self._is_child(g, l) + return self._is_child(g, l) or self._is_child(g, r) + + def _grandparent_index(self, i: int) -> int: + """Returns the grandparent's index of the node at index i. + + -1 is returned either if i has not a parent or the parent of i does not + have a parent. + + Time complexity: O(1).""" + p = self._parent_index(i) + return -1 if p == -1 else self._parent_index(p) + + def _is_on_even_level(self, i: int) -> bool: + """Returns true if node at index i is on a even-level, i.e., if i is on + a level multiple of 2. + + Time complexity: O(int(log(i + 1) % 2) == 0).""" + assert self._is_good_index(i) + return int(math.log2(i + 1) % 2) == 0 + + def _is_on_odd_level(self, i: int) -> bool: + """Returns true when self._is_on_even_level(i) returns false, and + vice-versa.""" + return not self._is_on_even_level(i) + + +# pylint: disable=protected-access, too-many-return-statements, too-many-branches +def is_min_max_heap(h: MinMaxHeap) -> bool: + """Returns true if h is a valid MinMaxHeap object, false otherwise.""" + logger.debug("Min-max heap: %s (type = %s)", h, type(h).__qualname__) + if not isinstance(h, MinMaxHeap): + return False + + if h.heap: + + if h.size == 1: + return True + if h.size == 2: + return max(h.heap) == h.heap[1] and min(h.heap) == h.heap[0] + if h.size >= 3: + if h.heap[0] != min(h.heap) or ( + h.heap[1] != max(h.heap) and h.heap[2] != max(h.heap) + ): + return False + + # i is the index of the current node. + for i, item in reversed(list(enumerate(h.heap))): + l = h._left_index(i) + r = h._right_index(i) + + # If item has a right child but not a left child, then this heap is + # not well structured. + if r != -1 and l == -1: + return False + + if h._is_on_even_level(i): + # If item is on an even (or min) level, then item should be + # smaller or equal than its descendants. + if l != -1 and item > h.heap[l]: + return False + if r != -1 and item > h.heap[r]: + return False + else: # odd (or max) level + # If item is on an odd (or max) level, then item should be + # greater or equal than descendants. + if l != -1 and item < h.heap[l]: + return False + if r != -1 and item < h.heap[r]: + return False + return True diff --git a/andz/ds/Queue.py b/andz/ds/Queue.py new file mode 100755 index 00000000..8a7e8e15 --- /dev/null +++ b/andz/ds/Queue.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 02/07/2015 + +Updated: 28/09/2017 + +# Description + +A queue is a FIFO (first-in, first-out) data structure, which means that +elements that are first inserted into the data structure are the ones that are +first removed from the same. + +# References + +- https://docs.python.org/3.1/tutorial/datastructures.html#using-lists-as-queues +""" + +from collections import deque +from collections.abc import Iterable + +__all__ = ["Queue"] + + +class Queue: + """This is a wrapper class around the Python deque data structure, which + supports the "dequeue" operation better, in terms of performance, w.r.t. + lists, to logically represent a queue (FIFO) data structure. + + You can initialize the class using an iterable (list, tuple, etc) of values, + which will be assumed to be already in the FIFO order. + + If ls is not an instance of Iterable, TypeError is raised. + If one of the values in ls is None, ValueError is raised. + A copy of ls is made, so that changes to the original self do not reflect in + the original iterable. + + This class does not allow None to be inserted as value to the data structure + through the methods of the same. + + It also returns None, instead of raising exceptions, when trying to dequeue, + when the data structure is empty.""" + + def __init__(self, ls=None): + if ls is not None: + if not isinstance(ls, Iterable): + raise TypeError("ls must be an iterable object") + if any(elem is None for elem in ls): + raise ValueError("all elements of ls must be not None") + else: + ls = [] + self._q = deque(ls) + + @property + def size(self) -> int: + """Returns the size of this queue.""" + return len(self._q) + + def is_empty(self) -> bool: + """Returns true if this queue is empty, false otherwise.""" + return self.size == 0 + + def enqueue(self, elem) -> None: + """Adds elem to the end of this queue. + + If elem is None, ValueError is raised.""" + if elem is None: + raise ValueError("elem cannot be None") + self._q.append(elem) + + def dequeue(self): + """Returns the first element of this queue, or None if the queue is + empty.""" + return None if self.is_empty() else self._q.popleft() + + def __str__(self): + return str(list(self._q)) + + def __repr__(self): + return self.__str__() diff --git a/andz/ds/RBT.py b/andz/ds/RBT.py new file mode 100755 index 00000000..7867513c --- /dev/null +++ b/andz/ds/RBT.py @@ -0,0 +1,669 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 01/08/2015 + +Updated: 28/09/2017 + +# Description + +## Red-black tree property + +1. Every node is either red or black. + +2. The root is black. + +3. Every NIL or leaf node is black. + +4. If a node is red, then both its children are black, which also means that +there cannot be two red nodes in a row. + +5. For every node x, each path from x to its descendant leaves has the same +number of black nodes. + +## Lemma + +The height h(x) of a red-black tree with n internal nodes is at most + + 2 * log₂(n + 1), + +that is, + + h(x) <= 2 * log₂(n + 1), + +which is equivalent to + + h(x) / 2 <= log₂(n + 1), + +which is equivalent to + + n >= 2^(h(x) / 2) - 1. + +To possibly better understand the previous statements, we can perform the +reversed reasoning: + + n >= 2^(h(x) / 2) - 1 ⇔ + + n + 1 >= 2^(h(x) / 2) + +Now, we log both parts: + + log₂(n + 1) >= log₂(2^(h(x) / 2)) ⇔ + + log₂(n + 1) >= h(x) / 2 * log₂(2) ⇔ + + log₂(n + 1) >= h(x) / 2 * 1 ⇔ + + 2 * log₂(n + 1) >= h(x) + + +### Proof + +We want to prove (by induction) that, for all x, size(x) >= 2^bh(x) - 1. + +1. Base case: x is a leaf, so size(x) = 0 and bh(x) = 0. + +2. Induction hypothesis: consider y₁, y₂, and x such that + + parent(y₁) = parent(y₂) = x, + +and assume () that + + size(y₁) >= 2^bh(y₁) - 1, and + size(y₂) >= 2^bh(y₂) - 1. + +We want to prove: size(x) >= 2^bh(x) - 1. + +1.3. Induction step + +size(x) = size(y₁) + size(y₂) + 1 >= (2^bh(y₁) - 1) + (2^bh(y₂) - 1) + 1 + +Since + + +---------------------------------+ + | bh(y), if color(y) = red | +bh(x) = | | + | bh(y) + 1, if color(y) = black | + +---------------------------------+ + +size(x) >= (2^(bh(x) - 1) - 1) + (2^(bh(x) - 1) - 1) + 1 = (2^bh(x) - 1). + +Since every red node has black children, in every path from x to a leaf +node, at least half the nodes are black, thus + + bh(x) >= h(x) / 2. + +So: n = size(x) >= 2^(h(x) / 2) - 1. + +Therefore: h(x) <= 2 * log₂(n + 1). + +## Red-black tree operations + +A red-black tree works as a binary-search tree for search, insert, etc, so the +complexity of those operations is T(n) = O(h), that is T(n) = O(log₂(n)), which +is also the worst case complexity. + +# References + +- https://en.wikipedia.org/wiki/Red%E2%80%93black_tree +- Slides by prof. A. Carzaniga +- Chapter 13 of Introduction to Algorithms (3rd ed.) by CLRS +""" + +import math + +from andz.ds.BST import BST, _BSTNode, is_bst + +__all__ = ["RBT", "is_rbt"] + +RED = "RED" +BLACK = "BLACK" + + +class _RBTNode(_BSTNode): + """Class to represent a node of a RBT.""" + + # pylint: disable=too-many-arguments + def __init__(self, key, color=BLACK, parent=None, left=None, right=None): + _BSTNode.__init__(self, key, parent, left, right) + self.color = color + + +class RBT(BST): + """Red-black tree, which is a self-balancing binary-search tree. + + Since it's self-balancing operations such as inserting, searching or + deletion all take O(log₂(n)).""" + + def __init__(self): + BST.__init__(self) + + def insert(self, key) -> None: + """Inserts key into this RBT. + + This operation is similar to the insert operation of a classical BST, + but, in this case, the red-black tree property must be maintained, so + additional work is needed. + + There are several cases of inserting into a RBT to handle: + + 1. key is the root node. + + 2. key.parent is BLACK. + + 3. key.parent and the uncle of key are RED. + + The uncle of key will be the left child of key.parent.parent, if + key.parent is the right child of key.parent.parent, otherwise the uncle + will be the right child of key.parent.parent. + + 4. key.parent is RED, but key.uncle is BLACK (or None). + + key.grandparent exists because key.parent is RED. + + 4.1. key is added to the right of a left child of key.parent.parent + (grandparent). + + 4.2. or key is added to the left of a right child of + key.parent.parent. + + 4.3. key is added to the left of a left child of key.parent.parent. + + 4.4. or key is added to the right of a right child of + key.parent.parent. + + _fix_insertion handles these cases in the same order as above. + + Time complexity: O(log₂(n)).""" + assert is_rbt(self) + + if key is None: + raise ValueError("key cannot be None") + + key_node = _RBTNode(key) + + c = self._root # Current node. + p = None # Current node's parent. + + while c is not None: + p = c + if key_node.key < c.key: + c = c.left + else: # key.key >= c.key + c = c.right + + key_node.parent = p + + # while loop was not executed even once. + # Case 1: node is inserted as root. + if p is None: + self._root = key_node + elif p.key > key_node.key: + p.left = key_node + else: # p.key < key.key + p.right = key_node + + key_node.color = RED + self._n += 1 + self._fix_insertion(key_node) + + assert is_rbt(self) + + def _fix_insertion(self, u: _RBTNode) -> None: + # u is the root and we color it BLACK. + if u.parent is None: + u.color = BLACK + + elif u.parent.color == BLACK: + return + + elif u.parent.color == RED and (u.uncle is not None and u.uncle.color == RED): + u.parent.color = BLACK + u.uncle.color = BLACK + u.grandparent.color = RED + self._fix_insertion(u.grandparent) + + elif u.parent.color == RED and (u.uncle is None or u.uncle.color == BLACK): + + # u is added as a right child to a node that is the left child. + if u.parent.is_left_child() and u.is_right_child(): + + # left_rotation does not violate the property: all paths from + # any given node to its leaf nodes contain the same number of + # black nodes. + self._left_rotate(u.parent) + + # With the previous _left_rotate call, u.parent has become the + # left child of u, or, u bas become the parent of what before + # was u.parent. + # + # We can pass to case 5, where we have 2 red nodes in a row, + # specifically, u.parent and u, which are both left children of + # their parents. + + self._fix_insertion(u.left) + + # u is added as a left child to a node that is the right child. + elif u.parent.is_right_child() and u.is_left_child(): + self._right_rotate(u.parent) + self._fix_insertion(u.right) + + # u is added as a left child to a node that is the left child. + elif u.parent.is_left_child() and u.is_left_child(): + # Note: grandparent is known to be black, since its former child + # could not have been RED without violating property 4. + self._right_rotate(u.grandparent) + u.parent.color = BLACK + u.parent.right.color = RED + + # u is added as a right child to a node that is the right child. + elif u.parent.is_right_child() and u.is_right_child(): + self._left_rotate(u.grandparent) + u.parent.color = BLACK + u.parent.left.color = RED + + else: + assert False + + def _left_rotate(self, u: _RBTNode) -> _RBTNode: + """Left rotates the subtree rooted at node u. + + Returns the node which is at the previous position of u, that is it + returns the parent of u. + + Time complexity: O(1).""" + assert u.has_right_child() + + u.right.parent = u.parent + + # Only the root has a None parent. + if not u.has_parent(): + self._root = u.right + + # Checking if u is a left or a right child, in order to set the new left + # or right child respectively of its parent. + elif u.is_left_child(): + u.parent.left = u.right + else: + u.parent.right = u.right + + u.parent = u.right + + # The new right child of u becomes what is the left child of its + # previous right child. + u.right = u.parent.left + + # Set u to be the parent of its new right child. + if u.has_right_child(): + u.right.parent = u + + # Set u to be the new left child of its new parent. + u.parent.left = u + return u.parent + + def _right_rotate(self, u: _RBTNode) -> _RBTNode: + """Right rotates the subtree rooted at node u. + + Time complexity: O(1).""" + assert u.has_left_child() + + u.left.parent = u.parent + + if not u.has_parent(): + self._root = u.left + elif u.is_left_child(): + u.parent.left = u.left + else: + u.parent.right = u.left + + u.parent = u.left + u.left = u.parent.right + + if u.has_left_child(): + u.left.parent = u + + u.parent.right = u + return u.parent + + # pylint: disable=too-many-statements, too-many-branches + def delete(self, key: object) -> None: + """Delete key from this RBT object. + + Time complexity: O(log₂(n)).""" + assert is_rbt(self) + + # A few checks of the inputs given. + if key is None: + raise ValueError("key cannot be None") + + key_node = self._search_key_iteratively(key, self._root) + if key_node is None: + raise LookupError("key not in this BST") + + # If key has 2 non-leaf children, then replace key with its successor. + # Note: we exchange also the colors of key and its successor. + if key_node.has_left_child() and key_node.has_right_child(): + s = self._successor(key_node) + self._switch(key_node, s) + key_node.color, s.color = s.color, key_node.color + + # At least one of the children must be None. Particularly, if key was + # exchanged with its successor, key now should not have a left child. + assert not key_node.has_left_child() or not key_node.has_right_child() + + # At this point key has at most 1 child. + # Keep in mind this when reading the next cases! + + # If key is a red node and it has a child, we simply replace it with its + # child c, which must be black by property 4. + + # This can only occur when key has 2 leaf children, because if key had a + # black non-leaf child on one side, but just a leaf child on the other + # side, then the count of black nodes on both sides would be different, + # thus the tree would violate property 5. + if key_node.color == RED: + + # A few checks while in alpha stage. + assert not key_node.has_left_child() and not key_node.has_right_child() + assert key_node != self._root + + if key_node.is_left_child(): + key_node.parent.left = None + else: + key_node.parent.right = None + + else: # key.color == BLACK + + # One of the children of key is red. + + # Simply removing key could break properties 4, i.e., both children + # of every red node are black, because key.parent could be red, and + # 5, i.e. all paths from any given node to its leaf nodes contain + # the same number of black nodes), but if we repaint c (the child) + # BLACK, both of these properties are preserved. + + if key_node.has_left_child() and key_node.left.color == RED: + if self._root != key_node: + if key_node.is_left_child(): + key_node.parent.left = key_node.left + else: + key_node.parent.right = key_node.left + + key_node.left.parent = key_node.parent + key_node.left.color = BLACK + + if self._root == key_node: + self._root = key_node.left + + elif key_node.has_right_child() and key_node.right.color == RED: + if self._root != key_node: + if key_node.is_left_child(): + key_node.parent.left = key_node.right + else: + key_node.parent.right = key_node.right + + key_node.right.parent = key_node.parent + key_node.right.color = BLACK + + if self._root == key_node: + self._root = key_node.right + else: + # This the complex case: both key and c (the child) are BLACK. + + # This can only occur when deleting a black node which has 2 + # leaf children, because if the black node key had a black + # non-leaf child on one side but just a leaf child on the other + # side, then the count of black nodes on both sides would be + # different, thus the tree would have been an invalid red–black + # tree by violation of property 5. + assert not key_node.has_left_child() and not key_node.has_right_child() + + # 6 cases + if self._root != key_node: + + assert key_node.sibling is not None + + # Note: key.sibling cannot be None, because otherwise the + # subtree containing it would have fewer black nodes than + # the subtree containing key. + # + # Specifically, the subtree containing key would have a + # black height of 2, whereas the one containing the sibling + # would have a black height of 1. + self._delete_case_1(key_node) + + # We begin by replacing key with its child c. + # Note: both children of key are leaf children. + if key_node.is_left_child(): + key_node.parent.left = None + else: + key_node.parent.right = None + + else: + self._root = None + + self._n -= 1 + + assert is_rbt(self) + + def _delete_case_1(self, u: _RBTNode) -> None: + # This check is necessary because this function is also called from the + # _delete_case_3 function. + if u.parent is not None: + self._delete_case_2(u) + + def _delete_case_2(self, u: _RBTNode) -> None: + if u.sibling.color == RED: + + assert u.parent.color == BLACK + + u.sibling.color = BLACK + u.parent.color = RED + + if u.is_left_child(): + self._left_rotate(u.parent) + else: + self._right_rotate(u.parent) + + assert u.sibling.color == BLACK + + self._delete_case_3(u) + + # pylint: disable=too-many-boolean-expressions + def _delete_case_3(self, u: _RBTNode) -> None: + # Not sure if the children of u.sibling can be None. + if ( + u.parent.color == BLACK + and u.sibling.color == BLACK + and ( + (u.sibling.left and u.sibling.left.color == BLACK) or not u.sibling.left + ) + and ( + (u.sibling.right and u.sibling.right.color == BLACK) + or not u.sibling.right + ) + ): + + u.sibling.color = RED + self._delete_case_1(u.parent) + else: + self._delete_case_4(u) + + # pylint: disable=too-many-boolean-expressions + def _delete_case_4(self, u: _RBTNode) -> None: + # Not sure if the children of u.sibling can be None. + if ( + u.parent.color == RED + and u.sibling.color == BLACK + and ( + (u.sibling.left and u.sibling.left.color == BLACK) or not u.sibling.left + ) + and ( + (u.sibling.right and u.sibling.right.color == BLACK) + or not u.sibling.right + ) + ): + + u.sibling.color = RED + u.parent.color = BLACK + else: + self._delete_case_5(u) + + def _delete_case_5(self, u: _RBTNode) -> None: + assert u.sibling is not None + + if u.sibling.color == BLACK: + if ( + u.is_left_child() + and (not u.sibling.right or u.sibling.right.color == BLACK) + and u.sibling.left.color == RED + ): + + u.sibling.color = RED + u.sibling.left.color = BLACK + self._right_rotate(u.sibling) + + elif ( + u.is_right_child() + and (not u.sibling.left or u.sibling.left.color == BLACK) + and u.sibling.right.color == RED + ): + + u.sibling.color = RED + u.sibling.right.color = BLACK + self._left_rotate(u.sibling) + + self._delete_case_6(u) + + def _delete_case_6(self, u: _RBTNode) -> None: + assert u.sibling is not None + + u.sibling.color, u.parent.color = u.parent.color, u.sibling.color + + if u.is_left_child(): + assert u.sibling.right + u.sibling.right.color = BLACK + self._left_rotate(u.parent) + else: + assert u.sibling.left + u.sibling.left.color = BLACK + self._right_rotate(u.parent) + + def remove_max(self) -> None: + """Removes the greatest element from self. + + Time complexity: O(log₂(n)).""" + assert is_rbt(self) + if self._root is not None: + m = self.maximum() + assert m is not None + self.delete(m) + assert is_rbt(self) + + def remove_min(self) -> None: + """Removes the smallest element from self. + + Time complexity: O(log₂(n)).""" + assert is_rbt(self) + if self._root is not None: + m = self.minimum() + assert m is not None + self.delete(m) + assert is_rbt(self) + + +def black_height(n: _RBTNode) -> int: + """Returns the black-height of the node n.""" + if n is None: + return 1 + + if not isinstance(n, _RBTNode): + raise TypeError("n must be an instance of _RBTNode") + + left_bh = black_height(n.left) + right_bh = black_height(n.right) + + if left_bh != right_bh: + return -1 + return left_bh + (1 if n.color == BLACK else 0) + + +def upper_bound_height(t: RBT) -> bool: + """Returns true if the height of the red-black tre t is bounded above by + log₂(n + 1).""" + return t.height() <= 2 * math.log2(t.size + 1) + + +# pylint: disable=protected-access +def is_rbt(t: RBT) -> bool: + """Returns true if t is a valid RBT object, false otherwise.""" + + def are_all_red_or_black(t: RBT) -> bool: + """Returns true if all colors are either RED or BLACK.""" + + def h(n: _RBTNode) -> bool: + if n is not None: + if ( + n.color != BLACK and n.color != RED + ): # pylint: disable=consider-using-in + return False + return h(n.right) and h(n.left) + return True + + return h(t._root) + + def is_root_black(t: RBT) -> bool: + """Returns true if the root is BLACK (or it is None), false + otherwise.""" + if t._root is not None: + return t._root.color == BLACK + return True + + def has_not_consecutive_red_nodes(t: RBT) -> bool: + def h(n: _RBTNode) -> bool: + if n is not None: + if n.parent is not None and n.color == RED and n.parent.color == RED: + return False + if n.parent is None and n.color == RED: + return False + return h(n.left) and h(n.right) + return True + + return h(t._root) + + def all_paths_have_same_black_height(t: RBT) -> bool: + return black_height(t._root) != -1 + + def are_all_rbt_nodes(t: RBT) -> bool: + def h(n: _RBTNode) -> bool: + if n is not None: + if not isinstance(n, _RBTNode): + return False + return h(n.left) and h(n.right) + return True + + return h(t._root) + + if not is_bst(t): + return False + + if not isinstance(t, RBT): + return False + + if not are_all_rbt_nodes(t): + return False + + if not upper_bound_height(t): + return False + + return ( + are_all_red_or_black(t) + and is_root_black(t) + and has_not_consecutive_red_nodes(t) + and all_paths_have_same_black_height(t) + ) diff --git a/andz/ds/README.md b/andz/ds/README.md new file mode 100644 index 00000000..56071df3 --- /dev/null +++ b/andz/ds/README.md @@ -0,0 +1,66 @@ +# Data Structures + +The subpackage that contains all implemented data structures in `andz`. + +A few interesting data structures which may be implemented in the future. + +## Graphs + +- Adjacency List +- Adjacency Matrix +- Directed Graph +- Undirected Graph + +## Linear Data Structures + +### Arrays + +- Matrix +- Sparse Matrix +- Gap Buffer + +### Lists + +- [Skip list](https://brilliant.org/wiki/skip-lists/) +- [Doubly connected edge list](https://en.wikipedia.org/wiki/Doubly_connected_edge_list) + +## Trees + +### Binary Trees + +- AVL Tree +- Splay Tree +- WAVL Tree + +### B-trees + +- B-tree +- 2-3 Tree + +### Heaps + +- Fibonacci Heap +- Binomial Heap + +### Trees and Tries + +- Trie +- Radix Tree +- Suffix Tree + +### Multi-way Trees + +- Van Emde Boas Tree + +### Space-partitioning Trees + +- K-d Tree + +## Probabilistic Data Structures + +- Bloom filter + +--- + +An article listing other possibly interesting data structures can be found at the +following URL [https://en.wikipedia.org/wiki/List_of_data_structures](https://en.wikipedia.org/wiki/List_of_data_structures). \ No newline at end of file diff --git a/andz/ds/Stack.py b/andz/ds/Stack.py new file mode 100755 index 00000000..edd1d3a1 --- /dev/null +++ b/andz/ds/Stack.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 05/07/2015 + +Updated: 28/09/2017 + +# Description + +A stack is a simple abstract data type. + +An abstract data type is a logical description or specification of a certain way +of viewing and/or organizing data, and which values and operations are allowed +on this data. An ADT is (as the same suggests) an abstract concept or +mathematical model. Thus an ADT can be implemented as a data structure in many +ways. Essentially, ADTs is all about ideas or concepts of representing and +manipulating data, whereas a data structure is an implementation of a specific +ADTs. Hence there can be more than one data structure for the same ADT. + +What defines a stack is the order of insertion/removal of elements to/from it: +a stack is a "last in, first out" (or, as an acronym, LIFO) abstract data type, +that is, the last element inserted into the stack is the first to be removed. +Since this is an ADT, we don't care how the elements are stored in memory, or +how we manipulate them so that the last element inserted is the first to be +removed. + +The insertion of an element is usually called "push", whereas the removal is +usually called "pop". There's also another operation (i.e. "peek" or "top") +which consists in looking at the last element inserted into the stack. +Of course, other operations, such as "size of the stack" (i.e. how many elements +in the stack) or "is empty" (i.e. checking if the stack contains elements or +not) may also be useful. + +# References + +- http://interactivepython.org/runestone/static/pythonds/Introduction/ + WhyStudyDataStructuresandAbstractDataTypes.html +- https://stackoverflow.com/q/195625/3924118 +- https://stackoverflow.com/q/1115313/3924118 +- https://stackoverflow.com/q/12342457/3924118 +""" + +from collections.abc import Iterable + +from tabulate import tabulate + +__all__ = ["Stack"] + + +class Stack: + """This is a wrapper class around the Python's list to represent a stack + data structure. + + It doesn't allow you to insert None elements through the public methods. + + It returns None whenever you try to pop from or peek at the stack, but it's + empty. + + The data structure can be initialized with an iterable object without None + values. A copy of the given iterable is made, so the original iterable is + not affected when performing operations.""" + + def __init__(self, s=None): + if s is not None: + if not isinstance(s, Iterable): + raise TypeError("s must be an instance of Iterable") + if any(elem is None for elem in s): + raise ValueError("all elements of s must be not None") + else: + s = [] + self._stack = list(s) + + @property + def size(self) -> int: + """Returns the size of this stack. + + Time complexity: O(1).""" + return len(self._stack) + + def is_empty(self) -> bool: + """Returns true if this stack is empty, false otherwise. + + Time complexity: O(1).""" + return self.size == 0 + + def push(self, elem: object) -> None: + """Pushes elem on top of this stack. + + If elem is None, ValueError is raised. + + Time complexity: O(1).""" + if elem is None: + raise ValueError("elem cannot be None") + self._stack.append(elem) + + def pop(self) -> object: + """Returns the top of this stack, or None if the stack is empty. + + Time complexity: O(1).""" + return None if self.is_empty() else self._stack.pop() + + def top(self) -> object: + """Returns but does not pop the top of the stack. + + If the stack is empty, None is returned. + + This operation is also called "peek". + + Time complexity: O(1).""" + return None if self.is_empty() else self._stack[-1] + + def __str__(self): + return tabulate([[e] for e in reversed(self._stack)], tablefmt="grid") + + def __repr__(self): + return self.__str__() diff --git a/andz/ds/TST.py b/andz/ds/TST.py new file mode 100644 index 00000000..f4606b1d --- /dev/null +++ b/andz/ds/TST.py @@ -0,0 +1,641 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 05/09/2015 + +Updated: 28/09/2017 + +# Description + +Ternary-search tries (or trees) combine the time efficiency of other tries with +the space efficiency of binary-search trees. + +An advantage compared to hash maps is that ternary search tries support sorting, +but the keys of a ternary-search trie can only be strings, whereas a hash map +supports any kind of hashable keys. + +## TSTs vs Hashing + +### Hashing + +- Need to examine entire key +- Search miss and hits cost about the same +- Performance relies on hash function +- Does not support ordered symbol table operations + +### TSTs + +- Works only for strings (or digital keys) +- Only examines just enough key characters +- Search miss may involve only a few characters +- Supports ordered symbol table operations: + - keys-that-match + - keys-with-prefix + - longest-prefix-of + +### Bottom line + +TSTs are: + +- faster than hashing (especially for search misses) +- more flexible than red-black trees + +# TODO + +- Improve is_tst function + +# References + +- https://www.cs.upc.edu/~ps/downloads/tst/tst.html +- https://www.cs.princeton.edu/~rs/strings/ +- http://algs4.cs.princeton.edu/52trie/TST.java.html +- https://www.youtube.com/watch?v=CIGyewO7868 +- https://en.wikipedia.org/wiki/Ternary_search_tree +- http://stackoverflow.com/a/27178771/3924118 +""" + +__all__ = ["TST"] + + +class _TSTNode: + """A _TSTNode has 6 fields: + + - key, which is a character; + + - value, which is None if self is not a terminal node (of an inserted + string in the TST); + + - parent, which is a pointer to a _TSTNode representing the parent of + self; + + - left, which is a pointer to a _TSTNode whose key is smaller + lexicographically than key; + + - right, which is similarly a pointer to a _TSTNode whose key is greater + lexicographically than key; + + - mid, which is a pointer to a _TSTNode whose key is the following + character of key in an inserted string.""" + + # pylint: disable=too-many-arguments + def __init__(self, key, value=None, parent=None, left=None, mid=None, right=None): + if not isinstance(key, str): + raise TypeError("key must be an instance of str.") + if not key: + raise ValueError("key must be a string of length >= 1.") + self.key = key + self.value = value + self.parent = parent + self.left = left + self.mid = mid + self.right = right + + def is_left_child(self) -> bool: + """ + Return true if self is the left child of its parent, else false. + + If self has no parent, an ``AttributeError`` is raised. + """ + if not self.parent: + raise AttributeError("self does not have a parent.") + if self.parent.left: + return self.parent.left == self + return False + + def is_right_child(self) -> bool: + """ + Return true if self is the right child of its parent, else false. + + If self has no parent, an ``AttributeError`` is raised. + """ + if not self.parent: + raise AttributeError("self does not have a parent.") + if self.parent.right: + return self.parent.right == self + return False + + def is_mid_child(self) -> bool: + """ + Return true if self is the middle child of its parent, else false. + + If self has no parent, an ``AttributeError`` is raised. + """ + if not self.parent: + raise AttributeError("self does not have a parent.") + if self.parent.mid: + return self.parent.mid == self + return False + + def has_children(self) -> bool: + """ + Return true if self has a left, right or middle child, else false. + """ + return self.left or self.right or self.mid + + def __str__(self): + return f"{self.key}: {self.value}" + + def __repr__(self): + return self.__str__() + + +class TST: + """ + An implementation of a typical ternary-search trie (or tree). + + It does not allow (through public methods) empty strings to be inserted. + + In general, the way the ternary search tree looks like highly depends on the + order of insertion of the keys, that is, inserting the same keys but in + different orders produces internally a different structure or shape of the + ternary-search tree. + """ + + def __init__(self): + self._n = 0 + self._root = None + + @property + def size(self) -> int: + """ + Return the number of strings in self. + """ + return self._n + + def is_empty(self) -> bool: + """ + Return true if the size of self is zero, false otherwise. + + Time complexity: O(1). + """ + return self.size == 0 + + def _is_root(self, u: _TSTNode) -> bool: + result = self._root == u + if result: + assert u.parent is None + else: + assert u.parent is not None + return result + + def count(self) -> int: + """Counts the number of strings in this TST. + + This method recursively passes through all the nodes and counts the ones + which have a non None value. + + YOU SHOULD CLEARLY USE size INSTEAD: THIS METHOD IS HERE ONLY FOR THE + FUN OF WRITING CODE! + + Time complexity: O(n), where n is the number of nodes in this TST.""" + c = self._count(self._root, 0) + assert c == self.size + return c + + def _count(self, node: _TSTNode, counter: int) -> int: + """Helper method to self.count. + + Time complexity: O(m), where m is the number of nodes under node.""" + if node is None: # Base case. + return counter + + counter = self._count(node.left, counter) + if node.value is not None: + counter += 1 + + counter = self._count(node.mid, counter) + counter = self._count(node.right, counter) + + return counter + + def insert(self, key: str, value: object) -> None: + """Inserts the key into the symbol table and associates with it value, + overwriting an eventual associated old value, if the key is already in + this ternary-search tree. + + If key is not an instance of str, TypeError is raised. + If key is an empty string, ValueError is raised. + If value is None, ValueError is raised. + + Nodes whose value is not None represent the last character of an + inserted word. + + Time complexity: O(m + h), where m = length(key), which also represents + how many times we follow the middle link, and h is the number of left + and right turns. So, a lower bound of the complexity would be Ω(m).""" + assert is_tst(self) + + if not isinstance(key, str): + raise TypeError("key must be an instance of type str.") + if not key: + raise ValueError("key must be a string of length >= 1.") + if value is None: + raise ValueError("value cannot be None.") + self._root = self._insert(self._root, key, value, 0) + + assert is_tst(self) + + def _insert(self, node: _TSTNode, key: str, value: object, index: int) -> _TSTNode: + """Inserts key with value into this TST starting from node.""" + if node is None: + node = _TSTNode(key[index]) + + if key[index] < node.key: + node.left = self._insert(node.left, key, value, index) + node.left.parent = node + elif key[index] > node.key: + node.right = self._insert(node.right, key, value, index) + node.right.parent = node + else: # key[index] == node.key + if index < len(key) - 1: + # If we are not at the end of the key, this is a match, so we + # recursively call self._insert from index + 1, and we move to + # the mid node (char) of node. + # + # Note: the last index of the key is len(key) - 1. + node.mid = self._insert(node.mid, key, value, index + 1) + node.mid.parent = node + else: + if node.value is None: + self._n += 1 + node.value = value + + return node + + def search(self, key: str) -> object: + """Returns the value associated with key, if key is in this TST, else + None. + + If key is not an instance of str, TypeError is raised. + If key is an empty string, ValueError is raised. + + The search in a TST works as follows. + + We start at the root and we compare its character with the first + character of key. + + - If they are the same, we follow the middle link of the root node. + + - If the first character of key is smaller lexicographically than + the key at the root, then we take the left link or pointer. + + We do this because we know that all strings that start with + characters that are smaller lexicographically than key[0] are on its + left subtree. + + - If the first character of key is greater lexicographically + than the key at the root, we take similarly the right link. + + We keep applying this idea at every node. + + Moreover, when there is a match, next time we compare the key of the + next node with the next character of key. + + For example, if there's a match between the first node (the root) and + key[0], we follow the middle link, and the next comparison is between + the key of the specific next node and key[1] (not key[0]). + + Time complexity: O(m + h). Check self.insert to see what m and h are.""" + if not isinstance(key, str): + raise TypeError("key must be an instance of type str.") + if not key: + raise ValueError("key must be a string of length >= 1.") + + node = self._search(self._root, key, 0) + + if node is not None: + assert self.search_iteratively(key) == node.value + return node.value + assert self.search_iteratively(key) is None + return None + + def _search(self, node: _TSTNode, key: str, index: int) -> _TSTNode: + """Searches for the node containing the value associated with key + starting from node. + + If returns None or a node with value None if there's no such node.""" + if node is None: + return None + + if key[index] < node.key: + return self._search(node.left, key, index) + if key[index] > node.key: + return self._search(node.right, key, index) + if index < len(key) - 1: + # This is a match, but we are not at the last character of key. + return self._search(node.mid, key, index + 1) + # This is a match, and we are at the last character of key. + return node # node could be None!! + + # pylint: disable=too-many-branches + def search_iteratively(self, key: str) -> object: + """Iterative alternative to self.search.""" + if not isinstance(key, str): + raise TypeError("key must be an instance of type str.") + if not key: + raise ValueError("key must be a string of length >= 1.") + + node = self._root + + if node is None: + return None + + # Up to the penultimate index (i.e. len(key) - 1), because if we reach + # the penultimate character and it's a match, then we follow the mid + # node (i.e. we end up in what's possibly the last node). + index = 0 + + while index < len(key) - 1: + while node and key[index] != node.key: + if key[index] < node.key: + node = node.left + else: + node = node.right + + if node is None: # Unsuccessful search. + return None + + # Arriving here only if exited from the while loop because the + # condition key[i] != node.key was false, that is + # key[index] == node.key, thus we follow the middle link. + node = node.mid + index += 1 + + assert index == len(key) - 1 + + # If node is not None, then we may still need to go left or right, and + # we stop when either we find a node which has the same key as the last + # character of key, or when node ends up being set to None, i.e. the key + # does not exist in this TST. + while node and key[index] != node.key: + if key[index] < node.key: + node = node.left + else: + node = node.right + + if node is None: # Unsuccessful search. + return None + # We exit the previous while loop because key[index] == node.key. + return node.value # This can be None!! + + def contains(self, key: str) -> bool: + """Returns true if key is in this TST, False otherwise. + + Time complexity: O(m + h). See the complexity analysis of self.insert + for more info about m and h.""" + return self.search(key) is not None + + def delete(self, key: str) -> object: + """Deletes and returns the value associated with key in this TST, if key + is in this TST, otherwise it returns None. + + If key is not an instance of str, TypeError is raised. + If key is an empty string, ValueError is raised. + + Time complexity: O(m + h + k). Check self.search to see what m and h + are. k is the number of "no more necessary" cleaned up after deletion of + the node associated with key. Unnecessary nodes are nodes with no + children and value equal to None.""" + assert is_tst(self) + + if not isinstance(key, str): + raise TypeError("key must be an instance of type str.") + if not key: + raise ValueError("key must be a string of length >= 1.") + + # Note: calling self._search, since self.search does not return a Node, + # but the value associated with the key passed as parameter. + node = self._search(self._root, key, 0) + + if node is not None and node.value is not None: + result = node.value # Forget the string tracked by node. + node.value = None + self._n -= 1 + self._delete_fix(node) + else: + result = None + + assert is_tst(self) + + return result + + def _delete_fix(self, u: _TSTNode) -> None: + """Does the clean up of this TST after deletion of node u.""" + assert u.value is None + + # While u has no children and his value is None, forget about u and + # start from his parent. So, this while loop terminates when either u is + # None, u has at least one child, or u's value is not None. + while u and not u.has_children() and u.value is None: + if self._is_root(u): + assert self._n == 0 + self._root = None + break + + if u.is_left_child(): + u.parent.left = None + elif u.is_right_child(): + u.parent.right = None + else: + u.parent.mid = None + + p = u.parent + u.parent = None + u = p + + if u.has_children() and u.value is None: + assert self._count(u, 0) > 0 + + def traverse(self) -> None: + """Traverses all nodes in this TST and prints the key: value + associations. + + Time complexity: O(n), where n is the number of nodes in self.""" + self._traverse(self._root, "") + + def _traverse(self, node: _TSTNode, prefix: str) -> None: + """Helper method to self.traverse. + + Time complexity: O(m), where m is the number of nodes under node.""" + if node is None: # Base case. + return + + self._traverse(node.left, prefix) + if node.value is not None: + print(prefix + node.key, ": ", node.value) + + self._traverse(node.mid, prefix + node.key) + self._traverse(node.right, prefix) + + def keys_with_prefix(self, prefix: str) -> list: + """Returns all keys in this TST that start with prefix. + + If prefix is not an instance of str, TypeError is raised. + + If prefix is an empty string, then all keys in this TST that start with + an empty string, thus all keys are returned.""" + if not isinstance(prefix, str): + raise TypeError("prefix must be an instance of str!") + + kwp = [] + + if not prefix: + self._keys_with_prefix(self._root, [], kwp) + else: + node = self._search(self._root, prefix, 0) + + if node is not None: + if node.value is not None: + # A key equals to prefix was found in the TST with an + # associated value. + kwp.append(prefix) + + self._keys_with_prefix(node.mid, list(prefix), kwp) + + return kwp + + def _keys_with_prefix(self, node: _TSTNode, prefix_list: list, kwp: list) -> None: + """Returns all keys rooted at node given the prefix given as a list of + characters prefix_list.""" + if node is None: + return + + self._keys_with_prefix(node.left, prefix_list, kwp) + + if node.value is not None: + kwp.append("".join(prefix_list + [node.key])) + + prefix_list.append(node.key) + self._keys_with_prefix(node.mid, prefix_list, kwp) + + prefix_list.pop() + self._keys_with_prefix(node.right, prefix_list, kwp) + + def all_pairs(self) -> dict: + """Returns all pairs of (key: value) from this TST as a Python dict.""" + pairs = {} + self._all_pairs(self._root, [], pairs) + return pairs + + def _all_pairs(self, node: _TSTNode, key_list: list, all_dict: list) -> None: + if node is None: + return + + self._all_pairs(node.left, key_list, all_dict) + + if node.value is not None: + key = "".join(key_list + [node.key]) + assert key not in all_dict + all_dict[key] = node.value + + key_list.append(node.key) + self._all_pairs(node.mid, key_list, all_dict) + + key_list.pop() + self._all_pairs(node.right, key_list, all_dict) + + def longest_prefix_of(self, query: str) -> str: + """Returns the key in this TST which is the longest prefix of query, if + such a key exists, else it returns None. + + If query is not a string TypeError is raised. + If query is a string but empty, ValueError is raised. + + If this TST is empty, it returns an empty string.""" + if not isinstance(query, str): + raise TypeError("query is not an instance of str!") + if not query: + raise ValueError("empty strings not allowed in this TST!") + + # It keeps track of the length of the longest prefix of query. + length = 0 + + x = self._root + i = 0 + + while x is not None and i < len(query): + c = query[i] + + if c < x.key: + x = x.left + elif c > x.key: + x = x.right + else: + i += 1 + if x.value is not None: + length = i + x = x.mid + + return query[:length] + + def keys_that_match(self, pattern: str) -> list: + """Returns a list of keys of this TST that match pattern. + + A key k of length m matches pattern if: + + 1. m = length(pattern), and + 2. Either k[i] == pattern[i] or k[i] == '.'. + + - Example: if pattern == ".ood", then k == "good" would match, but + not k == "foodie". + + If pattern is not a str, TypeError is raised. + If pattern is an empty string, ValueError is raised.""" + if not isinstance(pattern, str): + raise TypeError("pattern is not an instance of str!") + if not pattern: + raise ValueError("pattern cannot be an empty string") + + keys = [] + self._keys_that_match(self._root, [], 0, pattern, keys) + return keys + + # pylint: disable=too-many-arguments + def _keys_that_match( + self, node: _TSTNode, prefix_list: list, i: int, pattern: str, keys: list + ) -> None: + """Stores in the list keys the keys that match pattern starting from + node.""" + if node is None: + return + + c = pattern[i] + + if c == "." or c < node.key: + self._keys_that_match(node.left, prefix_list, i, pattern, keys) + + if c == "." or c == node.key: # pylint: disable=consider-using-in + + if i == len(pattern) - 1 and node.value is not None: + # If i is the last index and its value is not None. + keys.append("".join(prefix_list + [node.key])) + + if i < len(pattern) - 1: + prefix_list.append(node.key) + self._keys_that_match(node.mid, prefix_list, i + 1, pattern, keys) + prefix_list.pop() + + if c == "." or c > node.key: + self._keys_that_match(node.right, prefix_list, i, pattern, keys) + + +# pylint: disable=protected-access +def is_tst(t: TST) -> bool: + """These propositions should always be true at the BEGINNING + and END of every PUBLIC method of this TST. + + Call this method if you want to ensure the invariants are holding.""" + if not isinstance(t, TST): + return False + if t._n < 0: + return False + if t._n == 0: + return t._root is None + if not isinstance(t._root, _TSTNode) or t._root.parent is not None: + return False + return True diff --git a/andz/ds/__init__.py b/andz/ds/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/andz/ds/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/docs/big_o.md b/docs/big_o.md new file mode 100644 index 00000000..73601b65 --- /dev/null +++ b/docs/big_o.md @@ -0,0 +1,56 @@ +# [Big-O](https://en.wikipedia.org/wiki/Big_O_notation) + +Big-O notation is used to indicate the _computational complexity_ of an algorithm, both in terms of + +- number of operations (_time complexity_), and +- space/memory (_space complexity_) + +with respect to the input _size_ (or length), _in the limit_, i.e. as the input size/length grows to infinity, i.e. _asymptotically_. + +For example, if we say that an algorithm has time complexity `O(n)`, then it roughly means that there's a _linear_ function that _upper bounds_ the time complexity of our algorithm. So, the time required to run the algorithms grows linearly with the size of the input. Here, `n` depends on the algorithm and how we want to analyse our algorithm. For example, if you want to know how much time you will need to sort a list, as the number of elements grows to infinity, then `n` is the number of elements in the list. + +The complexity can also be given as a function of the _output size_ (e.g. for output-sensitive algorithms) or other parameters (e.g. number of layers in a neural network). + +Note that the computational complexity is usually expressed asymptotically. This is useful when you think of how your algorithm would scale as a function of the input/output or other parameters. However, in practice, if you know e.g. that the input size to your algorithm does not change, then you might consider (more) the _running time_ (e.g. measured in minutes) and actual memory that your algorithm takes and uses, respectively. + +We can analyse the time/space complexity of an algorithm in the _best_, _average_ and _worst cases_. Moreover, it can be given as _lower_ bound, an _upper bound_, or both. + +These 2 concepts are orthogonal. For example, you can provide the computational complexity for the average case as a lower bound, or maybe in the worst case as lower and upper bound. + +## Best, average and worst cases + +### Example: sorting + +- Best case: array is already sorted +- Average case: it's the average time across "many" possible inputs +- Worst case: it depends on the sorting algorithm, but it would be the case where you perform the maximal number of operations; so it could be e.g. when the array is completely reversed + +## Lower, lower and upper, and upper bounds + +- lower bounds (Omega/`Ω`) +- lower and upper bounds (Theta/`Θ`), i.e. we bound the complexity of our algorithm from above and below +- upper bounds (`O`) + +## Common complexities + +- constant time/space: `Ω(1)`, `Θ(1)`, and `O(1)` +- linear time/space: `Ω(n)`, `Θ(n)`, and `O(n)` +- logarithmic time/space: `Ω(log(n))`, `Θ(log(n))`, and `O(log(n))` +- "n log n" time/space: `Ω(n*log(n))`, `Θ(n*log(n))`, and `O(n*log(n))` +- quadratic time/space: `Ω(n²)`, `Θ(n²)`, and `O(n²)` +- cubic time/space: `Ω(n³)`, `Θ(n³)`, and `O(n³)` +- exponential time/space: `Ω(2ⁿ)`, `Θ(2ⁿ)`, and `O(2ⁿ)` + +## Terminology + +Sometimes, rather than saying that the complexity of an algorithm is e.g. `O(n)`, people will say that it's "linear" or "n". Of course, in this case, they are not specifying whether it's a lower or upper bound, or both, but, in many cases, this description is sufficient. + +## Importance + +It's important to have an idea of the time and space complexity of the most commonly used algorithms because the performance of our programs highly depends on the computational complexity. For example, you should know that there's a lower bound for comparison-based sorting (i.e. not comparison-based sorting can do better than this lower bound), or that searching in a balanced binary search tree takes "log n" time. It's also important to know how to analyse the complexities of your own algorithms, to understand if you can improve them or not. + +## Resources + +- https://www.khanacademy.org/computing/computer-science/algorithms/asymptotic-notation/a/asymptotic-notation +- https://en.wikipedia.org/wiki/Big_O_notation +- CLRS book \ No newline at end of file diff --git a/docs/contributors.md b/docs/contributors.md new file mode 100644 index 00000000..3ebff200 --- /dev/null +++ b/docs/contributors.md @@ -0,0 +1,3 @@ +# Contributors + +- Nelson Brochado ([nbro](https://github.com/nbro)), original and only developer so far \ No newline at end of file diff --git a/docs/general.md b/docs/general.md new file mode 100644 index 00000000..547553e2 --- /dev/null +++ b/docs/general.md @@ -0,0 +1,160 @@ +# General Guidelines + +## Git Branches + +- There are 2 types of branches: + - `master`: it contains the code in production + - feature branches or branches that fix bugs +- Do not introduce features that are **incomplete** or **not tested** to the `master`. + +## GitHub Issue Tracker + +You can use the [issue tracker](https://github.com/nbro/ands/issues) to + +- report an issue or bug +- suggest improvements or request a new feature/implementation +- ask a question about the project + +## Software Development + +- Expose only the public interfaces. For example, clients of a `BST` may not care about the existence of a `_BSTNode`, which is only useful to store information about the keys. + +## Comments + +- Modules' doc-strings should contain the + - author name, + - the creation date, + - the last update date, + - a description of module, + - references used to implement the module (if any), and eventually + - links to resources talking about the topic + +## Functions + +- Functions should have a doc-string comment containing: + - A description of the purpose of the function + - Assumptions about parameters and return values (optional, if using assertions for this!) + - Complexity analysis of the algorithm + +## Testing + +- Test at least the public interface +- All statements should be covered, though +- Unit tests should test only one feature or function + - Unit tests should be short + - Signature of unit test methods should be descriptive + - For example, if you wrote a data structure `Graph`, there should not just be a single test function `test_bst`, but instead there should be a test function for each method in `BST`. + +## Type Hints + +- Use type hints (both for parameters and return values) as a form of documenting the code and making sure that we're not passing unexpected inputs to functions or returning unexpected outputs. + +## Parameters + +- Use `isinstance` if + - the function supports more than one type for a certain parameter, and + - you need to decide between portions of code to execute + +## Names + +- Names should be as explicit and descriptive as possible + - If abbreviated to improve readability, they should have an associated comment explaining its purpose. + +## Complexity Analysis + +Use symbols O, Θ, Ω, α, ... to specify the time and space complexity of algorithms. + +## Design by Contract + +- Consists in asserting: + + - _preconditions_, + - _invariants_, and + - _postconditions_ + +- _Use assertions to ensure preconditions, invariants and postconditions_ + + > An assertion is instead a correctness condition governing the relationship between **two software modules** (not a software module and a human, or a software module and an external device). + + > If `sum` is _negative_ on entry to `deposit`, violating the precondition, the culprit is some other software element, whose author was not careful enough to observe the terms of the deal. + + - Essentially, if a programmer of a function `A` establishes a contract (which, apart from the assertion statements, could also be specified as a comment of the same function) with the world about, for example, a certain input `x`'s range, then he can assume whoever is going to use `A` is going to pass a correct value (in the acceptable range) for `x`. So, in this case, we should not check for the input `x`'s correctness (using for example `try .. catch` constructs), but we should use assertions. + + - Why? Assertions are used to check what **should never happen**! + + - We should also use assertions if the programmer of a certain function `A` ensures to the world that the function is going to maintain a certain invariant or postcondition, but he breaks it because of a logical error. + + > **Rule - Assertion Violation**: _A run-time assertion violation is the manifestation of a bug_. + + > To be more precise: + + > - A _precondition violation_ signals a bug in the client, which did not observe its part of the deal. + > - A _postcondition (or invariant) violation_ signals a bug in the supplier -- the routine -- which did not do its job. + + > That violations indicate bugs explains why it is legitimate to enable or disable assertion monitoring through mere compilation options: for a correct system -- one without bugs -- assertions will always hold, so the compilation option makes no difference to the semantics of the system. + +### Questions + +1. Should we establish contracts between a certain function `f` and a user `U`? + + - I think the answer depends (but not exclusively) on what `f` is intended to be used by, that is + - if the users of `f` are intended to be programmers, then we should establish contracts + + - if the users of `f` are real users, then usually the real user doesn't care about the implementation of `f`, and therefore exceptions should be raised. + + 1. We don't know 100% sure that only a certain type of user is going to use `f`, so what should we do in these cases? + + - The intended clients of `f` should be one of the first things specified in the documentation of the software. **We can't protect users that do respect the rules of the software**!!! + +### References + +- [ET: Design by Contract (tm), Assertions and Exceptions](https://www.eiffel.org/doc/eiffel/ET%3A%20Design%20by%20Contract%20(tm),%20Assertions%20and%20Exceptions) + +## Assertions + +- Use assertions for something that **should never happen**: + + - Use assertions in code **to catch implementation errors** (before releasing)!!! + + - Use assertions for preconditions, invariants and postconditions. + + - Preconditions may also be explicitly stated assumptions about the inputs. + + - Since these things should never happen, then in the release mode, assertions can be disable to speed up computations. + +### References + +- [http://stackoverflow.com/questions/1957645/when-to-use-an-assertion-and-when-to-use-an-exception](http://stackoverflow.com/questions/1957645/when-to-use-an-assertion-and-when-to-use-an-exception) + +- [http://stackoverflow.com/questions/117171/design-by-contract-using-assertions-or-exceptions](http://stackoverflow.com/questions/117171/design-by-contract-using-assertions-or-exceptions) + +## Exceptions + +- Use exceptions for something that **may** happen + + - Use exceptions to check correctness of input arguments to functions, if there are no assumptions about the same inputs. If there are assumptions, use assertions instead! + + - When to make assumptions??? + + - The level of paranoia to check for correctness of inputs may depend on a few factors: + + - readability of the code + + - robustness of the software + + - efficiency of the software + + - cost and consequences of erroneous behaviour + + - Use exceptions to handle possible erroneous behaviour after computation?? Why??? + + - Examples: division by zero .. ?? + + +### References + +- [http://stackoverflow.com/questions/117171/design-by-contract-tests-by-assert-or-by-exception](http://stackoverflow.com/questions/117171/design-by-contract-tests-by-assert-or-by-exception) + +- [http://softwareengineering.stackexchange.com/questions/125399/differences-between-design-by-contract-and-defensive-programming](http://softwareengineering.stackexchange.com/questions/125399/differences-between-design-by-contract-and-defensive-programming) + +- [http://www.engr.mun.ca/~theo/Courses/sd/5895-downloads/sds-spec-1.ppt.pdf](http://www.engr.mun.ca/~theo/Courses/sd/5895-downloads/sds-spec-1.ppt.pdf) diff --git a/docs/history.md b/docs/history.md new file mode 100644 index 00000000..2a132152 --- /dev/null +++ b/docs/history.md @@ -0,0 +1,7 @@ +# History + +This project was created for _personal use_ mostly while studying for an _exam_ (starting in the month of June in 2015) of a previous course that I followed called _Algorithms and Data Structures_. + +I decided to make it publicly available to use and modify, so that people with difficulties in understanding and applying these topics can take benefit from it. + +I discourage every beginner from copying **shamelessly** the source code, but instead you should definitely give a chance to your brain and sense of challenge first! At the end, you will definitely feel a better and more serious programmer! If you really do not have any ideas on how to do something, try to read the comments next to each function and/or class (or even the code itself) that you are interested in. They are there for a reason! diff --git a/docs/how_to_develop.md b/docs/how_to_develop.md new file mode 100644 index 00000000..ccff83e3 --- /dev/null +++ b/docs/how_to_develop.md @@ -0,0 +1,37 @@ +# How to develop? + +If you want to develop a new feature or fix a bug, here are the steps. + +1. Clone the repository `git clone git@github.com:nbro/andz.git` +2. Checkout `master` with `git checkout master` +3. Make sure it's up-to-date: `git pull` +4. Create the feature branch: `git checkout -b ` +5. Make the changes that you want +6. Run the checks and tests: `make check test` +7. Add your changes e.g. with `git add -u` or `git add .` +8. Commit your changes with a commit message that starts with a verb (e.g. `git commit -m "Update the README"`) +9. Push your changes to the remote: `git push --set-upstream origin ` +10. Make a pull request (PR) +11. Ask for code review (if there's another developer) +12. Once the PR is approved, merge your feature branch into `master` + +## How to run the tests? + +To run all tests, you can do + +``` +make test +``` +To run all the tests under e.g. `tests/algorithms/sorting/integer`, use + +``` +poetry run coverage run --source=. -m unittest discover -s tests/algorithms/sorting/integer -v +``` + +Or run a specific test + +``` +poetry run coverage run --source=. -m unittest tests/ds/test_MinMaxHeap.py -v +``` + +> See the `Makefile` for more info about other commands. \ No newline at end of file diff --git a/docs/how_to_release.md b/docs/how_to_release.md new file mode 100644 index 00000000..e04bf692 --- /dev/null +++ b/docs/how_to_release.md @@ -0,0 +1,19 @@ +# How to Release? + +> We use [semantic versioning](https://semver.org/). + +Once there are enough changes in `master`, we can release a new version + +1. Checkout the development branch: `git checkout master` +2. Make sure it's up-to-date with the remote: `git pull` +3. Create the release branch: `git checkout -b rel-x.y.z`, where `x.y.z` is the new version of the package. +4. Set the new version: `poetry version x.y.z`. +5. Add these changes: `git add -u` +6. Commit these changes: `git commit -m "Bump andz version to x.y.z"` +7. Push them to the remote: `git push --set-upstream origin rel-x.y.z` +8. Create a PR and ask for code review +10. Once the PR has been approved, merge `rel-x.y.z` into `master` +11. Create a new tag `git tag x.y.z` +12. Push it to the remote `git push origin --tags`, which triggers the GitHub workflow that releases the package to PyPI + + [1]: https://github.com/nbro/andz/releases/new \ No newline at end of file diff --git a/docs/objectives.md b/docs/objectives.md new file mode 100644 index 00000000..1573e6a0 --- /dev/null +++ b/docs/objectives.md @@ -0,0 +1,69 @@ +# Objectives + +1. Learn by implementing the following (but not exclusively) concepts: + + - Trees + - Graphs + - Heaps + - Sets + - Searching + - Sorting + - Hashing + - Dynamic programming + - Greedy algorithms + - Cryptographic algorithms + - Parsing + +2. Implement as many as possible of the most interesting and useful algorithms and data structures in the fields mentioned above (but not exclusively). + + - "Interesting" here can be from the implementation or concepts' point of view. + + - "Usefulness", instead, is especially with respect to me (since so far this is a personal project), but also with respect to any student or person involved in one of the fields mentioned above. + + - Implementation is performed in a very high-level and user-friendly programming language: Python. + + - No optimisation of the implementations. This doesn't mean at all that I won't always try to implement the best algorithm for the task (or operation), but simply that I will not introduce tricks of any sort to make the implementation faster. + +3. Learn to analyse the asymptotic complexity of the algorithms and in particular of their implementation. + +4. Prefer quality (i.e., completeness, correctness) and clarity over quantity of the work done. + + - Consequences: + + - Rate of implementation of new algorithms and data structures may be slow. + + - Greater reliability of the implementations and information given. + + - May serve as starting point to implement a high-performance version of a specific data structure, maybe in another programming language. + + - Specific goals: + + - Describe clearly the concepts, as if it was an explanation to someone who has almost no experience or knowledge of what's being described, without sacrificing rigor. + + - Add comments where appropriate with respect to the previous goal. + + - Specify exactly the coding (and naming) conventions being used. + + - Specify exactly how the concept has been implemented. Try to emphasize the difference between the abstract concept and the specific implementation using specific techniques, so that the user does not confuse the two. + + - Every method should have an asymptotic analysis associated with it. This analysis should obviously be related to the specific implementation and this should be noted. + + - Link to resources used to implement the concepts. + +5. Learn how to create unit tests. Specifically, I should at least consider the following techniques: + + - Equivalence partitioning + + - Boundary values + + - Statement coverage + +6. Learn new programming techniques, tools and paradigms: + + - Learn to decide which tools are best to do a job + + - Learn which paradigm is better for a particular implementation + + - Learn test-driven development + +7. Learn new useful Python techniques diff --git a/docs/resources.md b/docs/resources.md new file mode 100644 index 00000000..1ce2967c --- /dev/null +++ b/docs/resources.md @@ -0,0 +1,20 @@ +## Resources + +For each module, I always try not to forget to specify the specific references that I used to implement the particular concept exposed in that module. + +Apart from those, the following are the references which I always keep an eye on: + +- [_Introduction to Algorithms_ (3rd ed.)](https://mitpress.mit.edu/books/introduction-algorithms), book by Cormen, Leiserson, Rivest, Stein + +- [Algorithms, 4th Edition](http://algs4.cs.princeton.edu/home/), online book by Robert Sedgewick and Kevin Wayne + +Additionally, there are many useful resources around the web to help you (and me) understand how certain algorithms or data structures work. Examples are + +- [The Archive of Interesting Code](http://www.keithschwarz.com/interesting/) +by Keith Schwarz + +- [Notes on Data Structures and Programming Techniques](https://www.cs.yale.edu/homes/aspnes/classes/223/notes.html) + +- [https://github.com/tayllan/awesome-algorithms](https://github.com/tayllan/awesome-algorithms) + +- [Rosetta Code](http://rosettacode.org/wiki/Rosetta_Code) \ No newline at end of file diff --git a/docs/typical_se_and_python_issues.md b/docs/typical_se_and_python_issues.md new file mode 100644 index 00000000..68d76af0 --- /dev/null +++ b/docs/typical_se_and_python_issues.md @@ -0,0 +1,61 @@ +# Typical Python and Software Development Issues + +_This is a schematic and concise list of the things for which I should constantly be prepared to fix or improve_. + +#### Make sure that... + +1. **_default mutable values (like lists) end up acting as expected_** + + - Default parameter values seem to be treated as static + + def foo(a=[]): + a.append(1) + return a + + foo() # returns [1] + foo() # returns [1, 1] + + - This is one of the features that I hate more about Python!!! + - See: + - [http://effbot.org/zone/default-values.htm](http://effbot.org/zone/default-values.htm) + - [http://stackoverflow.com/questions/4841782/python-constructor-and-default-value](http://stackoverflow.com/questions/4841782/python-constructor-and-default-value) + +2. I'm using `X is None` or `X is not None` in conditions, when I really mean that `X` should be respectively `None` or `not None`. In other words, I should not be using `X` or `not X`, because `X` could be 0, and could conceptually still _contain_ a valid value. + +3. I'm raising the most appropriate exception for each specific anomaly. + +4. exceptions raised are consistent inside each module: + + - the same exception should be raised when the "same" error occurs in different methods or even within the same method! + +5. unit tests + + - are not redundant + + - cover all partitions + + - cover all statements + + - cover all boundary values + + - are descriptive + + - test only one feature + + - are short + +6. citations to resources used to implement an algorithm or data structure ar provided at the beginning of each module. + +7. the doc-strings of the files containing each data structure or algorithm contain a (good) introduction to what's being implemented. + +8. I use the same naming conventions throughout the modules. + +## Resources + +The following are some resources talking about how we should (or not) use Python. They also provide descriptions and examples of common mistakes that people do using Python. + +- [Idioms and Anti-Idioms in Python](https://docs.python.org/2/howto/doanddont.html) + +- [Common pitfalls in Python](http://stackoverflow.com/questions/1011431/common-pitfalls-in-python) + +- [Common Gotchas](https://docs.python-guide.org/writing/gotchas) \ No newline at end of file diff --git a/docs/warnings.md b/docs/warnings.md new file mode 100644 index 00000000..b8361ded --- /dev/null +++ b/docs/warnings.md @@ -0,0 +1,8 @@ +# Warnings + +- This is mostly a **personal project**, but I will accept PRs that fix bugs or introduce nice features +- This is a **work in progress**, so don't expect to find here all the algorithms and data structures you are searching. +- **Mistakes in the implementations are possible**, even if I always try to test all the algorithms and data structures and I do some research before implementing them. You can find the unit tests under the folder [`tests`](../tests). So, as the [license](../LICENSE.md) says, this project is provided "as is". +- **No optimisation** has been done to any algorithm or data structure. The purpose of the implementations is just for **_exposition of the concepts_**. However, note that this does not mean that I don't strive to implement the most efficient known version of the algorithm. +- My intent is to continue to contribute to this repository in my free time, and **new data structures and algorithms** will therefore be added. +- The history of this repository was squashed into one commit the 27/12/2022 in order to remove all noisy commits. From now on, each commit should change one specific thing, which doesn't mean one specific file or function, as certain changes may need to be applied to different functions or files in order to make sense. The `main_27_12_2022` is a copy of the original branch that was squashed, which had already been rebased in order to clean up some commits. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..0ed5e0c9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,67 @@ +[tool.poetry] +name = "andz" +version = "0.1.0" +description = "Algorithms and Data Structures" +authors = ["nbro "] +license = "MIT" +repository = "https://github.com/nbro/andz" +readme = "README.md" + +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", +] + +[tool.poetry.dependencies] +python = "^3.9" +numpy = "^1.24.1" +tabulate = "^0.9.0" + +[tool.poetry.group.dev.dependencies] +black = "^24.2.0" +isort = "^5.13.2" +pylint = "^3.1.0" +mypy = "^1.9.0" +coverage = "^7.4.3" +scipy = "^1.12.0" +pytest = "^8.1.1" + +[tool.pytest.ini_options] +#log_cli = true +log_cli_level = "DEBUG" +log_cli_format = "%(asctime)s [%(levelname)s] %(message)s (%(filename)s:%(lineno)s)" +log_cli_date_format = "%d-%m-%Y %H:%M:%S" + +[tool.coverage] +run.omit = ["*tests*"] +report.show_missing = true +report.skip_covered = true +report.fail_under = 95 # TODO: change this to 100 + +[tool.pylint."REPORTS"] +output-format = "colorized" + +[tool.pylint."MESSAGES CONTROL"] +disable = ["fixme", "invalid-name"] + +[tool.isort] +atomic = true +profile = "black" + +[tool.black] +target-version = ["py39"] + +[tool.mypy] +python_version = 3.9 +warn_return_any = true + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/setup.py b/setup.py new file mode 100755 index 00000000..b54ceb0b --- /dev/null +++ b/setup.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# A setup.py for editable installations. + +from setuptools import setup + +if __name__ == "__main__": + setup() diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..e928f603 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,3 @@ +# Tests + +Tests for all algorithms and data structures. \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/tests/algorithms/__init__.py b/tests/algorithms/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/tests/algorithms/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/tests/algorithms/crypto/__init__.py b/tests/algorithms/crypto/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/tests/algorithms/crypto/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/tests/algorithms/crypto/test_caesar.py b/tests/algorithms/crypto/test_caesar.py new file mode 100755 index 00000000..61105639 --- /dev/null +++ b/tests/algorithms/crypto/test_caesar.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 01/01/2017 + +Updated: 04/08/2018 + +# Description + +Unit tests for the functions in the andz.algorithms.crypto.caesar module. +""" + +import unittest +from random import randint + +from andz.algorithms.crypto.caesar import ( + MAX_MAPPED_INT, + decrypt, + decrypt_with_multiple_keys, + encrypt, + encrypt_with_multiple_keys, +) +from tests.algorithms.crypto.util import ( + find_max_char_ord_value, + gen_rand_keys, + generate_random_string, +) + + +class TestCaesarCipher(unittest.TestCase): + def template_test_one_key(self, n, size): + """n is the number of iterations. + size is the size of the message.""" + for _ in range(n): + m = generate_random_string(size) + key = randint(1, MAX_MAPPED_INT - find_max_char_ord_value(m)) + cipher = encrypt(m, key) + o = decrypt(cipher, key) + self.assertEqual(m, o) + + def template_test_multi_keys(self, n, size, total_keys): + for _ in range(n): + m = generate_random_string(size) + keys = gen_rand_keys( + total_keys, 1, MAX_MAPPED_INT - find_max_char_ord_value(m) + ) + cipher, pattern = encrypt_with_multiple_keys(m, keys) + o = decrypt_with_multiple_keys(cipher, pattern) + self.assertEqual(m, o) + + def test_empty_message(self): + for i in range(100): + m = "" + cipher = encrypt(m, i) + o = decrypt(cipher, i) + self.assertEqual(m, o) + + def test_encrypt_and_decrypt_size_1(self): + self.template_test_one_key(1000, 1) + + def test_encrypt_and_decrypt_random_size(self): + it = randint(3, 13) + size = randint(10, 1000) + self.template_test_one_key(it, size) + + def test_multi_encrypt_decrypt_size_one_key(self): + # Equivalent to Caesar because we just 1 key + self.template_test_multi_keys(1000, 1, 1) + + def test_multi_encrypt_decrypt_size_random_keys(self): + keys = randint(3, 7) + self.template_test_multi_keys(100, 1, keys) + + def test_multi_encrypt_decrypt_random(self): + """Random number of iterations, random length of message + and random number of keys.""" + it = randint(3, 13) + size = randint(10, 1000) + keys = randint(3, 11) + self.template_test_multi_keys(it, size, keys) diff --git a/tests/algorithms/crypto/test_one_time_pad.py b/tests/algorithms/crypto/test_one_time_pad.py new file mode 100755 index 00000000..717b9313 --- /dev/null +++ b/tests/algorithms/crypto/test_one_time_pad.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 01/01/2017 + +Updated: 04/08/2018 + +# Description + +Unit tests for the functions in the andz.algorithms.crypto.one_time_pad module. +""" + +import unittest +from random import randint + +from andz.algorithms.crypto.one_time_pad import decrypt, encrypt +from tests.algorithms.crypto.util import generate_random_string + + +class TestOneTimePad(unittest.TestCase): + def template_test(self, n, m): + """m is the size of the string and key. + n is the number of iterations.""" + for _ in range(n): + message = generate_random_string(m) + key = generate_random_string(m) + cipher_text = encrypt(message, key) + original = decrypt(cipher_text, key) + self.assertEqual(original, message) + + def test_empty_message(self): + self.template_test(1000, 0) + + def test_size_1(self): + self.template_test(1000, 1) + + def test_size_greater_than_1(self): + self.template_test(1000, 100) + + def test_random_size(self): + it = randint(3, 11) + size = randint(10, 1000) + self.template_test(it, size) diff --git a/tests/algorithms/crypto/util.py b/tests/algorithms/crypto/util.py new file mode 100755 index 00000000..ea748110 --- /dev/null +++ b/tests/algorithms/crypto/util.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import random +import string + + +def generate_random_string(size): + return "".join(random.choice(string.printable) for _ in range(size)) + + +def gen_rand_keys(size, _min, _max): + return [random.randint(_min, _max) for _ in range(size)] + + +def find_max_char_ord_value(message: str): + return max(ord(c) for c in message) diff --git a/tests/algorithms/dac/__init__.py b/tests/algorithms/dac/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/tests/algorithms/dac/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/tests/algorithms/dac/test_binary_search.py b/tests/algorithms/dac/test_binary_search.py new file mode 100644 index 00000000..a7038976 --- /dev/null +++ b/tests/algorithms/dac/test_binary_search.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 18/02/2017 + +Updated: 18/02/2017 + +# Description + +Unit tests for the functions in the andz.algorithms.dac.binary_search module. +""" + +import unittest +from random import choice, randint + +from andz.algorithms.dac.binary_search import * + + +class TestBinarySearch(unittest.TestCase): + def test_list_empty(self): + self.assertEqual(linear_search([], 3), -1) + self.assertEqual(binary_search_iteratively([], 3), -1) + self.assertEqual(binary_search_recursively_in_place([], 3), -1) + self.assertFalse(binary_search_recursively_not_in_place([], 3)) + + def test_list_size_1_exists(self): + self.assertEqual(linear_search([3], 3), 0) + self.assertEqual(binary_search_iteratively([3], 3), 0) + self.assertEqual(binary_search_recursively_in_place([3], 3), 0) + self.assertTrue(binary_search_recursively_not_in_place([3], 3)) + + def test_list_size_1_does_not_exist(self): + self.assertEqual(linear_search([3], 5), -1) + self.assertEqual(binary_search_iteratively([3], 5), -1) + self.assertEqual(binary_search_recursively_in_place([3], 5), -1) + self.assertFalse(binary_search_recursively_not_in_place([3], 5)) + + def test_list_size_2_exists_first_place(self): + self.assertEqual(linear_search([3, 5], 3), 0) + self.assertEqual(binary_search_iteratively([3, 5], 3), 0) + self.assertEqual(binary_search_recursively_in_place([3, 5], 3), 0) + self.assertTrue(binary_search_recursively_not_in_place([3, 5], 3)) + + def test_list_size_2_exists_second_place(self): + self.assertEqual(linear_search([3, 5], 5), 1) + self.assertEqual(binary_search_iteratively([3, 5], 5), 1) + self.assertEqual(binary_search_recursively_in_place([3, 5], 5), 1) + self.assertTrue(binary_search_recursively_not_in_place([3, 5], 5)) + + def test_list_size_2_does_not_exist(self): + self.assertEqual(linear_search([3, 5], 7), -1) + self.assertEqual(binary_search_iteratively([3, 5], 7), -1) + self.assertEqual(binary_search_recursively_in_place([3, 5], 7), -1) + self.assertFalse(binary_search_recursively_not_in_place([3, 5], 7)) + + def test_list_random_size_exists(self): + ls = list(range(randint(3, 10000))) + item = choice(ls) + index = ls.index(item) + + self.assertEqual(linear_search(ls, item), index) + self.assertEqual(binary_search_iteratively(ls, item), index) + self.assertEqual(binary_search_recursively_in_place(ls, item), index) + self.assertTrue(binary_search_recursively_not_in_place(ls, item)) + + def test_list_random_size_does_not_exist_upper(self): + upper = 10000 + ls = list(range(randint(3, upper))) + self.assertEqual(linear_search(ls, upper), -1) + self.assertEqual(binary_search_iteratively(ls, upper), -1) + self.assertEqual(binary_search_recursively_in_place(ls, upper), -1) + self.assertFalse(binary_search_recursively_not_in_place(ls, upper)) + + def test_list_random_size_does_not_exist_lower(self): + ls = list(range(randint(3, 10000))) + self.assertEqual(linear_search(ls, -1), -1) + self.assertEqual(binary_search_iteratively(ls, -1), -1) + self.assertEqual(binary_search_recursively_in_place(ls, -1), -1) + self.assertFalse(binary_search_recursively_not_in_place(ls, -1)) diff --git a/tests/algorithms/dac/test_find_extrema.py b/tests/algorithms/dac/test_find_extrema.py new file mode 100644 index 00000000..dbae116e --- /dev/null +++ b/tests/algorithms/dac/test_find_extrema.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 19/02/2017 + +Updated: 19/02/2017 + +# Description + +Unit tests for the functions in the andz.algorithms.dac.find_extrema module. +""" + +import unittest +from random import randint + +from andz.algorithms.dac.find_extrema import * + + +class TestFindExtrema(unittest.TestCase): + def test_list_empty(self): + self.assertIsNone(find_max([])) + self.assertIsNone(find_min([])) + + def test_list_size_1(self): + self.assertEqual(find_max([13]), 13) + self.assertEqual(find_min([13]), 13) + + def test_list_size_2_max_is_first_min_is_second(self): + self.assertEqual(find_max([13, 11]), 13) + self.assertEqual(find_min([13, 11]), 11) + + def test_list_size_2_max_is_second_min_is_first(self): + self.assertEqual(find_max([13, 19]), 19) + self.assertEqual(find_min([13, 19]), 13) + + def test_list_random_size_max_is_first_min_is_last(self): + ls = list(range(randint(3, 1000), -1, -1)) + self.assertEqual(find_max(ls), ls[0]) + self.assertEqual(find_min(ls), ls[-1]) + + def test_list_random_size_max_is_last_min_is_first(self): + ls = list(range(randint(3, 1000))) + self.assertEqual(find_max(ls), ls[-1]) + self.assertEqual(find_min(ls), ls[0]) + + def test_list_random_size_max_and_min_are_somewhere(self): + ls = [randint(-100, 100) for _ in range(3, 1000)] + self.assertEqual(find_max(ls), max(ls)) + self.assertEqual(find_min(ls), min(ls)) diff --git a/tests/algorithms/dac/test_find_peak.py b/tests/algorithms/dac/test_find_peak.py new file mode 100644 index 00000000..297cb3d9 --- /dev/null +++ b/tests/algorithms/dac/test_find_peak.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 10/03/2017 + +Updated: 10/03/2017 + +# Description + +Unit tests for the functions in the andz.algorithms.dac.find_peak module. +""" + +import unittest +from random import randint + +from andz.algorithms.dac.find_peak import find_peak, find_peak_linearly + + +class TestFindPeak(unittest.TestCase): + def test_find_peak_empty_list(self): + self.assertEqual(find_peak([]), -1) + self.assertEqual(find_peak_linearly([]), -1) + + def test_find_peak_size_1(self): + self.assertEqual(find_peak([47]), -1) + self.assertEqual(find_peak_linearly([47]), -1) + + def test_find_peak_size_2(self): + self.assertEqual(find_peak([47, 59]), -1) + self.assertEqual(find_peak_linearly([47, 59]), -1) + + def test_find_peak_size_3_no_peak(self): + self.assertEqual(find_peak([47, 59, 71]), -1) + self.assertEqual(find_peak_linearly([47, 59, 71]), -1) + + def test_find_peak_size_3(self): + self.assertEqual(find_peak([47, 71, 71]), 1) + self.assertEqual(find_peak_linearly([47, 71, 71]), 1) + + def test_find_peak_sorted_list(self): + self.assertEqual(find_peak(list(range(100))), -1) + self.assertEqual(find_peak_linearly(list(range(100))), -1) + self.assertEqual(find_peak(list(range(100, -1, -1))), -1) + self.assertEqual(find_peak_linearly(list(range(100, -1, -1))), -1) + + def test_find_peak_all_elements_are_equal(self): + a = [randint(-100, 100)] * 10 + self.assertNotEqual(find_peak(a), -1) + self.assertNotEqual(find_peak_linearly(a), -1) + + def test_find_peak_specific_case(self): + a = [7, 4, 5, 6, 7, 6] + self.assertEqual(find_peak(a), 4) + self.assertEqual(find_peak_linearly(a), 4) diff --git a/tests/algorithms/dac/test_select.py b/tests/algorithms/dac/test_select.py new file mode 100644 index 00000000..8b2d952e --- /dev/null +++ b/tests/algorithms/dac/test_select.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 10/03/2017 + +Updated: 10/03/2017 + +# Description + +Unit tests for the functions in the andz.algorithms.dac.select module. +""" + +import unittest +from random import randint, randrange, sample + +from andz.algorithms.dac.select import select + + +class TestSelect(unittest.TestCase): + def test_when_empty_list(self): + # No matter which value for k, with an empty list ValueError is always raised. + self.assertRaises(ValueError, select, [], 2) + + def test_when_list_size_1_invalid_k(self): + self.assertRaises(ValueError, select, [3], 1) + self.assertRaises(ValueError, select, [3], -1) + + def test_when_list_size_2_invalid_k(self): + self.assertRaises(ValueError, select, [3, 5], 2) + self.assertRaises(ValueError, select, [3, 5], -1) + + def test_when_list_size_1_k_is_zero(self): + self.assertEqual(select([7], 0), 7) + + def test_when_list_size_2_k_is_zero(self): + self.assertEqual(select([7, 5], 0), 5) + self.assertEqual(select([5, 7], 0), 5) + + def test_when_list_random_size_k_is_zero(self): + a = [randint(-100, 100) for _ in range(randint(3, 100))] + self.assertEqual(select(a, 0), min(a)) + + def test_when_list_random_size_all_elements_equal(self): + x = randint(-100, 100) + a = [x] * randint(1, 100) + self.assertEqual(select(a, randint(0, len(a) - 1)), x) + + def test_when_list_random_size_random_k(self): + a = sample(range(100), 100) + self.assertIn(select(a, randrange(0, len(a))), a) diff --git a/tests/algorithms/dp/__init__.py b/tests/algorithms/dp/__init__.py new file mode 100644 index 00000000..7a04cfe8 --- /dev/null +++ b/tests/algorithms/dp/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/tests/algorithms/dp/test_change_making.py b/tests/algorithms/dp/test_change_making.py new file mode 100644 index 00000000..74235678 --- /dev/null +++ b/tests/algorithms/dp/test_change_making.py @@ -0,0 +1,58 @@ +""" +# Meta-info + +Author: Nelson Brochado + +Created: 04/08/2018 + +Updated: 05/08/2018 + +# Description + +Unit tests for the functions in the andz.algorithms.ds.change_making module. +""" + +import unittest + +from andz.algorithms.dp.change_making import * + + +class TestChangeMaking(unittest.TestCase): + """We test both functions change_making and extended_change_making + together.""" + + def test_no_coins(self): + self.assertRaises(ValueError, change_making, [], 10) + self.assertRaises(ValueError, extended_change_making, [], 10) + + def test_n_is_negative(self): + self.assertRaises(ValueError, change_making, [1, 2, 5], -1) + self.assertRaises(ValueError, extended_change_making, [1, 2, 5], -1) + + def test_some_coin_is_negative(self): + self.assertRaises(ValueError, change_making, [1, 5, -3], 6) + self.assertRaises(ValueError, extended_change_making, [1, 5, -3], 6) + + def test_n_is_zero(self): + self.assertEqual(change_making([7, 2, 5], 0), 0) + self.assertEqual(extended_change_making([7, 2, 5], 0), []) + + def test_n_is_one(self): + # Note: the list of coins doesn't contain 1, but the algorithms assume + # the existence of the denomination 1. + self.assertEqual(change_making([7, 2, 5], 1), 1) + self.assertEqual(extended_change_making([7, 2, 5], 1), [1]) + + def arbitrary_list_of_coins_and_n( + self, coins=(1, 2, 5, 10, 20, 50), n=19, optimal=[2, 2, 5, 10] + ): + # https://www.sciencedirect.com/science/article/pii/S0195669809001292 + self.assertEqual(change_making(coins, n), len(optimal)) + self.assertEqual(sorted(extended_change_making(coins, n)), optimal) + + def test_1(self): + self.arbitrary_list_of_coins_and_n() + + def test_2(self): + # https://www.sciencedirect.com/science/article/pii/S0195669809001292 + self.arbitrary_list_of_coins_and_n((1, 5, 9, 16), 18, [9, 9]) diff --git a/tests/algorithms/dp/test_fibonacci.py b/tests/algorithms/dp/test_fibonacci.py new file mode 100644 index 00000000..b9cf8a45 --- /dev/null +++ b/tests/algorithms/dp/test_fibonacci.py @@ -0,0 +1,72 @@ +""" +# Meta-info + +Author: Nelson Brochado + +Created: 28/03/2022 + +Updated: 28/03/2022 + +# Description + +Unit tests for the functions in the andz.algorithms.ds.fibonacci module. +""" + +import unittest +from random import randint, random + +from andz.algorithms.dp.fibonacci import ( + bottom_up_fibonacci, + memoized_fibonacci, + recursive_fibonacci, +) + + +class TestFibonacci(unittest.TestCase): + def test_input_is_not_integer(self): + n = random() + self.assertRaises(TypeError, recursive_fibonacci, n) + self.assertRaises(TypeError, memoized_fibonacci, n) + self.assertRaises(TypeError, bottom_up_fibonacci, n) + + def test_input_is_negative(self): + n = randint(-1000, -1) + self.assertRaises(ValueError, recursive_fibonacci, n) + self.assertRaises(ValueError, memoized_fibonacci, n) + self.assertRaises(ValueError, bottom_up_fibonacci, n) + + def test_fib_0(self): + self.assertEqual(recursive_fibonacci(0), 0) + self.assertEqual(memoized_fibonacci(0), 0) + self.assertEqual(bottom_up_fibonacci(0), 0) + + def test_fib_1(self): + self.assertEqual(recursive_fibonacci(1), 1) + self.assertEqual(memoized_fibonacci(1), 1) + self.assertEqual(bottom_up_fibonacci(1), 1) + + def test_fib_2(self): + self.assertEqual(recursive_fibonacci(2), 1) + self.assertEqual(memoized_fibonacci(2), 1) + self.assertEqual(bottom_up_fibonacci(2), 1) + + def test_fib_n(self): + n = randint(3, 30) + fib_n = recursive_fibonacci(n) + self.assertEqual(fib_n, memoized_fibonacci(n)) + self.assertEqual(fib_n, bottom_up_fibonacci(n)) + + def test_when_return_seq_is_true(self): + self.assertIsInstance(bottom_up_fibonacci(4, return_seq=True), list) + + def test_when_return_seq_is_true_and_n_is_zero(self): + self.assertEqual(bottom_up_fibonacci(0, return_seq=True), [0]) + + def test_when_return_seq_is_true_and_n_is_one(self): + self.assertEqual(bottom_up_fibonacci(1, return_seq=True), [0, 1]) + + def test_when_return_seq_is_true_and_n_is_two(self): + self.assertEqual(bottom_up_fibonacci(2, return_seq=True), [0, 1, 1]) + + def test_when_return_seq_is_true_and_n_is_three(self): + self.assertEqual(bottom_up_fibonacci(3, return_seq=True), [0, 1, 1, 2]) diff --git a/tests/algorithms/matching/__init__.py b/tests/algorithms/matching/__init__.py new file mode 100644 index 00000000..7a04cfe8 --- /dev/null +++ b/tests/algorithms/matching/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/tests/algorithms/matching/test_gale_shapley.py b/tests/algorithms/matching/test_gale_shapley.py new file mode 100644 index 00000000..2ab630a8 --- /dev/null +++ b/tests/algorithms/matching/test_gale_shapley.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 29/09/2017 + +Updated: 29/09/2017 + +# Description + +Unit tests for the functions in the andz.algorithms.matching.gale_shapley +module. +""" + +import unittest + +from andz.algorithms.matching.gale_shapley import gale_shapley + + +class TestGaleShapley(unittest.TestCase): + def test_when_preferences_lists_are_empty(self): + self.assertEqual(gale_shapley([], []), ([], [])) + + def test_when_preferences_lists_sizes_mismatch(self): + self.assertRaises(ValueError, gale_shapley, [[]], []) + self.assertRaises(ValueError, gale_shapley, [], [[]]) + self.assertRaises(ValueError, gale_shapley, [[0, 1]], [[0]]) + self.assertRaises(ValueError, gale_shapley, [[0]], [[0, 1]]) + + def test_when_preferences_lists_have_duplicates(self): + gs = gale_shapley + self.assertRaises(ValueError, gs, [[0, 0], [1, 0]], [[0, 1], [1, 0]]) + self.assertRaises(ValueError, gs, [[1, 0], [0, 0]], [[0, 1], [1, 0]]) + self.assertRaises(ValueError, gs, [[1, 0], [0, 1]], [[0, 0], [1, 0]]) + self.assertRaises(ValueError, gs, [[1, 0], [0, 1]], [[1, 0], [0, 0]]) + + def test_when_preferences_lists_contains_out_of_range_values(self): + gs = gale_shapley + self.assertRaises(ValueError, gs, [[2, 0], [1, 0]], [[0, 1], [1, 0]]) + self.assertRaises(ValueError, gs, [[1, 0], [-4, 0]], [[0, 1], [1, 0]]) + self.assertRaises(ValueError, gs, [[1, 0], [0, 1]], [[10, 0], [1, 0]]) + self.assertRaises(ValueError, gs, [[1, 0], [0, 1]], [[1, 0], [-1, 0]]) + + def test_when_one_man_and_woman(self): + self.assertEqual(gale_shapley([[0]], [[0]]), ([0], [0])) + + def test_when_two_men_and_women(self): + self.assertEqual( + gale_shapley([[0, 1], [0, 1]], [[1, 0], [0, 1]]), ([1, 0], [1, 0]) + ) + + def test_when_three_men_and_women(self): + self.assertEqual( + gale_shapley( + [[2, 0, 1], [0, 2, 1], [0, 1, 2]], [[0, 2, 1], [2, 0, 1], [0, 1, 2]] + ), + ([2, 1, 0], [2, 1, 0]), + ) diff --git a/tests/algorithms/numerical/__init__.py b/tests/algorithms/numerical/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/tests/algorithms/numerical/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/tests/algorithms/numerical/polynomial_interpolation_tests.py b/tests/algorithms/numerical/polynomial_interpolation_tests.py new file mode 100644 index 00000000..7fa1369f --- /dev/null +++ b/tests/algorithms/numerical/polynomial_interpolation_tests.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 12/10/2017 + +Updated: 02/04/2018 + +# Description + +Common units tests for the algorithms to perform polynomial interpolation. +""" + +from math import sqrt +from random import uniform + +import numpy as np +from scipy.interpolate import barycentric_interpolate + + +def f(x: float) -> float: + """f : [-1, 1] -> R.""" + return 1 / (25 * (x**2) + 1) + + +def g(x: float) -> float: + return 1 / sqrt(x) + + +class PolynomialInterpolationTests: + def __init__(self, polynomial_interpolation_algorithm): + self.algorithm = polynomial_interpolation_algorithm + + def test_lists_of_different_lengths(self): + self.assertRaises(ValueError, self.algorithm, [1, 2], [3], 0) + + def test_f(self): + """Interpolation of function f with a polynomial p at the equidistant + points x[k] = −1 + 2 * (k / n), k = 0, ..., n.""" + + n = 20 # n points, so polynomial would be of degree n - 1. + xs = [-1 + 2 * (k / n) for k in range(n)] + ys = [f(x) for x in xs] + + for i in range(20): + x0 = uniform(-1.0, 1.0) + + y0 = self.algorithm(xs, ys, x0) + bi0 = barycentric_interpolate(xs, ys, x0) + + self.assertAlmostEqual(bi0, np.array(y0), 4) + + def test_g(self): + """Example taken from: + https://en.wikiversity.org/wiki/Numerical_Analysis/Neville%27s_algorithm_examples + """ + xs = [16, 64, 100] + ys = [g(x) for x in xs] + x0 = 81 + + y0 = self.algorithm(xs, ys, x0) + bi0 = barycentric_interpolate(xs, ys, x0) + + self.assertAlmostEqual(bi0, np.array(y0), 4) diff --git a/tests/algorithms/numerical/test_barycentric.py b/tests/algorithms/numerical/test_barycentric.py new file mode 100644 index 00000000..18c53905 --- /dev/null +++ b/tests/algorithms/numerical/test_barycentric.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 08/10/2017 + +Updated: 02/04/2018 + +# Description + +Unit tests for the functions in the andz.algorithms.numerical.barycentric +module. +""" + +import unittest + +from andz.algorithms.numerical.barycentric import barycentric, compute_weights +from tests.algorithms.numerical.polynomial_interpolation_tests import * + + +class TestBarycentric(unittest.TestCase, PolynomialInterpolationTests): + def __init__(self, method_name="__init__"): + unittest.TestCase.__init__(self, method_name) + PolynomialInterpolationTests.__init__(self, barycentric) + + def test_when_weights_are_provided(self): + # n points, so polynomial would be of degree n - 1. + xs = [8, 16, 64] + n = len(xs) + + # Given that we want to call barycentric multiple times with different y + # values and different points of evaluation of the polynomial, i.e. + # different x0's, then we pre-compute the weights and pass them to the + # function barycentric. + ws = compute_weights(xs) + + # f and g are functions. + for h in [f, g]: + ys = [h(x) for x in xs] # Evaluate the function at all xs points. + + for x0 in [-2, 2]: + y0 = barycentric(xs, ys, x0, ws) + bi0 = barycentric_interpolate(xs, ys, x0) + + self.assertAlmostEqual(bi0, np.array(y0)) diff --git a/tests/algorithms/numerical/test_gradient_descent.py b/tests/algorithms/numerical/test_gradient_descent.py new file mode 100644 index 00000000..15925ef9 --- /dev/null +++ b/tests/algorithms/numerical/test_gradient_descent.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 26/10/2017 + +Updated: 02/04/2018 + +# Description + +Unit tests for the functions in the andz.algorithms.numerical.gradient_descent +module. +""" + +import unittest + +from andz.algorithms.numerical.gradient_descent import * + +""" +def f(x: float) -> float: + return x ** 4 - 3 * x ** 3 + 2 +""" + + +def df(x: float) -> float: + """Derivative of f.""" + return 4 * x**3 - 9 * x**2 + + +class TestGradientDescent(unittest.TestCase): + def test_type_error_when_df_not_callable(self): + self.assertRaises(TypeError, gradient_descent, 0.3, 4) + + def test_find_local_min_of_f(self): + self.assertAlmostEqual(gradient_descent(3, df), 9 / 4, 4) diff --git a/tests/algorithms/numerical/test_horner.py b/tests/algorithms/numerical/test_horner.py new file mode 100644 index 00000000..420e1db9 --- /dev/null +++ b/tests/algorithms/numerical/test_horner.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 29/09/2017 + +Updated: 30/09/2017 + +# Description + +Unit tests for the functions in the andz.algorithms.numerical.horner module. +""" + +import unittest +from random import randint, uniform + +from numpy.polynomial.polynomial import polyval + +from andz.algorithms.numerical.horner import horner + + +class TestHorner(unittest.TestCase): + def setUp(self): + self.x0 = uniform(-10.0, -10.0) + self.degree = randint(0, 30) # degree of the polynomial + + # We have one more coefficient than the degree of the polynomial. + self.coefficients = [uniform(-10, 10) for _ in range(self.degree + 1)] + + def test_one(self): + self.assertAlmostEqual( + horner(self.x0, self.coefficients), polyval(self.x0, self.coefficients) + ) diff --git a/tests/algorithms/numerical/test_neville.py b/tests/algorithms/numerical/test_neville.py new file mode 100644 index 00000000..b3e30ba7 --- /dev/null +++ b/tests/algorithms/numerical/test_neville.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 08/10/2017 + +Updated: 08/10/2017 + +# Description + +Unit tests for the functions in the andz.algorithms.numerical.neville module. +""" + +import unittest + +from andz.algorithms.numerical.neville import neville +from tests.algorithms.numerical.polynomial_interpolation_tests import * + + +class TestNeville(unittest.TestCase, PolynomialInterpolationTests): + def __init__(self, method_name="__init__"): + unittest.TestCase.__init__(self, method_name) + PolynomialInterpolationTests.__init__(self, neville) diff --git a/tests/algorithms/numerical/test_newton.py b/tests/algorithms/numerical/test_newton.py new file mode 100644 index 00000000..12d1ae31 --- /dev/null +++ b/tests/algorithms/numerical/test_newton.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 29/09/2017 + +Updated: 29/09/2017 + +# Description + +Unit tests for the functions in the andz.algorithms.numerical.newton module. +""" + +import unittest + +from andz.algorithms.numerical.newton import * + + +class TestNewton(unittest.TestCase): + def test_f_not_callable(self): + self.assertRaises(TypeError, newton, 3, 2, max) + + def test_df_not_callable(self): + self.assertRaises(TypeError, newton, 3, max, 2) + + def test_find_square_root(self): + a = 9 # We want to find the square root of a. + x0 = 5 + f = lambda x: x * x - a + df = lambda x: 2 * x + self.assertAlmostEqual(newton(x0, f, df), 3.0) + + def test_find_reciprocal(self): + a = 2 # We want to find the reciprocal of a, i.e. 1 / a. + x0 = 0.3 + f = lambda x: a - (1 / x) + df = lambda x: 1 / (x * x) + # We could also use the iteration: x_next = x * (2 - x * a). + self.assertAlmostEqual(newton(x0, f, df), 1 / 2) diff --git a/tests/algorithms/ode/README.md b/tests/algorithms/ode/README.md new file mode 100644 index 00000000..d52ac53a --- /dev/null +++ b/tests/algorithms/ode/README.md @@ -0,0 +1,13 @@ +## What should I talk about? + +- method derivation + +- explicit vs. implicit methods + +- order of accuracy + +- local and global truncation errors + +- convergence + +- absolute stability and stiffness \ No newline at end of file diff --git a/tests/algorithms/ode/__init__.py b/tests/algorithms/ode/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/tests/algorithms/ode/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/tests/algorithms/ode/test_forward_euler.py b/tests/algorithms/ode/test_forward_euler.py new file mode 100644 index 00000000..331f7974 --- /dev/null +++ b/tests/algorithms/ode/test_forward_euler.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 01/05/2016 + +Updated: 19/02/2017 + +# Description + +Unit tests for the functions in the andz.algorithms.ode.forward_euler module. +""" + +import unittest + +from andz.algorithms.ode.forward_euler import * + + +class TestForwardEuler(unittest.TestCase): + @staticmethod + def f(ti, yi): + return yi + + @staticmethod + def gen_b(a: float, n: int, h: float) -> float: + return h * n + a + + def setUp(self): + # Runs before each test + self.a = 0 # start of the range [a, b] + self.n = 7 + self.h = 0.2 # step + self.b = TestForwardEuler.gen_b(self.a, self.n, self.h) + + def test_forward_euler_parameters_when_are_None(self): + self.assertRaises( + ValueError, forward_euler, None, self.b, self.n, 1, TestForwardEuler.f + ) + self.assertRaises( + ValueError, forward_euler, self.a, None, self.n, 1, TestForwardEuler.f + ) + self.assertRaises( + ValueError, forward_euler, self.a, self.b, None, 1, TestForwardEuler.f + ) + self.assertRaises( + ValueError, forward_euler, self.a, self.b, self.n, None, TestForwardEuler.f + ) + + def test_forward_euler_approx_parameters_when_are_None(self): + self.assertRaises( + ValueError, + forward_euler_approx, + None, + self.b, + self.n, + 1, + TestForwardEuler.f, + ) + self.assertRaises( + ValueError, + forward_euler_approx, + self.a, + None, + self.n, + 1, + TestForwardEuler.f, + ) + self.assertRaises( + ValueError, + forward_euler_approx, + self.a, + self.b, + None, + 1, + TestForwardEuler.f, + ) + self.assertRaises( + ValueError, + forward_euler_approx, + self.a, + self.b, + self.n, + None, + TestForwardEuler.f, + ) + + def test_forward_euler_b_less_than_a(self): + self.h = -0.2 + self.b = TestForwardEuler.gen_b(self.a, self.n, self.h) + self.assertRaises( + ValueError, forward_euler, self.a, self.b, self.n, 1, TestForwardEuler.f + ) + + def test_forward_euler_approx_b_less_than_a(self): + self.h = -0.2 + self.b = TestForwardEuler.gen_b(self.a, self.n, self.h) + self.assertRaises( + ValueError, + forward_euler_approx, + self.a, + self.b, + self.n, + 1, + TestForwardEuler.f, + ) + + def test_forward_euler_f_is_not_callable(self): + self.assertRaises(TypeError, forward_euler, self.a, self.b, self.n, 1, None) + + def test_forward_euler_approx_f_is_not_callable(self): + self.assertRaises( + TypeError, forward_euler_approx, self.a, self.b, self.n, 1, None + ) + + def test_forward_euler_result_is_not_None(self): + # Consider the problem y' = y, y(0) = 1, + # the exact solution is y(t) = e^t. + t, y = forward_euler(self.a, self.b, self.n, 1, TestForwardEuler.f) + self.assertIsNotNone(t) + self.assertIsNotNone(y) + + def test_forward_euler_approx_result_is_not_None(self): + y = forward_euler_approx(self.a, self.b, self.n, 1, TestForwardEuler.f) + self.assertIsNotNone(y) diff --git a/tests/algorithms/recursion/__init__.py b/tests/algorithms/recursion/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/tests/algorithms/recursion/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/tests/algorithms/recursion/test_ackermann.py b/tests/algorithms/recursion/test_ackermann.py new file mode 100644 index 00000000..73512331 --- /dev/null +++ b/tests/algorithms/recursion/test_ackermann.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 18/01/2017 + +Updated: 04/08/2018 + +# Description + +Unit tests for the functions in the andz.algorithms.recursion.ackermann module. +""" + +import unittest + +from andz.algorithms.recursion.ackermann import ackermann + + +class TestAckermann(unittest.TestCase): + # m is the first parameter to the Ackermann function + # whereas n is the second one. + + def test_m0_n0(self): + self.assertEqual(ackermann(0, 0), 1) + + def test_m0_n1(self): + self.assertEqual(ackermann(0, 1), 2) + + def test_m0_n2(self): + self.assertEqual(ackermann(0, 2), 3) + + def test_m1_n0(self): + self.assertEqual(ackermann(1, 0), 2) + + def test_m1_n1(self): + self.assertEqual(ackermann(1, 1), 3) + + def test_m1_n2(self): + self.assertEqual(ackermann(1, 2), 4) + + def test_m2_n0(self): + self.assertEqual(ackermann(2, 0), 3) + + def test_m2_n1(self): + self.assertEqual(ackermann(2, 1), 5) + + def test_m2_n2(self): + self.assertEqual(ackermann(2, 2), 7) diff --git a/tests/algorithms/recursion/test_count.py b/tests/algorithms/recursion/test_count.py new file mode 100644 index 00000000..b71e0bc6 --- /dev/null +++ b/tests/algorithms/recursion/test_count.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 15/01/2017 + +Updated: 04/08/2018 + +# Description + +Unit tests for the functions in the andz.algorithms.recursion.count module. +""" + +import unittest +from random import randint + +from andz.algorithms.recursion.count import count + + +class TestRecursiveCount(unittest.TestCase): + def test_empty_list(self): + ls = [] + self.assertEqual(count(7, ls), 0) + + def test_size_1(self): + ls = [3] + self.assertEqual(count(3, ls), 1) + self.assertEqual(count(11, ls), 0) + + def test_size_greater_than_1(self): + ls = [5, 2, 11, 2, 17, 29, 37, 41, 2, 5] + self.assertEqual(count(2, ls), 3) + self.assertEqual(count(13, ls), 0) + + def test_random_size(self): + ls = [randint(-10, 10) for _ in range(randint(5, 15))] + self.assertEqual(count(-11, ls), 0) diff --git a/tests/algorithms/recursion/test_factorial.py b/tests/algorithms/recursion/test_factorial.py new file mode 100644 index 00000000..dfa42b52 --- /dev/null +++ b/tests/algorithms/recursion/test_factorial.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 21/01/2017 + +Updated: 04/08/2018 + +# Description + +Unit tests for the functions in the andz.algorithms.recursion.factorial module. +""" + +import math +import unittest +from random import randint + +from andz.algorithms.recursion.factorial import * + + +class TestFactorial(unittest.TestCase): + def test_factorial_0(self): + self.assertEqual(factorial(0), 1) + self.assertEqual(iterative_factorial(0), 1) + + def test_factorial_1(self): + self.assertEqual(factorial(1), 1) + self.assertEqual(iterative_factorial(1), 1) + + def test_factorial_random_number(self): + r = randint(2, 100) + self.assertEqual(factorial(r), math.factorial(r)) + self.assertEqual(iterative_factorial(r), math.factorial(r)) + + def test_multiple_factorial_0(self): + self.assertEqual(multiple_factorial(0), [1]) + + def test_multiple_factorial_1(self): + self.assertEqual(multiple_factorial(1), [1, 1]) + + def test_multiple_factorial_random_n(self): + ls = [] + r = randint(2, 10) + for i in range(r + 1): + ls.append(math.factorial(i)) + self.assertEqual(multiple_factorial(r), ls) + + def test_smallest_geq_0(self): + self.assertEqual(smallest_geq(0), 0) + + def test_smallest_geq_1(self): + self.assertEqual(smallest_geq(1), 0) + + def test_smallest_geq_2(self): + self.assertEqual(smallest_geq(2), 2) + + def test_smallest_geq_3(self): + self.assertEqual(smallest_geq(3), 3) diff --git a/tests/algorithms/recursion/test_hanoi.py b/tests/algorithms/recursion/test_hanoi.py new file mode 100644 index 00000000..d01b204e --- /dev/null +++ b/tests/algorithms/recursion/test_hanoi.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 18/01/2017 + +Updated: 04/08/2018 + +# Description + +Unit tests for the functions in the andz.algorithms.recursion.hanoi module. +""" + +import unittest + +from andz.algorithms.recursion.hanoi import hanoi + + +class TestRecursiveHanoi(unittest.TestCase): + def test_n0(self): + self.assertEqual(hanoi(0), []) + + def test_n1(self): + self.assertEqual(hanoi(1), [(1, "A", "C")]) + + def test_n2(self): + self.assertEqual(hanoi(2), [(1, "A", "B"), (2, "A", "C"), (1, "B", "C")]) + + def test_n3(self): + self.assertEqual( + hanoi(3), + [ + (1, "A", "C"), + (2, "A", "B"), + (1, "C", "B"), + (3, "A", "C"), + (1, "B", "A"), + (2, "B", "C"), + (1, "A", "C"), + ], + ) diff --git a/tests/algorithms/recursion/test_is_sorted.py b/tests/algorithms/recursion/test_is_sorted.py new file mode 100644 index 00000000..ef51f2c9 --- /dev/null +++ b/tests/algorithms/recursion/test_is_sorted.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 21/01/2017 + +Updated: 04/08/2018 + +# Description + +Unit tests for the functions in the andz.algorithms.recursion.is_sorted module. +""" + +import unittest +from random import randint + +from andz.algorithms.recursion.is_sorted import * + + +class TestIsSorted(unittest.TestCase): + def test_empty_list(self): + self.assertTrue(is_sorted([])) + self.assertTrue(is_sorted([], True)) + + self.assertTrue(iterative_is_sorted([])) + self.assertTrue(iterative_is_sorted([], True)) + + self.assertTrue(pythonic_is_sorted([])) + self.assertTrue(pythonic_is_sorted([], True)) + + def test_empty_tuple(self): + self.assertTrue(is_sorted(())) + self.assertTrue(is_sorted((), True)) + + self.assertTrue(iterative_is_sorted(())) + self.assertTrue(iterative_is_sorted((), True)) + + self.assertTrue(pythonic_is_sorted(())) + self.assertTrue(pythonic_is_sorted((), True)) + + def test_size_1_list(self): + r = randint(-10, 10) + self.assertTrue(is_sorted([r])) + self.assertTrue(is_sorted([r], True)) + + self.assertTrue(iterative_is_sorted([r])) + self.assertTrue(iterative_is_sorted([r], True)) + + self.assertTrue(pythonic_is_sorted([r])) + self.assertTrue(pythonic_is_sorted([r], True)) + + def test_size_1_tuple(self): + r = randint(-10, 10) + self.assertTrue(is_sorted((r,))) + self.assertTrue(is_sorted((r,), True)) + + self.assertTrue(iterative_is_sorted((r,))) + self.assertTrue(iterative_is_sorted((r,), True)) + + self.assertTrue(pythonic_is_sorted((r,))) + self.assertTrue(pythonic_is_sorted((r,), True)) + + def test_size_2_list_sorted(self): + ls = [13, 19] + + self.assertTrue(is_sorted(ls)) + self.assertFalse(is_sorted(ls, True)) + + self.assertTrue(iterative_is_sorted(ls)) + self.assertFalse(iterative_is_sorted(ls, True)) + + self.assertTrue(pythonic_is_sorted(ls)) + self.assertFalse(pythonic_is_sorted(ls, True)) + + def test_size_2_list_rev(self): + ls = [23, 11] + + self.assertFalse(is_sorted(ls)) + self.assertTrue(is_sorted(ls, True)) + + self.assertFalse(iterative_is_sorted(ls)) + self.assertTrue(iterative_is_sorted(ls, True)) + + self.assertFalse(pythonic_is_sorted(ls)) + self.assertTrue(pythonic_is_sorted(ls, True)) + + def test_size_2_tuple_sorted(self): + tup = (13, 19) + + self.assertTrue(is_sorted(tup)) + self.assertFalse(is_sorted(tup, True)) + + self.assertTrue(iterative_is_sorted(tup)) + self.assertFalse(iterative_is_sorted(tup, True)) + + self.assertTrue(pythonic_is_sorted(tup)) + self.assertFalse(pythonic_is_sorted(tup, True)) + + def test_size_2_tuple_rev(self): + tup = (23, 11) + + self.assertFalse(is_sorted(tup)) + self.assertTrue(is_sorted(tup, True)) + + self.assertFalse(iterative_is_sorted(tup)) + self.assertTrue(iterative_is_sorted(tup, True)) + + self.assertFalse(pythonic_is_sorted(tup)) + self.assertTrue(pythonic_is_sorted(tup, True)) + + def test_random_size_list_sorted(self): + ls = [randint(-100, 100) for _ in range(randint(3, 300))] + ls.sort() + self.assertTrue(is_sorted(ls)) + self.assertTrue(iterative_is_sorted(ls)) + self.assertTrue(pythonic_is_sorted(ls)) + + def test_random_size_list_rev(self): + ls = [randint(-100, 100) for _ in range(randint(3, 300))] + ls.sort(reverse=True) + self.assertTrue(is_sorted(ls, True)) + self.assertTrue(iterative_is_sorted(ls, True)) + self.assertTrue(pythonic_is_sorted(ls, True)) + + def test_random_size_tuple_sorted(self): + tup = [randint(-100, 100) for _ in range(randint(3, 300))] + tup.sort() + tup = tuple(tup) + + self.assertTrue(is_sorted(tup)) + self.assertTrue(iterative_is_sorted(tup)) + self.assertTrue(pythonic_is_sorted(tup)) + + def test_random_size_tuple_rev(self): + tup = [randint(-100, 100) for _ in range(randint(3, 300))] + tup.sort(reverse=True) + tup = tuple(tup) + + self.assertTrue(is_sorted(tup, True)) + self.assertTrue(iterative_is_sorted(tup, True)) + self.assertTrue(pythonic_is_sorted(tup, True)) diff --git a/tests/algorithms/recursion/test_make_decimal.py b/tests/algorithms/recursion/test_make_decimal.py new file mode 100644 index 00000000..2d4e190f --- /dev/null +++ b/tests/algorithms/recursion/test_make_decimal.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 20/01/2017 + +Updated: 04/08/2018 + +# Description + +Unit tests for the functions in the andz.algorithms.recursion.make_decimal +module. +""" + +import string +import unittest +from random import choice, randint + +from andz.algorithms.recursion.make_decimal import make_decimal + + +class TestMakeDecimal(unittest.TestCase): + @staticmethod + def generate_number(base: int): + def build_possible_digits(base: int): + possible_digits = list(string.digits) + + if base < 10: + possible_digits = possible_digits[0:base] + elif base > 10: + possible_digits += list(string.ascii_lowercase)[0 : base - 10] + + return possible_digits + + pd = build_possible_digits(base) + + # Length of the string representing the random number in base `base`. + length = randint(1, 10) + + return "".join([choice(pd) for _ in range(length)]) + + def test_empty_number(self): + self.assertRaises(ValueError, make_decimal, "", 11) + + def test_none_number(self): + self.assertRaises(ValueError, make_decimal, None, 13) + + def test_base_1(self): + self.assertRaises(ValueError, make_decimal, "3", 1) + + def test_base_37(self): + self.assertRaises(ValueError, make_decimal, "7", 37) + + def test_random_base(self): + # Testing the implementation of make_decimal against int() + for _ in range(randint(100, 1000)): + b = randint(2, 36) + n = TestMakeDecimal.generate_number(b) + self.assertEqual(make_decimal(n, b), int(n, b)) diff --git a/tests/algorithms/recursion/test_palindrome.py b/tests/algorithms/recursion/test_palindrome.py new file mode 100644 index 00000000..b87dc6ac --- /dev/null +++ b/tests/algorithms/recursion/test_palindrome.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 20/01/2017 + +Updated: 15/03/2022 + +# Description + +Unit tests for the functions in the andz.algorithms.recursion.palindrome +module. +""" + +import string +import unittest +from random import choice, randint + +from andz.algorithms.recursion.palindrome import is_palindrome, iterative_is_palindrome + + +def make_test(func: callable): + class TestIsPalindrome(unittest.TestCase): + @staticmethod + def generate_palindrome(n: int): + """Generates a palindrome of size `n`""" + assert n > 0 + if n == 1: + return choice(string.ascii_letters) + + p = [] + for _ in range(n - 1): + char = choice(string.ascii_letters) + p.insert(len(p) // 2, char) + p.insert(len(p) // 2, char) + return "".join(p) + + def test_empty_str(self): + self.assertTrue(func("")) + + def test_size_1(self): + self.assertTrue(func(TestIsPalindrome.generate_palindrome(1))) + + def test_size_2(self): + self.assertTrue(func(TestIsPalindrome.generate_palindrome(2))) + + def test_size_2_not(self): + self.assertFalse(func("xy")) + + def test_random_size(self): + n = randint(3, 100) + self.assertTrue(func(TestIsPalindrome.generate_palindrome(n))) + + return TestIsPalindrome + + +test_is_palindrome_recursively = make_test(is_palindrome) +test_is_palindrome_iteratively = make_test(iterative_is_palindrome) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/algorithms/recursion/test_power.py b/tests/algorithms/recursion/test_power.py new file mode 100644 index 00000000..e78874f7 --- /dev/null +++ b/tests/algorithms/recursion/test_power.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 18/01/2017 + +Updated: 04/08/2018 + +# Description + +Unit tests for the functions in the andz.algorithms.recursion.power module. +""" + +import unittest +from random import randint + +from andz.algorithms.recursion.power import power + + +class TestRecursivePower(unittest.TestCase): + # Testing raising to power of 0. + + def test_base_0_power_0(self): + self.assertEqual(power(0, 0), 1) + + def test_base_minus_1_power_0(self): + self.assertEqual(power(-1, 0), 1) + + def test_base_1_power_0(self): + self.assertEqual(power(1, 0), 1) + + def test_base_random_base_power_0(self): + self.assertEqual(power(randint(2, 100), 0), 1) + self.assertEqual(power(randint(-100, -2), 0), 1) + + def test_base_0_power_1(self): + self.assertEqual(power(0, 1), 0) + + def test_base_1_power_1(self): + self.assertEqual(power(1, 1), 1) + + def test_base_minus_1_power_1(self): + self.assertEqual(power(-1, 1), -1) + + def test_random_base_power_1(self): + a = randint(2, 100) + b = randint(-100, -2) + self.assertEqual(power(a, 1), a) + self.assertEqual(power(b, 1), b) + + def test_random_base_random_positive_power(self): + b = randint(-100, 100) + p = randint(2, 100) + self.assertEqual(power(b, p), b**p) diff --git a/tests/algorithms/recursion/test_reverse.py b/tests/algorithms/recursion/test_reverse.py new file mode 100644 index 00000000..6f50d84f --- /dev/null +++ b/tests/algorithms/recursion/test_reverse.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 16/01/2017 + +Updated: 04/08/2018 + +# Description + +Unit tests for the functions in the andz.algorithms.recursion.reverse module. +""" + +import unittest +from random import randint + +from andz.algorithms.recursion.reverse import reverse + + +class TestRecursiveReverse(unittest.TestCase): + def test_empty(self): + l = [] + rev = reverse(l) + self.assertIs(rev, l) + self.assertEqual(rev, []) + + def test_size_one(self): + l = [97] + rev = reverse(l) + self.assertIs(rev, l) + self.assertEqual(rev, [97]) + + def test_size_two(self): + l = [101, 67] + rev = reverse(l) + self.assertIs(rev, l) + self.assertEqual(rev, [67, 101]) + + def test_greater_than_two(self): + l = [randint(0, 100) for _ in range(100)] + copy = l[:] # shallow copy is ok in this case. + rev = reverse(l) + copy.reverse() + self.assertIs(rev, l) + self.assertEqual(rev, copy) diff --git a/tests/algorithms/sorting/__init__.py b/tests/algorithms/sorting/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/tests/algorithms/sorting/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/tests/algorithms/sorting/base_tests.py b/tests/algorithms/sorting/base_tests.py new file mode 100644 index 00000000..38a54ffa --- /dev/null +++ b/tests/algorithms/sorting/base_tests.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 20/03/2016 + +Updated: 07/03/2022 + +# Description + +Common unit tests for the functions to sort sequences. +""" + +from random import randint + +from andz.algorithms.recursion.is_sorted import iterative_is_sorted + + +def build_random_list(size=10, start=-10, end=10): + """Returns a list of random elements. + You can specify the size of the list. + You can also specify the range of numbers in the list.""" + return [randint(start, end) for _ in range(size)] + + +class SortingAlgorithmTests: + def __init__( + self, + sorting_algorithm, + in_place=True, + start=randint(-10001, -1), + end=randint(0, 10000), + ): + self.sorting_algorithm = sorting_algorithm + + # the default smallest and biggest value in the lists that are used + # to test the sorting algorithms. + self.start = start + self.end = end + self.in_place = in_place + + def assert_commonalities(self, a): + b = self.sorting_algorithm(a) + + if self.in_place: + self.assertIsNone(b) + self.assertTrue(iterative_is_sorted(a)) + else: # In-place sorting algorithms return all None + self.assertTrue(iterative_is_sorted(b)) + + def test_empty(self): + self.assert_commonalities([]) + + def test_size_1(self): + a = build_random_list(size=1, start=self.start, end=self.end) + self.assert_commonalities(a) + + def test_size_2(self): + a = build_random_list(size=2, start=self.start, end=self.end) + self.assert_commonalities(a) + + def test_random_size(self): + size = randint(3, 1113) + a = build_random_list(size=size, start=self.start, end=self.end) + self.assert_commonalities(a) diff --git a/tests/algorithms/sorting/comparison/__init__.py b/tests/algorithms/sorting/comparison/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/tests/algorithms/sorting/comparison/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/tests/algorithms/sorting/comparison/test_bubble_sort.py b/tests/algorithms/sorting/comparison/test_bubble_sort.py new file mode 100755 index 00000000..4fde9aa4 --- /dev/null +++ b/tests/algorithms/sorting/comparison/test_bubble_sort.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 1/01/2017 + +Updated: 04/08/2018 + +# Description + +Unit tests for the functions in the +andz.algorithms.sorting.comparison.bubble_sort module. +""" + +import unittest + +from andz.algorithms.sorting.comparison.bubble_sort import bubble_sort +from tests.algorithms.sorting.base_tests import * + + +class TestBubbleSort(unittest.TestCase, SortingAlgorithmTests): + def __init__(self, method_name="__init__"): + unittest.TestCase.__init__(self, method_name) + SortingAlgorithmTests.__init__(self, bubble_sort) diff --git a/tests/algorithms/sorting/comparison/test_heap_sort.py b/tests/algorithms/sorting/comparison/test_heap_sort.py new file mode 100644 index 00000000..0331ba8b --- /dev/null +++ b/tests/algorithms/sorting/comparison/test_heap_sort.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 1/01/2017 + +Updated: 04/08/2018 + +# Description + +Unit tests for the functions in the +andz.algorithms.sorting.comparison.heap_sort module. +""" + +import unittest + +from andz.algorithms.sorting.comparison.heap_sort import heap_sort +from tests.algorithms.sorting.base_tests import * + + +class TestHeapSort(unittest.TestCase, SortingAlgorithmTests): + def __init__(self, method_name="__init__"): + unittest.TestCase.__init__(self, method_name) + SortingAlgorithmTests.__init__(self, heap_sort) diff --git a/tests/algorithms/sorting/comparison/test_insertion_sort.py b/tests/algorithms/sorting/comparison/test_insertion_sort.py new file mode 100644 index 00000000..d73cd089 --- /dev/null +++ b/tests/algorithms/sorting/comparison/test_insertion_sort.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 1/01/2017 + +Updated: 04/08/2018 + +# Description + +Unit tests for the functions in the +andz.algorithms.sorting.comparison.insertion_sort +module. +""" + +import unittest + +from andz.algorithms.sorting.comparison.insertion_sort import insertion_sort +from tests.algorithms.sorting.base_tests import * + + +class TestInsertionSort(unittest.TestCase, SortingAlgorithmTests): + def __init__(self, method_name="__init__"): + unittest.TestCase.__init__(self, method_name) + SortingAlgorithmTests.__init__(self, insertion_sort) diff --git a/tests/algorithms/sorting/comparison/test_merge_sort.py b/tests/algorithms/sorting/comparison/test_merge_sort.py new file mode 100644 index 00000000..559fcb62 --- /dev/null +++ b/tests/algorithms/sorting/comparison/test_merge_sort.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 1/01/2017 + +Updated: 04/08/2018 + +# Description + +Unit tests for the functions in the +andz.algorithms.sorting.comparison.merge_sort module. +""" + +import unittest + +from andz.algorithms.sorting.comparison.merge_sort import merge_sort +from tests.algorithms.sorting.base_tests import * + + +class TestMergeSort(unittest.TestCase, SortingAlgorithmTests): + def __init__(self, method_name="__init__"): + unittest.TestCase.__init__(self, method_name) + SortingAlgorithmTests.__init__(self, merge_sort, in_place=False) diff --git a/tests/algorithms/sorting/comparison/test_quick_sort.py b/tests/algorithms/sorting/comparison/test_quick_sort.py new file mode 100644 index 00000000..81a30933 --- /dev/null +++ b/tests/algorithms/sorting/comparison/test_quick_sort.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 1/01/2017 + +Updated: 04/08/2018 + +# Description + +Unit tests for the functions in the +andz.algorithms.sorting.comparison.quick_sort module. +""" + +import unittest + +from andz.algorithms.sorting.comparison.quick_sort import quick_sort +from tests.algorithms.sorting.base_tests import * + + +class TestQuickSort(unittest.TestCase, SortingAlgorithmTests): + def __init__(self, method_name="__init__"): + unittest.TestCase.__init__(self, method_name) + SortingAlgorithmTests.__init__(self, quick_sort) diff --git a/tests/algorithms/sorting/comparison/test_selection_sort.py b/tests/algorithms/sorting/comparison/test_selection_sort.py new file mode 100644 index 00000000..c5afeb19 --- /dev/null +++ b/tests/algorithms/sorting/comparison/test_selection_sort.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 1/01/2017 + +Updated: 04/08/2018 + +# Description + +Unit tests for the functions in the +andz.algorithms.sorting.comparison.selection_sort module. +""" + +import unittest + +from andz.algorithms.sorting.comparison.selection_sort import selection_sort +from tests.algorithms.sorting.base_tests import * + + +class TestSelectionSort(unittest.TestCase, SortingAlgorithmTests): + def __init__(self, method_name="__init__"): + unittest.TestCase.__init__(self, method_name) + SortingAlgorithmTests.__init__(self, selection_sort) diff --git a/tests/algorithms/sorting/integer/__init__.py b/tests/algorithms/sorting/integer/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/tests/algorithms/sorting/integer/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/tests/algorithms/sorting/integer/test_counting_sort.py b/tests/algorithms/sorting/integer/test_counting_sort.py new file mode 100644 index 00000000..623d21ea --- /dev/null +++ b/tests/algorithms/sorting/integer/test_counting_sort.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 03/03/2022 + +Updated: 07/03/2022 + +# Description + +Unit tests for the functions in the +andz.algorithms.sorting.integer.counting_sort module. +""" + +import random +import string +import unittest + +from andz.algorithms.sorting.integer.counting_sort import counting_sort +from tests.algorithms.sorting.base_tests import SortingAlgorithmTests + + +def gen_random_string(k=5): + return "".join(random.choices(string.ascii_uppercase + string.digits, k=k)) + + +def gen_random_key_indexed_list(n=100, a=0, b=1000): + return [(random.randint(a, b), gen_random_string()) for _ in range(n)] + + +def make_test(sedgewick_wayne=False): + class TestCountingSort(unittest.TestCase, SortingAlgorithmTests): + def __init__(self, method_name="__init__"): + unittest.TestCase.__init__(self, method_name) + SortingAlgorithmTests.__init__( + self, counting_sort, in_place=False, start=0, end=10000 + ) + + def test_raises_when_not_all_int(self): + self.assertRaises( + TypeError, + self.sorting_algorithm, + [1, "1"], + sedgewick_wayne=sedgewick_wayne, + ) + + def test_raises_when_k_is_not_non_negative(self): + self.assertRaises( + ValueError, + self.sorting_algorithm, + [1], + -1, + sedgewick_wayne=sedgewick_wayne, + ) + + def test_raises_when_not_all_elements_are_less_than_k(self): + self.assertRaises( + ValueError, + self.sorting_algorithm, + [10, 1], + 3, + sedgewick_wayne=sedgewick_wayne, + ) + + def test_raises_when_not_all_elements_are_non_negative(self): + self.assertRaises( + ValueError, + self.sorting_algorithm, + [10, -1], + sedgewick_wayne=sedgewick_wayne, + ) + + def test_raises_when_key_is_not_callable(self): + self.assertRaises( + TypeError, + self.sorting_algorithm, + [3, 2], + key=3, + sedgewick_wayne=sedgewick_wayne, + ) + + def test_raises_when_bad_key(self): + class A: + def __init__(self, y=2): + self.y = y + + self.assertRaises( + KeyError, + self.sorting_algorithm, + [A(), ("zero", 0)], + key=lambda x: x.y, + sedgewick_wayne=sedgewick_wayne, + ) + + def test_raises_when_non_int_key(self): + self.assertRaises( + TypeError, + self.sorting_algorithm, + [("zero", 0), ("one", 1)], + key=lambda x: x[0], + sedgewick_wayne=sedgewick_wayne, + ) + + def test_key_indexed_list(self): + a = gen_random_key_indexed_list() + key = lambda x: x[0] + b = self.sorting_algorithm(a, key=key) + b1 = sorted(a, key=key) + + # Python's sorted is guaranteed to be stable. If that wasn't the + # case, we couldn't use it to test the correctness of counting + # sort. + # https://stackoverflow.com/q/1915376/3924118 + assert b == b1 + + return TestCountingSort + + +test_sw_counting_sort = make_test(sedgewick_wayne=True) +test_counting_sort = make_test(sedgewick_wayne=False) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/algorithms/sorting/integer/test_radix_sort.py b/tests/algorithms/sorting/integer/test_radix_sort.py new file mode 100644 index 00000000..87c41ede --- /dev/null +++ b/tests/algorithms/sorting/integer/test_radix_sort.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 05/03/2022 + +Updated: 05/03/2022 + +# Description + +Unit tests for the functions in the andz.algorithms.sorting.integer.radix_sort +module. +""" + +import unittest + +from andz.algorithms.sorting.integer.radix_sort import radix_sort +from tests.algorithms.sorting.base_tests import SortingAlgorithmTests + + +class TestRadixSort(unittest.TestCase, SortingAlgorithmTests): + def __init__(self, method_name="__init__"): + unittest.TestCase.__init__(self, method_name) + SortingAlgorithmTests.__init__( + self, radix_sort, in_place=False, start=0, end=10000 + ) + + def test_raises_when_not_all_int(self): + self.assertRaises(TypeError, self.sorting_algorithm, [1, "1"]) + + def test_raises_when_not_all_elements_are_non_negative(self): + self.assertRaises(ValueError, self.sorting_algorithm, [10, -1]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/ds/__init__.py b/tests/ds/__init__.py new file mode 100755 index 00000000..7a04cfe8 --- /dev/null +++ b/tests/ds/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# File to allow this directory to be treated as a Python package. diff --git a/tests/ds/test_BST.py b/tests/ds/test_BST.py new file mode 100755 index 00000000..23791be9 --- /dev/null +++ b/tests/ds/test_BST.py @@ -0,0 +1,451 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 13/02/2016 + +Updated: 14/03/2017 + +# Description + +Unit tests for the classes and functions in the andz.ds.BST module. +""" + +import string +import unittest +from random import choice, randint + +from andz.ds.BST import BST, _BSTNode + + +class TestBST(unittest.TestCase): + def setUp(self): + self.t = BST() + + def test_create_default(self): + self.assertEqual(self.t.size, 0) + self.assertTrue(self.t.is_empty()) + + def test_clear(self): + for e in [2, 3, 5]: + self.t.insert(e) + self.t.clear() + self.assertTrue(self.t.is_empty()) + + def test_insert_one_when_key_is_None(self): + self.assertRaises(ValueError, self.t.insert, None) + + def test_insert_one(self): + self.t.insert("one") + self.assertEqual(self.t.size, 1) + self.assertEqual(self.t.height(), 1) + self.assertEqual(self.t.rank("one"), 0) + self.assertTrue(self.t.contains("one")) + + def test_insert_many(self): + for letter in string.printable: + self.t.insert(letter) + + self.assertEqual(self.t.size, len(string.printable)) + + for letter in string.printable: + self.assertTrue(self.t.contains(letter)) + + def test_contains_when_key_is_None(self): + self.assertRaises(ValueError, self.t.contains, None) + + def test_contains_when_empty_tree(self): + self.assertFalse(self.t.contains("two")) + + def test_contains_true(self): + self.t.insert(12) + self.t.insert(5) + self.assertTrue(self.t.contains(12)) + + def test_contains_false(self): + self.t.insert(12) + self.assertFalse(self.t.contains(14)) + + def test_rank_when_key_is_None(self): + self.assertRaises(ValueError, self.t.rank, None) + + def test_rank_when_key_not_found(self): + self.assertRaises(LookupError, self.t.rank, 19) + + def test_rank_when_key_is_the_smallest_element(self): + for i in range(3): + self.t.insert(i) + self.assertEqual(self.t.rank(0), 0) + + def test_rank_when_key_is_the_greatest_element(self): + for i in range(5): + self.t.insert(i) + self.assertEqual(self.t.rank(4), 4) + + def test_rank_when_key_is_some_element_in_the_middle(self): + for e in [10, 5, 6, 19]: + self.t.insert(e) + self.assertEqual(self.t.rank(6), 1) + + def test_height_when_tree_empty(self): + self.assertEqual(self.t.height(), 0) + + def test_minimum_when_empty_tree(self): + self.assertIsNone(self.t.minimum()) + + def test_minimum(self): + for e in [10, 8, 5, 5, 1, 2, 3]: + self.t.insert(e) + self.assertEqual(self.t.minimum(), 1) + + def test_maximum_when_empty_tree(self): + self.assertIsNone(self.t.maximum()) + + def test_maximum(self): + for i in range(3): + self.t.insert(i) + self.assertEqual(self.t.maximum(), 2) + + def test_successor_when_key_is_None(self): + self.assertRaises(ValueError, self.t.successor, None) + + def test_successor_when_key_does_not_exist(self): + self.assertRaises(LookupError, self.t.successor, 4) + + def test_successor_when_no_successor(self): + for e in [4, 2, 50, 8]: + self.t.insert(e) + self.assertIsNone(self.t.successor(50)) + + def test_successor_when_is_min_of_right_sub_tree(self): + for e in [5, 2, 10, 8, 9]: + self.t.insert(e) + self.assertEqual(8, self.t.successor(5)) + + def test_successor_when_is_first_node_up_to_root_such_that_child_is_not_right(self): + for e in [5, 2, 10, 8, 9]: + self.t.insert(e) + self.assertEqual(self.t.successor(9), 10) + + def test_predecessor_when_key_is_None(self): + self.assertRaises(ValueError, self.t.predecessor, None) + + def test_predecessor_when_key_does_not_exist(self): + self.assertRaises(LookupError, self.t.predecessor, 4) + + def test_predecessor_when_is_None(self): + for e in [4, 5, 6, 10, 5]: + self.t.insert(e) + self.assertIsNone(self.t.predecessor(4)) + + def test_predecessor_when_is_max_of_left_sub_tree(self): + for e in [5, 2, 10, 8, 9]: + self.t.insert(e) + self.assertEqual(self.t.predecessor(10), 9) + + def test_predecessor_when_is_first_node_up_to_root_such_that_child_is_not_left( + self, + ): + for e in [5, 2, 10, 8, 9]: + self.t.insert(e) + self.assertEqual(self.t.predecessor(8), 5) + + def test_remove_max_when_empty_tree(self): + self.assertIsNone(self.t.remove_max()) + + def test_remove_max_when_greatest_node_has_left_child_and_is_root(self): + for e in [10, 8, 9]: + self.t.insert(e) + self.t.remove_max() + self.assertEqual(self.t.size, 2) + + def test_remove_max_when_greatest_node_has_left_child_and_is_not_root(self): + for e in [5, 2, 10, 8, 9]: + self.t.insert(e) + self.t.remove_max() + self.assertEqual(self.t.size, 4) + + def test_remove_max_when_greatest_node_does_not_have_left_child_and_is_root(self): + self.t.insert(5) + self.t.remove_max() + self.assertEqual(self.t.size, 0) + + def test_remove_max_when_greatest_node_does_not_have_left_child_and_is_not_root( + self, + ): + for e in [5, 2, 10]: + self.t.insert(e) + self.t.remove_max() + self.assertEqual(self.t.size, 2) + + def test_remove_min_when_empty_tree(self): + self.assertIsNone(self.t.remove_min()) + + def test_remove_min_when_smallest_node_has_right_child_and_is_root(self): + for e in [2, 3, 5]: + self.t.insert(e) + self.t.remove_min() + self.assertEqual(self.t.size, 2) + + def test_remove_min_when_smallest_node_has_right_child_and_is_not_root(self): + for e in [5, 2, 3]: + self.t.insert(e) + self.t.remove_min() + self.assertEqual(self.t.size, 2) + + def test_remove_min_when_smallest_node_does_not_have_right_child_and_is_root(self): + self.t.insert(2) + self.t.remove_min() + self.assertTrue(self.t.is_empty()) + + def test_remove_min_when_smallest_node_does_not_have_right_child_and_is_not_root( + self, + ): + for e in [5, 10, 2]: + self.t.insert(e) + self.t.remove_min() + self.assertTrue(self.t.size, 2) + + def test_delete_when_key_is_None(self): + self.assertRaises(ValueError, self.t.delete, None) + + def test_delete_when_key_not_found(self): + self.assertRaises(LookupError, self.t.delete, 3) + for e in [1, 3, 4]: + self.t.insert(e) + self.assertRaises(LookupError, self.t.delete, 5) + + def test_delete_when_size_1(self): + self.t.insert(12) + self.assertIsNone(self.t.delete(12)) + self.assertFalse(self.t.contains(12)) + self.assertTrue(self.t.is_empty()) + + def test_delete_when_no_children(self): + for e in [5, 2, 10, 8, 9]: + self.t.insert(e) + self.assertIsNone(self.t.delete(9)) + self.assertEqual(self.t.size, 4) + + def test_delete_when_one_child(self): + for e in [5, 2, 10, 8, 9]: + self.t.insert(e) + self.assertIsNone(self.t.delete(8)) + self.assertEqual(self.t.size, 4) + + def test_delete_when_two_children(self): + for e in [5, 2, 10, 8, 9, 8, 12, 11, 13]: + self.t.insert(e) + self.assertIsNone(self.t.delete(10)) + self.assertEqual(self.t.size, 8) + + def test_delete_all_in_random_order(self): + ls = [randint(-100, 100) for _ in range(1000)] + + for e in ls: + self.t.insert(e) + + for _ in range(len(ls)): + elem = choice(ls) + ls.remove(elem) + self.assertIsNone(self.t.delete(elem)) + + self.assertTrue(self.t.is_empty()) + + def test_in_order_traversal(self): + for e in [10, 4, 85, 43, 6, 1, 69]: + self.t.insert(e) + self.t.in_order_traversal() + + def test_pre_order_traversal(self): + for e in [10, 4, 85, 43, 6, 1, 69]: + self.t.insert(e) + self.t.pre_order_traversal() + + def test_post_order_traversal(self): + for e in [10, 4, 85, 43, 6, 1, 69]: + self.t.insert(e) + self.t.post_order_traversal() + + def test_reverse_in_order_traversal(self): + for e in [10, 4, 85, 43, 6, 1, 69]: + self.t.insert(e) + self.t.reverse_in_order_traversal() + + +class TestBSTNode(unittest.TestCase): + def test_create_when_key_None(self): + self.assertRaises(ValueError, _BSTNode, None) + + def test_create_when_no_key(self): + self.assertRaises(TypeError, _BSTNode) + + def test_create_default(self): + n = _BSTNode(12) + self.assertEqual(n.key, 12) + self.assertIsNone(n.left) + self.assertIsNone(n.right) + self.assertIsNone(n.parent) + self.assertEqual(n.count(), 1) + + def test_comparison_when_values_are_of_different_types(self): + a = _BSTNode(12) + b = _BSTNode(14) + with self.assertRaises(TypeError): + a < b + with self.assertRaises(TypeError): + a >= b + + def test_when_no_parent(self): + n = _BSTNode(12) + self.assertRaises(AttributeError, n.is_left_child) + self.assertRaises(AttributeError, n.is_right_child) + self.assertIsNone(n.sibling) + self.assertIsNone(n.grandparent) + self.assertIsNone(n.uncle) + + def test_set_parent(self): + a = _BSTNode(12) + b = _BSTNode(14) + b.parent = a + + self.assertIs(b.parent, a) + self.assertEqual(a.count(), 1) + self.assertIsNone(a.parent) + self.assertIsNone(a.left) + self.assertIsNone(a.right) + + # If we just set the parent of a node + # the parent does NOT automatically have children. + self.assertFalse(a.has_children()) + self.assertFalse(a.has_one_child()) + self.assertFalse(a.has_two_children()) + + def test_when_no_children(self): + n = _BSTNode(12) + self.assertFalse(n.has_children()) + self.assertFalse(n.has_one_child()) + self.assertFalse(n.has_two_children()) + + def test_set_left_child(self): + a = _BSTNode(12) + b = _BSTNode(14) + a.left = b + + self.assertIs(a.left, b) + self.assertIsNone(b.parent) + self.assertEqual(a.count(), 2) + + self.assertTrue(a.has_children()) + self.assertTrue(a.has_one_child()) + self.assertFalse(a.has_two_children()) + + def test_set_right_child(self): + a = _BSTNode(12) + b = _BSTNode(28) + a.right = b + + self.assertIs(a.right, b) + self.assertEqual(a.count(), 2) + self.assertIsNone(b.parent) + + self.assertTrue(a.has_children()) + self.assertTrue(a.has_one_child()) + self.assertFalse(a.has_two_children()) + + def test_set_both_children(self): + a = _BSTNode(12) + a.left = _BSTNode(11) + a.right = _BSTNode(13) + self.assertEqual(a.count(), 3) + self.assertTrue(a.has_children()) + self.assertFalse(a.has_one_child()) + self.assertTrue(a.has_two_children()) + + def test_is_left_child(self): + a = _BSTNode(3) + b = _BSTNode(4) + a.left = b + b.parent = a + self.assertTrue(b.is_left_child()) + self.assertFalse(b.is_right_child()) + + def test_is_right_child(self): + a = _BSTNode(3) + b = _BSTNode(4) + a.right = b + b.parent = a + self.assertFalse(b.is_left_child()) + self.assertTrue(b.is_right_child()) + + def test_sibling(self): + p = _BSTNode(12) + l = _BSTNode(14) + r = _BSTNode(28) + + self.assertIsNone(r.sibling) + self.assertIsNone(l.sibling) + + p.left = l + p.right = r + l.parent = p + r.parent = p + + self.assertIs(l.sibling, r) + self.assertIs(r.sibling, l) + + # Without the parent pointers to its children, + # we can't determine if the children are siblings. + p.left = None + + self.assertIsNone(r.sibling) + self.assertIsNone(l.sibling) + + def test_grandparent(self): + a = _BSTNode(12) + b = _BSTNode(14) + c = _BSTNode(28) + + self.assertIsNone(a.grandparent) + self.assertIsNone(b.grandparent) + self.assertIsNone(c.grandparent) + + b.left = a + a.parent = b + + self.assertIsNone(a.grandparent) + + c.right = b + b.parent = c + + self.assertIsNone(b.grandparent) + self.assertIsNotNone(b.parent) + self.assertIsNone(c.grandparent) + self.assertIsNotNone(a.grandparent) + self.assertIs(a.grandparent, c) + + def test_uncle(self): + n = _BSTNode(12) + p = _BSTNode(14) + g = _BSTNode(28) + + n.parent = p + p.left = n + p.parent = g + g.right = p + + self.assertIsNotNone(n.parent) + self.assertIsNotNone(n.grandparent) + self.assertIsNone(n.sibling) + self.assertIsNone(n.uncle) + + u = _BSTNode(7) + g.left = u + u.parent = g + + self.assertIsNotNone(n.uncle) + self.assertIs(n.uncle, u) diff --git a/tests/ds/test_DisjointSetsForest.py b/tests/ds/test_DisjointSetsForest.py new file mode 100755 index 00000000..3f2c0579 --- /dev/null +++ b/tests/ds/test_DisjointSetsForest.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 22/02/2016 + +Updated: 08/03/2017 + +# Description + +Unit tests for the classes and functions in the andz.ds.DisjointSetsForest +module. +""" + +import unittest +from random import choice, randint + +from andz.ds.DisjointSetsForest import DisjointSetsForest, _DSFNode + + +class TestDSFNode(unittest.TestCase): + def test_creation(self): + n = _DSFNode(7) + self.assertTrue(n.is_root()) + self.assertEqual(n.parent, n) + self.assertEqual(n.next, n) + self.assertEqual(n.rank, 0) + self.assertEqual(n.value, 7) + + def test_creation_custom_rank(self): + n = _DSFNode(9, 101) + self.assertEqual(n.rank, 101) + + def test_repr(self): + n = _DSFNode(31) + self.assertEqual("(value: 31, rank: 0, parent: self)", repr(n)) + n.parent = "null" + self.assertEqual("(value: 31, rank: 0, parent: null)", repr(n)) + + def test_str(self): + n = _DSFNode(39) + self.assertEqual("39", str(n)) + + +class TestDSForests(unittest.TestCase): + def setUp(self): + self.d = DisjointSetsForest() + + def test_make_set_elem_already_exits(self): + self.d.make_set(3) + self.assertRaises(LookupError, self.d.make_set, 3) + + def test_make_set_one(self): + self.assertIsNone(self.d.make_set(3)) + self.assertEqual(self.d.size, 1) + self.assertEqual(self.d.sets, 1) + self.assertEqual(self.d.find(3), 3) + + def test_make_set_many(self): + n = randint(5, 11) + + for elem in range(n): + self.d.make_set(elem) + self.assertEqual(self.d.find(elem), elem) + + self.assertEqual(self.d.size, n) + self.assertEqual(self.d.sets, n) + + def test_contains(self): + self.assertFalse(self.d.contains(3)) + self.d.make_set(3) + self.assertTrue(self.d.contains(3)) + + def test_find_one_when_does_not_exist(self): + self.assertRaises(LookupError, self.d.find, 7) + + def test_find_one(self): + self.d.make_set(5) + self.assertEqual(self.d.find(5), 5) + + def test_find_two(self): + self.d.make_set(-11) + self.d.make_set(13) + self.assertEqual(self.d.find(-11), -11) + self.assertEqual(self.d.find(13), 13) + + def test_union_elements_do_not_exist(self): + self.d.make_set(7) + self.assertRaises(LookupError, self.d.union, 5, 7) + self.assertRaises(LookupError, self.d.union, 7, 5) + self.assertRaises(LookupError, self.d.union, 11, 5) + + def test_union_same_element(self): + self.d.make_set(51) + self.assertIsNone(self.d.union(51, 51)) + self.assertEqual(self.d.size, 1) + self.assertEqual(self.d.sets, 1) + + def test_union(self): + self.d.make_set(51) + self.d.make_set(53) + self.assertEqual(self.d.sets, 2) + self.assertIsNotNone(self.d.union(51, 53)) + self.assertEqual(self.d.size, 2) + self.assertEqual(self.d.sets, 1) + + def test_union_when_already_in_same_set(self): + self.d.make_set(17) + self.d.make_set(19) + self.d.union(17, 19) + self.assertIsNone(self.d.union(17, 19)) + self.assertEqual(self.d.size, 2) + self.assertEqual(self.d.sets, 1) + + def test_sequence_of_make_set_find_and_union(self): + + n = randint(43, 101) + ls = [] + + for _ in range(n): + + x = randint(-33, 77) + while self.d.contains(x): + x = randint(-33, 77) + ls.append(x) + + self.d.make_set(x) + + # While there's more than one set do a few unions + while self.d.sets > 1: + x = choice(ls) + y = choice(ls) + self.d.union(x, y) + + # Assert that all elements are still in the ds + for elem in ls: + self.assertIsNotNone(self.d.find(elem)) + self.assertTrue(self.d.contains(elem)) + + self.assertEqual(self.d.size, n) + + def test_print_set_when_elem_not_exist(self): + self.assertRaises(LookupError, self.d.print_set, 3) + + def test_print_set(self): + for i in range(1, 17): + self.d.make_set(i) + + self.d.print_set(3) + + # Merge i and i+1 into the same set. + for i in range(1, 16, 2): + self.d.union(i, i + 1) + + self.d.print_set(3) + + for i in range(2, 15, 4): + self.d.union(i, i + 2) + + self.d.union(4, 7) + self.d.union(10, 16) + self.d.union(8, 13) + + self.d.print_set(3) diff --git a/tests/ds/test_LinearProbingHashTable.py b/tests/ds/test_LinearProbingHashTable.py new file mode 100755 index 00000000..e765053d --- /dev/null +++ b/tests/ds/test_LinearProbingHashTable.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 21/02/2016 + +Updated: 19/02/2017 + +# Description + +Unit tests for the classes and functions in the andz.ds.LinearProbingHashTable +module. + +# Reference + +- https://stackoverflow.com/q/9755538/3924118 +""" + +import string +import unittest +from random import choice, randint, sample, shuffle + +from andz.ds.LinearProbingHashTable import ( + LinearProbingHashTable, + has_duplicates_ignore_nones, +) + + +def gen_rand_list_of_distinct_ascii_and_numbers() -> list: + n = randint(1, 1000) + ls = list(string.ascii_lowercase) + sample(range(n), n) + shuffle(ls) + return ls + + +class TestHasDuplicatesIgnoreNones(unittest.TestCase): + def test_empty_list(self): + self.assertFalse(has_duplicates_ignore_nones([])) + + def test_list_size_1_no_None(self): + self.assertFalse(has_duplicates_ignore_nones([3])) + + def test_list_size_1_None(self): + self.assertFalse(has_duplicates_ignore_nones([None])) + + def test_list_size_2_no_None_no_duplicate(self): + self.assertFalse(has_duplicates_ignore_nones([3, 5])) + + def test_list_size_2_no_None_with_duplicates(self): + self.assertTrue(has_duplicates_ignore_nones([5, 5])) + + def test_list_size_2_with_None_no_duplicate(self): + self.assertFalse(has_duplicates_ignore_nones([None, 5])) + + def test_list_size_2_both_None(self): + self.assertFalse(has_duplicates_ignore_nones([None, None])) + + def test_list_size_n_all_None(self): + self.assertFalse(has_duplicates_ignore_nones([None] * randint(3, 100))) + + def test_list_size_n_no_None_no_duplicate(self): + self.assertFalse(has_duplicates_ignore_nones(sample(range(100), 100))) + + def test_list_has_duplicates_on_bounds(self): + self.assertTrue(has_duplicates_ignore_nones([3, 12, 4, 6, 3])) + + def test_list_has_duplicates_not_on_bounds(self): + self.assertTrue(has_duplicates_ignore_nones([3, 12, 3, 6, 17])) + + +class TestLinearProbingHashTable(unittest.TestCase): + def test_create_capacity_not_int(self): + self.assertRaises(TypeError, LinearProbingHashTable, 3.14) + self.assertRaises(TypeError, LinearProbingHashTable, "not at int") + + def test_create_capacity_less_than_1(self): + self.assertRaises(ValueError, LinearProbingHashTable, 0) + self.assertRaises(ValueError, LinearProbingHashTable, -1) + + def test_create_set_initial_capacity(self): + t = LinearProbingHashTable(9) + self.assertEqual(t.capacity, 9) + self.assertEqual(t.size, 0) + + def test_create_capacity_int(self): + t = LinearProbingHashTable() + self.assertEqual(t.size, 0) + + def test_get_key_None(self): + t = LinearProbingHashTable() + self.assertRaises(TypeError, t.get, None) + + def test_get_non_hashable_type(self): + t = LinearProbingHashTable() + t.put(23, 31) + t.put(11, 13) + self.assertRaises(TypeError, t.get, []) + self.assertRaises(TypeError, t.get, {}) + + def test_get_empty_table(self): + t = LinearProbingHashTable() + self.assertIsNone(t.get(3)) + + def test_get_with_syntactic_sugar(self): + t = LinearProbingHashTable() + t.put(5, 12) + self.assertEqual(t[5], 12) + + def test_get_no_key_found(self): + t = LinearProbingHashTable() + t.put("three", 3) + t["four"] = 4 + self.assertIsNone(t.get("five")) + + def test_get_all(self): + t = LinearProbingHashTable() + + ls = gen_rand_list_of_distinct_ascii_and_numbers() + + for elem in ls: + t.put(elem, 13) + + for elem in reversed(ls): + self.assertEqual(t.get(elem), 13) + + def test_put_key_None(self): + t = LinearProbingHashTable() + self.assertRaises(TypeError, t.put, None, 5) + + def test_put_non_hashable_type(self): + t = LinearProbingHashTable() + self.assertRaises(TypeError, t.put, [], 12) + self.assertRaises(TypeError, t.put, {}, None) + + def test_put_key_not_None_value_None(self): + t = LinearProbingHashTable() + t.put(3, None) + self.assertTrue(t.size, 1) + self.assertIsNone(t.get(3)) + + def test_put_key_not_None_value_not_None(self): + t = LinearProbingHashTable() + t.put(5, 19) + self.assertTrue(t.size, 1) + self.assertEqual(t.get(5), 19) + + def test_put_same_key_multiple_times(self): + t = LinearProbingHashTable() + t.put(3, "three") + t.put(5, 6) + t.put(3, 3) + t.put(3, "three") + self.assertEqual(t.size, 2) + self.assertEqual(t.get(3), "three") + + def test_put_n_distinct_keys_equal_values(self): + t = LinearProbingHashTable() + + n = randint(2, 1000) + population = sample(range(n), n) + + for elem in population: + t.put(elem, elem) + + self.assertEqual(t.size, n) + + for elem in population: + self.assertIsNotNone(t.get(elem)) + + def test_put_n_distinct_keys_all_values_different(self): + """Testing that the same elements inserted + multiple times in the same order, + but always with different values associated with them.""" + t = LinearProbingHashTable() + + ls = gen_rand_list_of_distinct_ascii_and_numbers() + n = len(ls) + + for val, key in enumerate(ls): + t.put(key, val) + + self.assertEqual(t.size, n) + for elem in ls: + self.assertIsNotNone(t.get(elem)) + + def test_delete_key_None(self): + t = LinearProbingHashTable() + self.assertRaises(TypeError, t.delete, None) + + def test_delete_non_hashable_type(self): + t = LinearProbingHashTable() + self.assertRaises(TypeError, t.delete, []) + self.assertRaises(TypeError, t.delete, {}) + + def test_delete_empty_table(self): + t = LinearProbingHashTable() + self.assertIsNone(t.delete(3)) + + def test_delete_key_not_present(self): + t = LinearProbingHashTable() + t.put(-10, "testing deletion when key not in the table") + self.assertIsNone(t.delete(7)) + + def test_delete_some(self): + t = LinearProbingHashTable() + ls = [(1, 3), (5, "two"), (2.72, 10), ("one", 3.14), (1, "seven")] + + for k, v in ls: + t.put(k, v) + + self.assertEqual(t.size, 4) + self.assertEqual(t.delete(1), "seven") + self.assertEqual(t.delete(2.72), 10) + self.assertEqual(t.size, 2) + + def test_delete_all(self): + t = LinearProbingHashTable() + ls = gen_rand_list_of_distinct_ascii_and_numbers() + for elem in ls: + t.put(elem, elem) + + self.assertEqual(t.size, len(ls)) + + for key in ls: + self.assertEqual(t.delete(key), key) + + self.assertEqual(t.size, 0) + + def test_show(self): + t = LinearProbingHashTable() + ls = sample(range(3), 3) + for elem in ls: + t.put(elem, choice(string.ascii_letters)) + print() + t.show() diff --git a/tests/ds/test_MaxHeap.py b/tests/ds/test_MaxHeap.py new file mode 100755 index 00000000..a87fcffd --- /dev/null +++ b/tests/ds/test_MaxHeap.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 17/02/2016 + +Updated: 12/03/2017 + +# Description + +Unit tests for the classes and functions in the andz.ds.MaxHeap module. +""" + +import unittest +from random import choice, randint, sample + +from andz.ds.MaxHeap import MaxHeap, is_max_heap + + +class TestMaxHeap(unittest.TestCase): + def test_heap_creation_default(self): + h = MaxHeap() + self.assertTrue(h.is_empty()) + self.assertEqual(h.size, 0) + + def test_heap_creation_given_list(self): + a = [12, 14, 28, 6, 7, 10, 18] + h = MaxHeap(a) + self.assertFalse(h.is_empty()) + self.assertEqual(h.size, len(a)) + + def test_clear_empty_heap(self): + h = MaxHeap() + self.assertIsNone(h.clear()) + self.assertEqual(h.size, 0) + self.assertTrue(h.is_empty()) + + def test_clear_heap_of_random_size(self): + h = MaxHeap([randint(-100, 100) for _ in range(100)]) + self.assertIsNone(h.clear()) + self.assertEqual(h.size, 0) + self.assertTrue(h.is_empty()) + + def test_add_when_argument_is_None(self): + h = MaxHeap() + self.assertRaises(ValueError, h.add, None) + + def test_add_add_one(self): + h = MaxHeap() + self.assertIsNone(h.add(2)) + self.assertEqual(h.size, 1) + self.assertFalse(h.is_empty()) + self.assertEqual(h.find_max(), 2) + + def test_add_multiple_elements(self): + a = [randint(-100, 100) for _ in range(100)] + h = MaxHeap() + + for i, elem in enumerate(a): + self.assertIsNone(h.add(elem)) + self.assertEqual(h.size, i + 1) + + self.assertFalse(h.is_empty()) + self.assertEqual(h.find_max(), max(a)) + + def test_contains_when_argument_is_None(self): + h = MaxHeap() + self.assertRaises(ValueError, h.contains, None) + + def test_contains_when_empty_heap(self): + h = MaxHeap() + self.assertFalse(h.contains(3)) + + def test_contains_true(self): + h = MaxHeap([6, 8, 2, 2, 60, 7, 9]) + self.assertTrue(h.contains(2)) + + def test_contains_false(self): + h = MaxHeap([6, 8, 2, 60, 7, 9, 3, 67]) + self.assertFalse(h.contains(10)) + + def test_delete_when_argument_is_None(self): + self.assertRaises(ValueError, MaxHeap().delete, None) + + def test_delete_when_elem_does_not_exist(self): + self.assertRaises(LookupError, MaxHeap().delete, 3) + + def test_delete_when_elem_is_last(self): + h = MaxHeap([3, 4]) + self.assertIsNone(h.delete(4)) + self.assertTrue(is_max_heap(h)) + self.assertEqual(h.size, 1) + self.assertFalse(h.is_empty()) + + def test_delete_all_when_heap_of_random_size(self): + size = randint(3, 100) + a = [randint(-100, 100) for _ in range(size)] + h = MaxHeap(a) + + for _ in range(size): + self.assertIsNone(h.delete(choice(a))) + self.assertTrue(is_max_heap(h)) + + self.assertEqual(h.size, 0) + self.assertTrue(h.is_empty()) + + def test_find_max_when_empty_heap(self): + h = MaxHeap() + self.assertIsNone(h.find_max()) + + def test_find_max_when_heap_has_size_1(self): + h = MaxHeap([5]) + self.assertEqual(h.find_max(), 5) + + def test_find_max_when_heap_has_size_2(self): + h = MaxHeap([13, 7]) + self.assertEqual(h.find_max(), 13) + + def test_find_max_when_heap_has_random_size(self): + a = [randint(-100, 100) for _ in range(3, 100)] + h = MaxHeap(a) + self.assertEqual(h.find_max(), max(a)) + + def test_remove_max_when_empty_heap(self): + h = MaxHeap() + self.assertIsNone(h.remove_max()) + + def test_remove_max_when_heap_has_size_1(self): + h = MaxHeap([13]) + self.assertEqual(h.remove_max(), 13) + self.assertTrue(h.is_empty()) + self.assertEqual(h.size, 0) + + def test_remove_max_when_heap_has_size_2(self): + h = MaxHeap([11, 13]) + self.assertEqual(h.remove_max(), 13) + self.assertFalse(h.is_empty()) + self.assertEqual(h.size, 1) + + def test_remove_max_when_heap_has_random_size(self): + size = randint(3, 100) + a = [randint(-100, 100) for _ in range(size)] + h = MaxHeap(a) + m = max(a) + self.assertEqual(h.remove_max(), m) + self.assertFalse(h.is_empty()) + self.assertEqual(h.size, size - 1) + + def test_merge_empty_heap_with_empty_heap(self): + a = MaxHeap() + b = MaxHeap() + self.assertIsNone(a.merge(b)) + + def test_merge_empty_heap_with_non_empty_heap(self): + a = MaxHeap() + ls = [-3, 5, 7, 9, 1, 5, 2] + b = MaxHeap(ls) + self.assertIsNone(a.merge(b)) + self.assertEqual(a.size, len(ls)) + self.assertEqual(b.size, len(ls)) + + def test_merge_non_empty_heap_with_empty_heap(self): + ls = [-3, 5, 7, 9, 1, 5, 2] + a = MaxHeap(ls) + b = MaxHeap() + self.assertIsNone(a.merge(b)) + self.assertEqual(a.size, len(ls)) + self.assertEqual(b.size, 0) + self.assertTrue(b.is_empty()) + + def test_merge_non_empty_heap_with_non_empty_heap(self): + ls = [-3, 5, 7, 9, 1, 5, 2] + size = len(ls) + a = MaxHeap(ls) + b = MaxHeap(sample(ls, size)) + self.assertIsNone(a.merge(b)) + self.assertEqual(a.size, size * 2) + self.assertEqual(b.size, size) diff --git a/tests/ds/test_MinHeap.py b/tests/ds/test_MinHeap.py new file mode 100755 index 00000000..86f4b9e9 --- /dev/null +++ b/tests/ds/test_MinHeap.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 14/02/2016 + +Updated: 12/03/2017 + +# Description + +Unit tests for the classes and functions in the andz.ds.MinHeap module. +""" + +import unittest +from random import choice, randint, sample + +from andz.ds.MinHeap import MinHeap, is_min_heap + + +class TestMinHeap(unittest.TestCase): + def test_heap_creation_default(self): + h = MinHeap() + self.assertTrue(h.is_empty()) + self.assertEqual(h.size, 0) + + def test_heap_creation_given_list(self): + a = [12, 14, 28, 6, 7, 10, 18] + h = MinHeap(a) + self.assertFalse(h.is_empty()) + self.assertEqual(h.size, len(a)) + + def test_clear_empty_heap(self): + h = MinHeap() + self.assertIsNone(h.clear()) + self.assertEqual(h.size, 0) + self.assertTrue(h.is_empty()) + + def test_clear_heap_of_random_size(self): + h = MinHeap([randint(-100, 100) for _ in range(100)]) + self.assertIsNone(h.clear()) + self.assertEqual(h.size, 0) + self.assertTrue(h.is_empty()) + + def test_add_when_argument_is_None(self): + h = MinHeap() + self.assertRaises(ValueError, h.add, None) + + def test_add_add_one(self): + h = MinHeap() + self.assertIsNone(h.add(2)) + self.assertEqual(h.size, 1) + self.assertFalse(h.is_empty()) + self.assertEqual(h.find_min(), 2) + + def test_add_multiple_elements(self): + a = [randint(-100, 100) for _ in range(100)] + h = MinHeap() + + for i, elem in enumerate(a): + self.assertIsNone(h.add(elem)) + self.assertEqual(h.size, i + 1) + + self.assertFalse(h.is_empty()) + self.assertEqual(h.find_min(), min(a)) + + def test_contains_when_argument_is_None(self): + h = MinHeap() + self.assertRaises(ValueError, h.contains, None) + + def test_contains_when_empty_heap(self): + h = MinHeap() + self.assertFalse(h.contains(3)) + + def test_contains_true(self): + h = MinHeap([6, 8, 2, 2, 60, 7, 9]) + self.assertTrue(h.contains(2)) + + def test_contains_false(self): + h = MinHeap([6, 8, 2, 60, 7, 9, 3, 67]) + self.assertFalse(h.contains(10)) + + def test_delete_when_argument_is_None(self): + self.assertRaises(ValueError, MinHeap().delete, None) + + def test_delete_when_elem_does_not_exist(self): + self.assertRaises(LookupError, MinHeap().delete, 3) + + def test_delete_when_elem_is_last(self): + h = MinHeap([3, 4]) + self.assertIsNone(h.delete(4)) + self.assertTrue(is_min_heap(h)) + self.assertEqual(h.size, 1) + self.assertFalse(h.is_empty()) + + def test_delete_all_when_heap_of_random_size(self): + size = randint(3, 100) + a = [randint(-100, 100) for _ in range(size)] + h = MinHeap(a) + + for _ in range(size): + self.assertIsNone(h.delete(choice(a))) + self.assertTrue(is_min_heap(h)) + + self.assertEqual(h.size, 0) + self.assertTrue(h.is_empty()) + + def test_find_min_when_empty_heap(self): + h = MinHeap() + self.assertIsNone(h.find_min()) + + def test_find_min_when_heap_has_size_1(self): + h = MinHeap([5]) + self.assertEqual(h.find_min(), 5) + + def test_find_min_when_heap_has_size_2(self): + h = MinHeap([13, 7]) + self.assertEqual(h.find_min(), 7) + + def test_find_min_when_heap_has_random_size(self): + a = [randint(-100, 100) for _ in range(3, 100)] + h = MinHeap(a) + self.assertEqual(h.find_min(), min(a)) + + def test_remove_min_when_empty_heap(self): + h = MinHeap() + self.assertIsNone(h.remove_min()) + + def test_remove_min_when_heap_has_size_1(self): + h = MinHeap([13]) + self.assertEqual(h.remove_min(), 13) + self.assertTrue(h.is_empty()) + self.assertEqual(h.size, 0) + + def test_remove_min_when_heap_has_size_2(self): + h = MinHeap([11, 13]) + self.assertEqual(h.remove_min(), 11) + self.assertFalse(h.is_empty()) + self.assertEqual(h.size, 1) + + def test_remove_min_when_heap_has_random_size(self): + size = randint(3, 100) + a = [randint(-100, 100) for _ in range(size)] + h = MinHeap(a) + m = min(a) + self.assertEqual(h.remove_min(), m) + self.assertFalse(h.is_empty()) + self.assertEqual(h.size, size - 1) + + def test_merge_empty_heap_with_empty_heap(self): + a = MinHeap() + b = MinHeap() + self.assertIsNone(a.merge(b)) + + def test_merge_empty_heap_with_non_empty_heap(self): + a = MinHeap() + ls = [-3, 5, 7, 9, 1, 5, 2] + b = MinHeap(ls) + self.assertIsNone(a.merge(b)) + self.assertEqual(a.size, len(ls)) + self.assertEqual(b.size, len(ls)) + + def test_merge_non_empty_heap_with_empty_heap(self): + ls = [-3, 5, 7, 9, 1, 5, 2] + a = MinHeap(ls) + b = MinHeap() + self.assertIsNone(a.merge(b)) + self.assertEqual(a.size, len(ls)) + self.assertEqual(b.size, 0) + self.assertTrue(b.is_empty()) + + def test_merge_non_empty_heap_with_non_empty_heap(self): + ls = [-3, 5, 7, 9, 1, 5, 2] + size = len(ls) + a = MinHeap(ls) + b = MinHeap(sample(ls, size)) + self.assertIsNone(a.merge(b)) + self.assertEqual(a.size, size * 2) + self.assertEqual(b.size, size) diff --git a/tests/ds/test_MinMaxHeap.py b/tests/ds/test_MinMaxHeap.py new file mode 100755 index 00000000..62af8afa --- /dev/null +++ b/tests/ds/test_MinMaxHeap.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 20/02/2016 + +Updated: 23/03/2024 + +# Description + +Unit tests for the classes and functions in the andz.ds.MinMaxHeap module. +""" + +import unittest +from random import choice, randint, sample + +from andz.ds.MinMaxHeap import MinMaxHeap, is_min_max_heap + + +class TestMinMaxHeap(unittest.TestCase): + def test_heap_creation_default(self): + h = MinMaxHeap() + self.assertTrue(h.is_empty()) + self.assertEqual(h.size, 0) + + def test_heap_creation_given_list(self): + a = [12, 14, 28, 6, 7, 10, 18] + h = MinMaxHeap(a) + self.assertFalse(h.is_empty()) + self.assertEqual(h.size, len(a)) + + def test_clear_empty_heap(self): + h = MinMaxHeap() + self.assertIsNone(h.clear()) + self.assertEqual(h.size, 0) + self.assertTrue(h.is_empty()) + + def test_clear_heap_of_random_size(self): + h = MinMaxHeap([randint(-100, 100) for _ in range(100)]) + self.assertIsNone(h.clear()) + self.assertEqual(h.size, 0) + self.assertTrue(h.is_empty()) + + def test_add_when_argument_is_None(self): + h = MinMaxHeap() + self.assertRaises(ValueError, h.add, None) + + def test_add_add_one(self): + h = MinMaxHeap() + self.assertIsNone(h.add(2)) + self.assertEqual(h.size, 1) + self.assertFalse(h.is_empty()) + self.assertEqual(h.find_max(), 2) + self.assertEqual(h.find_min(), 2) + + def test_add_multiple_elements(self): + a = [randint(-100, 100) for _ in range(100)] + h = MinMaxHeap() + + for i, elem in enumerate(a): + self.assertIsNone(h.add(elem)) + self.assertEqual(h.size, i + 1) + + self.assertFalse(h.is_empty()) + self.assertEqual(h.find_max(), max(a)) + self.assertEqual(h.find_min(), min(a)) + + def test_contains_when_argument_is_None(self): + h = MinMaxHeap() + self.assertRaises(ValueError, h.contains, None) + + def test_contains_when_empty_heap(self): + h = MinMaxHeap() + self.assertFalse(h.contains(3)) + + def test_contains_true(self): + h = MinMaxHeap([6, 8, 2, 2, 60, 7, 9]) + self.assertTrue(h.contains(2)) + + def test_contains_false(self): + h = MinMaxHeap([6, 8, 2, 60, 7, 9, 3, 67]) + self.assertFalse(h.contains(10)) + + def test_delete_when_argument_is_None(self): + self.assertRaises(ValueError, MinMaxHeap().delete, None) + + def test_delete_when_elem_does_not_exist(self): + self.assertRaises(LookupError, MinMaxHeap().delete, 3) + + def test_delete_when_elem_is_last(self): + h = MinMaxHeap([3, 4]) + self.assertIsNone(h.delete(4)) + self.assertTrue(is_min_max_heap(h)) + self.assertEqual(h.size, 1) + self.assertFalse(h.is_empty()) + + def test_delete_all_when_heap_of_random_size(self): + size = randint(3, 100) + a = [randint(-100, 100) for _ in range(size)] + h = MinMaxHeap(a) + + for _ in range(size): + self.assertIsNone(h.delete(choice(a))) + self.assertTrue(is_min_max_heap(h)) + + self.assertEqual(h.size, 0) + self.assertTrue(h.is_empty()) + + # Testing find_max and remove_max + + def test_find_max_when_empty_heap(self): + h = MinMaxHeap() + self.assertIsNone(h.find_max()) + + def test_find_max_when_heap_has_size_1(self): + h = MinMaxHeap([5]) + self.assertEqual(h.find_max(), 5) + + def test_find_max_when_heap_has_size_2(self): + h = MinMaxHeap([13, 7]) + self.assertEqual(h.find_max(), 13) + + def test_find_max_when_heap_has_random_size(self): + a = [randint(-100, 100) for _ in range(3, 100)] + h = MinMaxHeap(a) + self.assertEqual(h.find_max(), max(a)) + + def test_remove_max_when_empty_heap(self): + h = MinMaxHeap() + self.assertIsNone(h.remove_max()) + + def test_remove_max_when_heap_has_size_1(self): + h = MinMaxHeap([13]) + self.assertEqual(h.remove_max(), 13) + self.assertTrue(h.is_empty()) + self.assertEqual(h.size, 0) + + def test_remove_max_when_heap_has_size_2(self): + h = MinMaxHeap([11, 13]) + self.assertEqual(h.remove_max(), 13) + self.assertFalse(h.is_empty()) + self.assertEqual(h.size, 1) + + def test_remove_max_when_heap_has_random_size(self): + size = randint(3, 100) + a = [randint(-100, 100) for _ in range(size)] + h = MinMaxHeap(a) + m = max(a) + self.assertEqual(h.remove_max(), m) + self.assertFalse(h.is_empty()) + self.assertEqual(h.size, size - 1) + + # Testing find_min and remove_min + + def test_find_min_when_empty_heap(self): + h = MinMaxHeap() + self.assertIsNone(h.find_min()) + + def test_find_min_when_heap_has_size_1(self): + h = MinMaxHeap([5]) + self.assertEqual(h.find_min(), 5) + + def test_find_min_when_heap_has_size_2(self): + h = MinMaxHeap([13, 7]) + self.assertEqual(h.find_min(), 7) + + def test_find_min_when_heap_has_random_size(self): + a = [randint(-100, 100) for _ in range(3, 100)] + h = MinMaxHeap(a) + self.assertEqual(h.find_min(), min(a)) + + def test_remove_min_when_empty_heap(self): + h = MinMaxHeap() + self.assertIsNone(h.remove_min()) + + def test_remove_min_when_heap_has_size_1(self): + h = MinMaxHeap([13]) + self.assertEqual(h.remove_min(), 13) + self.assertTrue(h.is_empty()) + self.assertEqual(h.size, 0) + + def test_remove_min_when_heap_has_size_2(self): + h = MinMaxHeap([11, 13]) + self.assertEqual(h.remove_min(), 11) + self.assertFalse(h.is_empty()) + self.assertEqual(h.size, 1) + + def test_remove_min_when_heap_has_random_size(self): + size = randint(3, 100) + a = [randint(-100, 100) for _ in range(size)] + h = MinMaxHeap(a) + m = min(a) + self.assertEqual(h.remove_min(), m) + self.assertFalse(h.is_empty()) + self.assertEqual(h.size, size - 1) + + def test_merge_empty_heap_with_empty_heap(self): + a = MinMaxHeap() + b = MinMaxHeap() + self.assertIsNone(a.merge(b)) + + def test_merge_empty_heap_with_non_empty_heap(self): + a = MinMaxHeap() + ls = [-3, 5, 7, 9, 1, 5, 2] + b = MinMaxHeap(ls) + self.assertIsNone(a.merge(b)) + self.assertEqual(a.size, len(ls)) + self.assertEqual(b.size, len(ls)) + + def test_merge_non_empty_heap_with_empty_heap(self): + ls = [-3, 5, 7, 9, 1, 5, 2] + a = MinMaxHeap(ls) + b = MinMaxHeap() + self.assertIsNone(a.merge(b)) + self.assertEqual(a.size, len(ls)) + self.assertEqual(b.size, 0) + self.assertTrue(b.is_empty()) + + def test_merge_non_empty_heap_with_non_empty_heap(self): + ls = [-3, 5, 7, 9, 1, 5, 2] + size = len(ls) + a = MinMaxHeap(ls) + b = MinMaxHeap(sample(ls, size)) + self.assertIsNone(a.merge(b)) + self.assertEqual(a.size, size * 2) + self.assertEqual(b.size, size) diff --git a/tests/ds/test_Queue.py b/tests/ds/test_Queue.py new file mode 100644 index 00000000..47bfe3a7 --- /dev/null +++ b/tests/ds/test_Queue.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 24/01/2017 + +Updated: 12/03/2017 + +# Description + +Unit tests for the classes and functions in the andz.ds.Queue module. +""" + +import unittest +from random import randint + +from andz.ds.Queue import Queue + + +class TestQueue(unittest.TestCase): + """Size and is_empty are somehow tested implicitly.""" + + def test_creation(self): + q = Queue() + self.assertTrue(q.is_empty()) + + def test_creation_explicit_None(self): + q = Queue(None) + self.assertTrue(q.is_empty()) + + def test_creation_not_iterable(self): + self.assertRaises(TypeError, Queue, 13) + + def test_creation_good_list_empty(self): + q = Queue([]) + self.assertTrue(q.is_empty()) + + def test_creation_good_list_size_1(self): + q = Queue([3]) + self.assertEqual(q.size, 1) + + def test_creation_list_with_None(self): + self.assertRaises(ValueError, Queue, [31, None, 2, 3]) + + def test_creation_good_list_random_size(self): + r = randint(2, 50) + q = Queue([randint(-10, 10) for _ in range(r)]) + self.assertEqual(q.size, r) + + def test_enqueue_one(self): + q = Queue() + q.enqueue("first") + self.assertEqual(q.size, 1) + + def test_enqueue_None(self): + q = Queue([93, 97]) + self.assertRaises(ValueError, q.enqueue, None) + + def test_enqueue_many(self): + q = Queue() + + r = randint(2, 100) + ls = [randint(-100, 100) for _ in range(r)] + + for i, elem in enumerate(ls): + q.enqueue(elem) + self.assertEqual(q.size, i + 1) + + def test_dequeue_empty(self): + q = Queue() + self.assertIsNone(q.dequeue()) + + def test_dequeue_one(self): + q = Queue(["one"]) + self.assertEqual(q.dequeue(), "one") + self.assertTrue(q.is_empty()) + + def test_dequeue_many(self): + ls = [2, 3, 5, 7, 11, 13] + q = Queue(ls) + + for i, _ in enumerate(ls): + elem = q.dequeue() + self.assertEqual(elem, ls[i]) + + self.assertTrue(q.is_empty()) diff --git a/tests/ds/test_RBT.py b/tests/ds/test_RBT.py new file mode 100755 index 00000000..c27dbaa1 --- /dev/null +++ b/tests/ds/test_RBT.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 15/02/2016 + +Updated: 14/03/2017 + +# Description + +Unit tests for the classes and functions in the andz.ds.RBT module. +""" + +from andz.ds.RBT import BLACK, RBT, RED, _RBTNode +from tests.ds.test_BST import TestBST, TestBSTNode + +# Only testing new functionality with respect to _BSTNode + + +class TestRBTNode(TestBSTNode): + def test_default_color(self): + n = _RBTNode(3) + self.assertEqual(n.color, BLACK) + + def test_set_color(self): + n = _RBTNode(3) + n.color = RED + self.assertEqual(n.color, RED) + + +class TestRBT(TestBST): + def setUp(self): + self.t = RBT() diff --git a/tests/ds/test_Stack.py b/tests/ds/test_Stack.py new file mode 100644 index 00000000..c0924918 --- /dev/null +++ b/tests/ds/test_Stack.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 01/07/16 + +Updated: 12/03/2017 + +# Description + +Unit tests for the classes and functions in the andz.ds.Stack module. +""" + +import unittest +from random import randint + +from andz.ds.Stack import Stack + + +class TestStack(unittest.TestCase): + def test_creation_default(self): + s = Stack() + self.assertEqual(s.size, 0) + self.assertTrue(s.is_empty()) + + def test_creation_good_list(self): + s = Stack(["first", 2, 3.14]) + self.assertEqual(s.size, 3) + self.assertFalse(s.is_empty()) + + def test_creation_list_with_None(self): + self.assertRaises(ValueError, Stack, ["first", 2, None, 3.14, None]) + + def test_creation_argument_not_iterable(self): + self.assertRaises(TypeError, Stack, 2.72) + + def test_top_empty_stack(self): + s = Stack() + self.assertIsNone(s.top()) + + def test_push_None(self): + s = Stack() + self.assertRaises(ValueError, s.push, None) + + def test_push_one(self): + s = Stack() + s.push(3) + self.assertEqual(s.size, 1) + self.assertFalse(s.is_empty()) + self.assertEqual(s.top(), 3) + + def test_push_many(self): + s = Stack() + + for i in range(randint(2, 100)): + s.push(i) + self.assertEqual(s.size, i + 1) + self.assertFalse(s.is_empty()) + self.assertEqual(s.top(), i) + + def test_pop_empty_stack(self): + s = Stack() + self.assertIsNone(s.pop()) + + def test_pop_last(self): + s = Stack([7]) + self.assertEqual(s.pop(), 7) + self.assertTrue(s.is_empty()) + self.assertEqual(s.size, 0) + + def test_pop_until_empty(self): + ls = [randint(-10, 10) for _ in range(randint(1, 100))] + s = Stack(ls) + + for i, e in enumerate(reversed(ls)): + elem = s.pop() + self.assertEqual(elem, e) + self.assertEqual(s.size, len(ls) - (i + 1)) + + self.assertTrue(s.is_empty()) diff --git a/tests/ds/test_TST.py b/tests/ds/test_TST.py new file mode 100644 index 00000000..65e552a7 --- /dev/null +++ b/tests/ds/test_TST.py @@ -0,0 +1,500 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +# Meta-info + +Author: Nelson Brochado + +Created: 29/01/2017 + +Updated: 12/03/2017 + +# Description + +Unit tests for the classes and functions in the andz.ds.TST module. +""" + +import random +import string +import unittest + +from andz.ds.TST import TST, _TSTNode + + +class TestTST(unittest.TestCase): + @staticmethod + def gen_rand_str(n): + """Generates a string of size n of printable characters.""" + return "".join(random.choice(string.ascii_letters) for _ in range(n)) + + def test_creation(self): + t = TST() + self.assertTrue(t.is_empty()) + self.assertEqual(t.count(), 0) + + def test_insert_key_not_string(self): + t = TST() + self.assertRaises(TypeError, t.insert, 10, 5) + + def test_insert_key_empty_string(self): + t = TST() + self.assertRaises(ValueError, t.insert, "", 2) + + def test_insert_none_value(self): + t = TST() + self.assertRaises(ValueError, t.insert, "key", None) + + def test_insert_one(self): + t = TST() + t.insert("one", 97) + self.assertFalse(t.is_empty()) + self.assertEqual(t.count(), 1) + self.assertEqual(t.search("one"), 97) + self.assertTrue(t.contains("one")) + + def test_insert_two(self): + t = TST() + t.insert("he", 0) + t.insert("she", 1) + self.assertFalse(t.is_empty()) + self.assertEqual(t.count(), 2) + self.assertEqual(t.search("he"), 0) + self.assertEqual(t.search("she"), 1) + self.assertTrue(t.contains("he")) + self.assertTrue(t.contains("she")) + + def test_insert_same_twice_to_update(self): + t = TST() + t.insert("seven", 7) + t.insert("fly away", 11) + t.insert("fly away", 101) + t.insert("bandit queen", "Looptroop") + self.assertFalse(t.is_empty()) + self.assertEqual(t.count(), 3) + self.assertEqual(t.search("seven"), 7) + self.assertEqual(t.search("fly away"), 101) + self.assertEqual(t.search("bandit queen"), "Looptroop") + self.assertTrue(t.contains("seven")) + self.assertTrue(t.contains("fly away")) + self.assertTrue(t.contains("bandit queen")) + + def test_insert_random_keys(self): + t = TST() + + n = random.randint(4, 100) + random_pairs = {} + + for _ in range(n): + key = TestTST.gen_rand_str(random.randint(1, 11)) + random_pairs[key] = key + t.insert(key, key) + + self.assertFalse(t.is_empty()) + self.assertEqual(t.count(), len(random_pairs)) + + for k, v in random_pairs.items(): + self.assertEqual(t.search(k), v) + self.assertTrue(t.contains(k)) + + # Testing search and contains_key in the "bad" cases (of inputs) + + def test_search_empty_tst(self): + t = TST() + self.assertIsNone(t.search("search in an empty tst")) + self.assertIsNone(t.search_iteratively("search in an empty tst")) + + def test_search_key_not_string(self): + t = TST() + self.assertRaises(TypeError, t.search, 5) + self.assertRaises(TypeError, t.search_iteratively, 5) + + def test_search_key_empty_string(self): + t = TST() + self.assertRaises(ValueError, t.search, "") + self.assertRaises(ValueError, t.search_iteratively, "") + + def test_contains_key_not_string(self): + t = TST() + self.assertRaises(TypeError, t.contains, 3.14) + + def test_contains_empty_tst(self): + t = TST() + self.assertFalse(t.contains("contains in an empty tst")) + + def test_contains_key_empty_string(self): + t = TST() + self.assertRaises(ValueError, t.contains, "") + + def test_traverse_tst(self): + t = TST() + t.insert("one", 1) + t.insert("two", 2) + t.insert("three", 3) + self.assertIsNone(t.traverse()) + + def test_delete_empty_tst(self): + t = TST() + self.assertIsNone(t.delete("war")) + + def test_delete_key_not_string(self): + t = TST() + self.assertRaises(TypeError, t.delete, 0.1) + + def test_delete_key_empty_string(self): + t = TST() + self.assertRaises(ValueError, t.delete, "") + + def test_delete_inexistent_key(self): + t = TST() + t.insert("first", "1st") + + self.assertIsNone(t.delete("second")) + self.assertFalse(t.is_empty()) + self.assertEqual(t.count(), 1) + self.assertTrue(t.contains("first")) + self.assertEqual(t.search("first"), "1st") + + def test_delete_same_key_twice(self): + t = TST() + t.insert("one", 1) + t.insert("two", 2) + t.insert("three", 3) + + self.assertEqual(t.delete("three"), 3) + self.assertIsNone(t.delete("three")) + self.assertFalse(t.is_empty()) + self.assertEqual(t.count(), 2) + self.assertTrue(t.contains("one")) + self.assertTrue(t.contains("two")) + self.assertEqual(t.search("one"), 1) + self.assertEqual(t.search("two"), 2) + + def test_delete_the_only_key(self): + t = TST() + t.insert("seven", 7) + + self.assertEqual(t.delete("seven"), 7) + self.assertTrue(t.is_empty()) + self.assertEqual(t.count(), 0) + self.assertFalse(t.contains("seven")) + self.assertIsNone(t.search("seven")) + + def test_delete_the_two_keys(self): + t = TST() + t.insert("one", 1) + t.insert("two", 2) + + self.assertEqual(t.delete("one"), 1) + self.assertFalse(t.is_empty()) + self.assertEqual(t.count(), 1) + self.assertFalse(t.contains("one")) + self.assertTrue(t.contains("two")) + self.assertIsNone(t.search("one")) + self.assertEqual(t.search("two"), 2) + + self.assertEqual(t.delete("two"), 2) + self.assertTrue(t.is_empty()) + self.assertEqual(t.count(), 0) + self.assertFalse(t.contains("one")) + self.assertFalse(t.contains("two")) + self.assertIsNone(t.search("one")) + self.assertIsNone(t.search("two")) + + def test_delete_after_inserting_again(self): + t = TST() + + t.insert("boo", 0.5) + t.insert("neg", 1) + self.assertEqual(t.delete("neg"), 1) + + t.insert("neg", 1) + self.assertEqual(t.delete("neg"), 1) + + self.assertFalse(t.is_empty()) + self.assertEqual(t.count(), 1) + + def test_delete_all_random_keys(self): + t = TST() + + n = random.randint(3, 2000) + random_pairs = {} + + for _ in range(n): + key = TestTST.gen_rand_str(random.randint(1, 11)) + random_pairs[key] = key + t.insert(key, key) + + for k, v in random_pairs.items(): + self.assertEqual(t.delete(k), v) + self.assertIsNone(t.search(k)) + self.assertFalse(t.contains(k)) + + self.assertTrue(t.is_empty()) + self.assertEqual(t.count(), 0) + + # TODO: test_insert_delete_some_insert_delete_all + + def test_keys_with_prefix_not_str_prefix(self): + t = TST() + self.assertRaises(TypeError, t.keys_with_prefix, 3) + + def test_keys_with_prefix_empty_prefix(self): + t = TST() + + n = random.randint(1, 50) + keys = set() + + for _ in range(n): + key = TestTST.gen_rand_str(random.randint(1, 11)) + keys.add(key) + t.insert(key, key) + + kwp = t.keys_with_prefix("") + kwp_set = set(kwp) + self.assertEqual( + len(kwp), len(kwp_set) + ) # I should not need to check this here!!! + self.assertEqual(kwp_set, keys) + + def test_keys_with_prefix_none_found(self): + t = TST() + t.insert("one", 1) + t.insert("two", 2) + t.insert("three", 3) + self.assertEqual(t.keys_with_prefix("four"), []) + + def test_keys_with_prefix_prefix_size_equal_to_key_size(self): + t = TST() + t.insert("valete", "dama") + self.assertEqual(t.keys_with_prefix("valete"), ["valete"]) + + def test_keys_with_prefix_one_found(self): + t = TST() + t.insert("one", 1) + t.insert("two", 2) + t.insert("three", 3) + self.assertEqual(t.keys_with_prefix("on"), ["one"]) + + def test_keys_with_prefix_two_found(self): + t = TST() + t.insert("one", 1) + t.insert("two", 2) + t.delete("one") + t.insert("three", 3) + self.assertEqual(sorted(t.keys_with_prefix("t")), ["three", "two"]) + + def test_keys_with_prefix_all_found(self): + t = TST() + t.insert("occasion", 2) + t.insert("occasionally", 2) + t.insert("occam", 2) + self.assertEqual( + sorted(t.keys_with_prefix("occa")), ["occam", "occasion", "occasionally"] + ) + + def test_all_pairs_empty_tst(self): + t = TST() + self.assertEqual(t.all_pairs(), {}) + + def test_all_pairs_tst_size_1(self): + t = TST() + t.insert("the most sadistic", "necro") + self.assertEqual(t.all_pairs(), {"the most sadistic": "necro"}) + + def test_all_pairs_random_size_and_strings(self): + t = TST() + + n = random.randint(3, 1000) + random_pairs = {} + + for _ in range(n): + key = TestTST.gen_rand_str(random.randint(1, 17)) + random_pairs[key] = key + t.insert(key, key) + + self.assertEqual(t.all_pairs(), random_pairs) + + def test_longest_prefix_of_query_not_str(self): + t = TST() + self.assertRaises(TypeError, t.longest_prefix_of, -0.12) + + def test_longest_prefix_of_query_empty(self): + t = TST() + self.assertRaises(ValueError, t.longest_prefix_of, "") + + def test_longest_prefix_of_empty_tst(self): + t = TST() + self.assertEqual(t.longest_prefix_of(TestTST.gen_rand_str(10)), "") + + def test_longest_prefix_of_longest_prefix_size_zero(self): + t = TST() + t.insert("obnoxious", 7) + # obnoxious is NOT even a prefix of over + self.assertEqual(t.longest_prefix_of("over"), "") + + def test_longest_prefix_of_longest_prefix_size_one(self): + t = TST() + t.insert("o", 7) + t.insert("obnoxious", 23) + self.assertEqual(t.longest_prefix_of("overall"), "o") + + def test_longest_prefix_of_longest_prefix_size_two(self): + t = TST() + t.insert("p", 7) + t.insert("oa", 23) + self.assertEqual(t.longest_prefix_of("oak"), "oa") + + def test_longest_prefix_of_longest_prefix_size_of_query(self): + t = TST() + t.insert("allen", "first") + t.insert("allen halloween", "underrated!") + self.assertEqual(t.longest_prefix_of("allen halloween"), "allen halloween") + + def test_keys_that_match_pattern_not_str(self): + t = TST() + self.assertRaises(TypeError, t.keys_that_match, 1 / 2) + + def test_keys_that_match_pattern_empty_str(self): + t = TST() + self.assertRaises(ValueError, t.keys_that_match, "") + + def test_keys_that_match_tst_empty_pattern_one_dot(self): + t = TST() + self.assertEqual(t.keys_that_match("."), []) + + def test_keys_that_match_tst_empty_pattern_many_dots(self): + t = TST() + self.assertEqual(t.keys_that_match("......."), []) + + def test_keys_that_match_pattern_no_dots(self): + t = TST() + t.insert("one", 1) + t.insert("on", "fire") + self.assertEqual(t.keys_that_match("on"), ["on"]) + + def test_keys_that_match_example_docs(self): + t = TST() + t.insert("food", 3) + t.insert("good", 3) + t.insert("foodie", 3) + self.assertEqual(sorted(t.keys_that_match(".ood")), ["food", "good"]) + + def test_keys_that_match_pattern_using_dots(self): + t = TST() + t.insert("nop", 0) + t.insert("one", 1) + t.insert("on", "fire") + t.insert("fno", "ok") + self.assertEqual(sorted(t.keys_that_match(".n.")), ["fno", "one"]) + + def test_keys_that_match_pattern_using_dots_to_retrieve_all_keys_of_certain_length( + self, + ): + t = TST() + t.insert("zero", 0) + t.insert("one", 1) + t.insert("two", 2) + t.insert("three", 3) + t.insert("four", 4) + t.insert("five", 5) + t.insert("six", 6) + self.assertEqual(sorted(t.keys_that_match("...")), ["one", "six", "two"]) + self.assertEqual(sorted(t.keys_that_match("....")), ["five", "four", "zero"]) + self.assertEqual(sorted(t.keys_that_match(".....")), ["three"]) + + +class TestTSTNode(unittest.TestCase): + def test_create_key_not_string(self): + self.assertRaises(TypeError, _TSTNode, 13) + + def test_create_key_empty_string(self): + self.assertRaises(ValueError, _TSTNode, "") + + def test_create_acceptable_key(self): + self.assertIsInstance(_TSTNode("unit testing"), _TSTNode) + + def test_create_default(self): + u = _TSTNode("default values") + self.assertEqual(u.key, "default values") + self.assertIsNone(u.value) + self.assertIsNone(u.parent) + self.assertIsNone(u.mid) + self.assertIsNone(u.left) + self.assertIsNone(u.right) + + def test_create_custom(self): + p = _TSTNode("parent") + left = _TSTNode("left") + mid = _TSTNode("mid") + right = _TSTNode("right") + u = _TSTNode("u", 11, p, left, mid, right) + self.assertEqual(u.value, 11) + self.assertIs(u.parent, p) + self.assertIs(u.left, left) + self.assertIs(u.mid, mid) + self.assertIs(u.right, right) + + def test_is_left_child_no_parent(self): + u = _TSTNode("u") + self.assertRaises(AttributeError, u.is_left_child) + + def test_is_left_child_false(self): + p = _TSTNode("p") + u = _TSTNode("u", 3, p) + self.assertFalse(u.is_left_child()) + + def test_is_left_child_true(self): + p = _TSTNode("p") + u = _TSTNode("u", 3, p) + p.left = u + self.assertTrue(u.is_left_child()) + + def test_is_right_child_no_parent(self): + u = _TSTNode("u") + self.assertRaises(AttributeError, u.is_right_child) + + def test_is_right_child_false(self): + p = _TSTNode("p") + u = _TSTNode("u", 3, p) + self.assertFalse(u.is_right_child()) + + def test_is_right_child_true(self): + p = _TSTNode("p") + u = _TSTNode("u", 3, p) + p.right = u + self.assertTrue(u.is_right_child()) + + def test_is_mid_child_no_parent(self): + u = _TSTNode("u") + self.assertRaises(AttributeError, u.is_mid_child) + + def test_is_mid_child_false(self): + p = _TSTNode("p") + u = _TSTNode("u", 3, p) + self.assertFalse(u.is_mid_child()) + + def test_is_mid_child_true(self): + p = _TSTNode("p") + u = _TSTNode("u", 3, p) + p.mid = u + self.assertTrue(u.is_mid_child()) + + def test_has_children_0(self): + u = _TSTNode("u") + self.assertFalse(u.has_children()) + + def test_has_children_1(self): + u = _TSTNode("u", right=_TSTNode("right")) + self.assertTrue(u.has_children()) + + def test_has_children_2(self): + u = _TSTNode("u", mid=_TSTNode("mid"), left=_TSTNode("left")) + self.assertTrue(u.has_children()) + + def test_has_children_3(self): + u = _TSTNode( + "u", mid=_TSTNode("mid"), left=_TSTNode("left"), right=_TSTNode("right") + ) + self.assertTrue(u.has_children())