diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000000..edee3bc4febb --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,7 @@ +# https://github.com/microsoft/vscode-dev-containers/blob/main/containers/python-3/README.md +ARG VARIANT=3.13-bookworm +FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT} +COPY requirements.txt /tmp/pip-tmp/ +RUN python3 -m pip install --upgrade pip \ + && python3 -m pip install --no-cache-dir -r /tmp/pip-tmp/requirements.txt \ + && pipx install pre-commit ruff diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 000000000000..af7502768154 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,43 @@ +# Development Container + +This is **Devcontainer** configuration to provide a consistent development environment for all contributors. + +## Features + +- [x] Pre-configured **Python environment** +- [x] Automatic installation of **pre-commit hooks** +- [x] **Ruff** linter ready to check your code +- [x] **Oh My Zsh** with plugins: +- `zsh-autosuggestions` +- `zsh-syntax-highlighting` + +## Usage + +1. Install [**Docker**](https://www.docker.com/get-started/) and [**Visual Studio Code**](https://code.visualstudio.com/) +2. Install the **Remote - Containers** extension in VS Code + + - Do `CTRL+P`, paste this command and press `Enter` + + ```shell + ext install ms-vscode-remote.remote-containers + ``` + +3. Open this repository in VS Code +4. When prompted, click **"Reopen in Container"** +5. Wait for the environment to build and initialize + +After setup: + +- `pre-commit` hooks are installed +- `ruff` and other tools are available +- The shell uses Zsh by default + +## Tips + +To manually run checks on all files: + +```bash +pre-commit run --all-files +``` + +> For further information here's [Microsoft tutorial about devcontainers.](https://code.visualstudio.com/docs/devcontainers/tutorial) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000000..96691bc86fe4 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,57 @@ +{ + "name": "Python 3", + + // Use a prebuilt dev container image instead of building from a local + // Dockerfile. The repo migrated its dependencies to pyproject.toml, so the + // old Dockerfile's `COPY requirements.txt` step no longer had a file to copy + // and the image build failed. The upstream images already ship Python + a + // full toolchain, so pulling one is both faster and less to maintain. + // + // This repo tracks the latest-and-greatest CPython on the newest stable + // Debian. Images are published per CPython minor version on Debian 13 + // "Trixie" (3.11-trixie ... 3.15-trixie); bump this to the newest available + // when a new stable CPython ships. + // NOTE: these images do not publish free-threaded (`t`) variants, so 3.15t + // cannot be selected via the tag alone -- but the repo's `.python-version` + // pins 3.15t, and `uv run`/`uv sync` in the container honor it, so uv gives + // contributors free-threaded 3.15t regardless of the base tag. + "image": "mcr.microsoft.com/devcontainers/python:latest", + + // Install the tools post_install and CI expect (pre-commit + ruff), plus uv + // for the free-threaded workflow above, then run the existing setup script. + "postCreateCommand": "pipx install pre-commit ruff uv && zsh .devcontainer/post_install", + + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python", + // Formatting/linting is handled by Ruff (matches pre-commit and CI). + "editor.formatOnSave": true, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + } + }, + "terminal.integrated.defaultProfile.linux": "zsh" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "charliermarsh.ruff" + ] + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.devcontainer/post_install b/.devcontainer/post_install new file mode 100755 index 000000000000..589ee361f5cb --- /dev/null +++ b/.devcontainer/post_install @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +echo "Begin post-installation steps..." + +set -e + +echo "Installing pre-commit hooks..." +pre-commit install + +echo "Installing Oh My Zsh plugins..." + +# Install zsh-autosuggestions if not present +if [ ! -d "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-autosuggestions" ]; then + echo "Cloning zsh-autosuggestions..." + git clone https://github.com/zsh-users/zsh-autosuggestions \ + "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-autosuggestions" +fi + +# Install zsh-syntax-highlighting if not present +if [ ! -d "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting" ]; then + echo "Cloning zsh-syntax-highlighting..." + git clone https://github.com/zsh-users/zsh-syntax-highlighting.git \ + "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting" +fi + +echo "Configuring plugins in ~/.zshrc..." +sed -i '/^plugins=/c\plugins=(git zsh-autosuggestions zsh-syntax-highlighting)' ~/.zshrc + +echo "Post-installation steps completed successfully. Enjoy!" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000000..176a458f94e0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000000..3cc25d1bae1c --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,86 @@ +# This is a comment. +# Each line is a file pattern followed by one or more owners. + +# More details are here: https://help.github.com/articles/about-codeowners/ + +# The '*' pattern is global owners. + +# Order is important. The last matching pattern has the most precedence. + +/.* @cclauss + +# /backtracking/ + +# /bit_manipulation/ + +# /blockchain/ + +# /boolean_algebra/ + +# /cellular_automata/ + +# /ciphers/ + +# /compression/ + +# /computer_vision/ + +# /conversions/ + +# /data_structures/ + +# /digital_image_processing/ + +# /divide_and_conquer/ + +# /dynamic_programming/ + +# /file_transfer/ + +# /fuzzy_logic/ + +# /genetic_algorithm/ + +# /geodesy/ + +# /graphics/ + +# /graphs/ + +# /greedy_method/ + +# /hashes/ + +# /images/ + +# /linear_algebra/ + +# /machine_learning/ + +# /maths/ + +# /matrix/ + +# /networking_flow/ + +# /neural_network/ + +# /other/ + +# /project_euler/ + +# /quantum/ + +# /scheduling/ + +# /scripts/ + +# /searches/ + +# /sorts/ + +# /strings/ + +# /traversals/ + +/web_programming/ @cclauss diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000000..4ccdb52cad24 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,54 @@ +name: Bug report +description: Create a bug report to help us address errors in the repository +labels: [bug] +body: + - type: markdown + attributes: + value: > + Before requesting please search [existing issues](https://github.com/TheAlgorithms/Python/labels/bug). + Usage questions such as "How do I...?" belong on the + [Discord](https://discord.gg/c7MnfGFGa6) and will be closed. + + - type: input + attributes: + label: "Repository commit" + description: > + The commit hash for `TheAlgorithms/Python` repository. You can get this + by running the command `git rev-parse HEAD` locally. + placeholder: "a0b0f414ae134aa1772d33bb930e5a960f9979e8" + validations: + required: true + + - type: input + attributes: + label: "Python version (python --version)" + placeholder: "Python 3.10.7" + validations: + required: true + + - type: textarea + attributes: + label: "Dependencies version (pip freeze)" + description: > + This is the output of the command `pip freeze --all`. Note that the + actual output might be different as compared to the placeholder text. + placeholder: | + appnope==0.1.3 + asttokens==2.0.8 + backcall==0.2.0 + ... + validations: + required: true + + - type: textarea + attributes: + label: "Expected behavior" + description: "Describe the behavior you expect. May include images or videos." + validations: + required: true + + - type: textarea + attributes: + label: "Actual behavior" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000000..62019bb08938 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Discord community + url: https://discord.gg/c7MnfGFGa6 + about: Have any questions or need any help? Please contact us via Discord diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000000..20823bd58ab1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,19 @@ +name: Feature request +description: Suggest features, propose improvements, discuss new ideas. +labels: [enhancement] +body: + - type: markdown + attributes: + value: > + Before requesting please search [existing issues](https://github.com/TheAlgorithms/Python/labels/enhancement). + Do not create issues to implement new algorithms as these will be closed. + Usage questions such as "How do I...?" belong on the + [Discord](https://discord.gg/c7MnfGFGa6) and will be closed. + + - type: textarea + attributes: + label: "Feature description" + description: > + This could include new topics or improving any existing implementations. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/other.yml b/.github/ISSUE_TEMPLATE/other.yml new file mode 100644 index 000000000000..44d6ff541506 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/other.yml @@ -0,0 +1,19 @@ +name: Other +description: Use this for any other issues. PLEASE do not create blank issues +labels: ["awaiting triage"] +body: + - type: textarea + id: issuedescription + attributes: + label: What would you like to share? + description: Provide a clear and concise explanation of your issue. + validations: + required: true + + - type: textarea + id: extrainfo + attributes: + label: Additional information + description: Is there anything else we should know about this issue? + validations: + required: false diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..0b7f161181df --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +# Keep GitHub Actions up to date with Dependabot... +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + cooldown: + default-days: 7 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000000..9b7eb5bca370 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ +### Describe your change + +* [ ] Add an algorithm? +* [ ] Fix a bug or typo in an existing algorithm? +* [ ] Add or change doctests? -- Note: Please avoid changing both code and tests in a single pull request. +* [ ] Documentation change? + +### Checklist + +* [ ] I have read [CONTRIBUTING.md](https://github.com/TheAlgorithms/Python/blob/master/CONTRIBUTING.md). +* [ ] This pull request is all my own work -- I have not plagiarized. +* [ ] I know that pull requests will not be merged if they fail the automated tests. +* [ ] This PR only changes one algorithm file. To ease review, please open separate PRs for separate algorithms. +* [ ] All new Python files are placed inside an existing directory. +* [ ] All filenames are in all lowercase characters with no spaces or dashes. +* [ ] All functions and variable names follow Python naming conventions. +* [ ] All function parameters and return values are annotated with Python [type hints](https://docs.python.org/3/library/typing.html). +* [ ] All functions have [doctests](https://docs.python.org/3/library/doctest.html) that pass the automated testing. +* [ ] All new algorithms include at least one URL that points to Wikipedia or another similar explanation. +* [ ] If this pull request resolves one or more open issues, then the description above includes the issue number(s) with a [closing keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue): "Fixes #ISSUE-NUMBER". diff --git a/.github/skills/code-review/SKILL.md b/.github/skills/code-review/SKILL.md new file mode 100644 index 000000000000..f6b9689acfb2 --- /dev/null +++ b/.github/skills/code-review/SKILL.md @@ -0,0 +1,95 @@ +# Skill: Code review for TheAlgorithms/Python + +Review a pull request against the rules already written in +[`CONTRIBUTING.md`](../../../CONTRIBUTING.md). The goal is a review that any +reviewer (human or AI) can run the same way every time, and that produces a clear, +kind, actionable verdict. + +## How to run this skill + +Read the PR diff, then work through the four `CONTRIBUTING.md` sections in order +and emit the fixed output shape below. Cite the exact rule you are applying and +suggest the fix — never just "rejected". + +### 1. Before contributing / Is this an algorithm? + +- [ ] The change adds, fixes, or documents **one algorithm** — not multiple, and + not both code and doctest changes in the same PR. +- [ ] It is a genuine algorithm or data structure (see the *What is an Algorithm?* + section), not a script, snippet, how-to-use for an existing API, or exercise + dump. +- [ ] It is **not already in the repository** (search the existing directories). +- [ ] **No earlier open PR** already does the same thing — link it if one exists. +- [ ] Properly attributed — no plagiarism; prior sources credited. + +### 2. Coding Style + +- [ ] `from __future__ import annotations` is not needed because this repo only uses + the latest version of CPython. +- [ ] File and directory names are lowercase, use underscores, and land inside an + existing directory. +- [ ] Public functions/classes have **type hints**. +- [ ] Public functions have **doctests that actually pass**. +- [ ] Descriptive variable and function names (no single letters where a word helps). +- [ ] Code is formatted and lint-clean (`ruff`, `pre-commit`). + +> **Optional hint:** When a PR hand-writes a simple class that is mostly a +> bundle of fields (a manual `__init__` plus `__repr__`/`__eq__`), it is worth +> **suggesting** `from typing import NamedTuple` or +> `from dataclasses import dataclass` where they would simplify the code. These +> are underutilized tools that our contributors would benefit from using where +> they make sense. Offer it as an optional improvement, not a blocker — do not +> request changes solely because a class was written the longhand way. + +#### When a PR fails `ruff check` + +Don't just report the failure — try the mechanical fixes and recommend the one +that works, in this order: + +1. Run `ruff check --fix file_path.py`. If that makes the file pass, recommend + that solution — these are the fixes `ruff` considers **safe**. +2. If it still fails, run `ruff check --fix --unsafe-fixes file_path.py`. If that + makes the file pass **and** the resulting diff is genuinely safe (it preserves + behavior — review it, don't trust it blindly), recommend that solution and note + that it required `--unsafe-fixes`. +3. If neither passes, or the unsafe fix would change behavior, describe the + remaining rule violations and the manual change the author needs to make. + +Always quote the exact rule code(s) `ruff` reports (e.g., `ruff rule UP047`, +`ruff rule RUF100`) so the author can run those commands to read the rules being +flagged. Also, paste the concrete command you ran. + +### 3. Other Requirements for Submissions + +- [ ] At least one **Wikipedia (or equivalent) URL** documenting the algorithm. +- [ ] Docstring explains what the function does and its parameters/returns. +- [ ] No unnecessary third-party dependencies. + +### 4. Verdict — fixed output shape + +Emit exactly these headings so reviews are comparable and easy to automate: + +```text +### Is this an algorithm? — +### Duplicate / prior-art check — <#NNNN | none found> +### Coding style — +### Other requirements (doctests, type hints, descriptive names, Wikipedia URL) — +### Verdict — + one-line reason +``` + +## Tone + +Be specific and kind. Point at the exact `CONTRIBUTING.md` rule and offer the fix +rather than a bare rejection — first-time and Hacktoberfest contributors are more +likely to come back and improve the PR when the path forward is clear. + +## Map findings to labels + +Where a finding matches an existing label, name it so the review lines up with the +maintenance/cleanup tooling: + +- missing/failing doctests → `require tests` +- missing type hints → `require type hints` +- non-descriptive names → `require descriptive names` +- CI red → `tests are failing` +- otherwise ready for a maintainer → `awaiting reviews` diff --git a/.github/skills/new-pull-request/SKILL.md b/.github/skills/new-pull-request/SKILL.md new file mode 100644 index 000000000000..ca6a0dda5fb2 --- /dev/null +++ b/.github/skills/new-pull-request/SKILL.md @@ -0,0 +1,75 @@ +# Skill: New pull request for TheAlgorithms/Python + +Create a new pull request using the rules already written in +[`CONTRIBUTING.md`](../../../CONTRIBUTING.md). The goal is that creating a new +pull request (human or AI) can run the same way every time, and that produces a +clear, kind, tested, type-hinted, mergeable contribution. + +## How to run this skill + +Make sure that the local `master` branch is synced with `upstream/master` before +creating a new pull request. + +Create a new clearly named branch for the pull request. Pull request changes must +not be made or submitted on the `master` branch. + +Never hand-edit or revert the `uv.lock` file. If you add a legitimate +dependency, let the `uv-lock` pre-commit hook regenerate it — do not touch it by +hand. A hand-modified `uv.lock` makes the `algorithms-keeper` bot close the pull +request as invalid, and even a repo maintainer cannot undo that. + +Always check at least one Markdown checkbox in the pull request description (the "Describe your change" section), or the +`algorithms-keeper` bot will close the pull request as invalid — and it does +this *before* a human reads the PR, so a genuinely good change gets closed for a +formatting reason. This applies to **every** pull request, including CI, docs, +and tooling changes that are not algorithms: tick the boxes that genuinely apply +so the body is never submitted with all boxes empty. Any repo maintainer can +undo this if you @mention them on the closed pull request, but re-opening is +often unreliable, so it is far better to get it right the first time. + +### 1. Before contributing / Is this an algorithm? + +- [ ] The change adds, fixes, or documents **one algorithm** — not multiple, and + not both code and doctest changes in the same PR. +- [ ] It is a genuine algorithm or data structure (see the *What is an Algorithm?* + section), not a script, snippet, how-to-use for an existing API, or exercise + dump. +- [ ] It is **not already in the repository** (search the existing directories). +- [ ] **No earlier open PR** already does the same thing — link it if one exists. +- [ ] Properly attributed — no plagiarism; prior sources credited. + +### 2. Coding Style + +- [ ] `from __future__ import annotations` is not needed because this repo only uses + the latest version of CPython. +- [ ] File and directory names are lowercase, use underscores, and land inside an + existing directory. +- [ ] Public functions/classes have **type hints**. +- [ ] Public functions have **doctests that actually pass**. +- [ ] Descriptive variable and function names (no single letters where a word helps). +- [ ] For a simple class that is mostly a bundle of fields, **consider** + `from typing import NamedTuple` or `from dataclasses import dataclass` + instead of a hand-written `__init__`/`__repr__`/`__eq__`. These are + underutilized tools that make simple classes shorter and clearer — use + them where they genuinely simplify the code, not everywhere. +- [ ] Code is formatted and lint-clean (`ruff`, `pre-commit`). +- [ ] `DIRECTORY.md` and `README.md` are **not hand-edited** — the + `algorithms-keeper` bot regenerates them automatically after merge. + +### 3. Other Requirements for Submissions + +- [ ] At least one **Wikipedia (or equivalent) URL** documenting the algorithm. +- [ ] Docstring explains what the function does and its parameters/returns. +- [ ] No unnecessary third-party dependencies. + +## Before you click "Create pull request" + +This is the final gate. Do not open the pull request until every item here is true: + +- [ ] At least one Markdown checkbox in the PR description is checked. **Verify + this by re-reading the rendered body** — if every box is still `- [ ]`, the + `algorithms-keeper` bot will auto-close the PR before any human sees it. + Check the boxes that genuinely apply to this change; never submit an + all-empty checklist, even for a CI, docs, or tooling PR. +- [ ] The branch is not `master`, and `master` is synced with `upstream/master`. +- [ ] `uv.lock` was not hand-edited. diff --git a/.github/skills/review-open-issue/SKILL.md b/.github/skills/review-open-issue/SKILL.md new file mode 100644 index 000000000000..7603f38fdbff --- /dev/null +++ b/.github/skills/review-open-issue/SKILL.md @@ -0,0 +1,110 @@ +# Skill: Review an open issue for TheAlgorithms/Python + +Triage an open issue — most often one carrying the `bug` label — **together with +every pull request attached to it**, and produce a verdict a maintainer can act on +without re-doing the work. The goal is a review that any triager (human or AI) can +run the same way every time, and that ends in an explicit list of `Closes #NNNNN` +lines the maintainer can paste into a merge commit. + +This repo exists to **teach visitors to fix bugs by doing**. So the default is to +merge adequate existing contributor work, not to open a fresh PR that races them. +Only open your own PR when no attached PR adequately solves the issue. + +## How to run this skill + +### 1. Reproduce before you trust the report + +- Check out current `master` and actually **run the failing case** from the issue. +- If it reproduces, say so and paste the minimal reproducer (inputs → observed + output, e.g. `nan` + a `RuntimeWarning`). +- If you **cannot** confirm it locally — an optional dependency isn't installed, + the failure needs an external service, the report is too vague — **say that + explicitly rather than guessing.** "Not reproducible in a vanilla checkout" + is a real, useful verdict. + +### 2. Classify the issue + +Sort each issue into one bucket and act accordingly: + +- **Confirmed bug with an adequate open PR** → recommend the specific PR to merge. +- **Confirmed bug, no adequate PR** → open a new PR yourself (see step 5). +- **Not a bug / working-as-intended** → recommend closing, and explain why (e.g. + binary search returning *an* index of a duplicate is correct, not a defect). +- **Feature request mislabeled as a bug** → recommend dropping the `bug` label; do + **not** close it as a bug. +- **Too vague / needs reporter info** → comment **on the issue itself** asking for + the one concrete thing missing (a file path, a traceback, a reproducer), and note + it should be closed if no answer arrives. +- **Environment / optional-dependency, not an algorithm defect** → note it needs + someone with that environment; it is not fixable in a vanilla checkout. + +### 3. Examine every attached PR — pick one, name the duplicates + +For a bug with several open PRs: + +- **Verify each fix's doctest values numerically** against a trusted reference + (`scipy`, `numpy.linalg`, `geopy`, or a brute-force check over random inputs). + Don't take a doctest's word for it — confirm the number. +- Prefer the **first-in PR with the smallest correct diff** that follows repo + convention (doctests over new test files, `raise` over `assert`, no unrelated + reindentation or churn of clean doctests into floating-point noise). +- **Reject out-of-scope "fixes"** — e.g. swapping a real divide-by-zero for a + `1e-15` magic constant makes results implementation-defined. Fix the issue, not + more. +- Identify the byte-for-byte **duplicates** so the maintainer can close them with + thanks in the same action. + +### 4. Emit the verdict — fixed output shape + +Post one comment that a maintainer can act on directly. For each issue give a +one-line rationale and the exact autoclose line. Two formatting rules matter a lot +to human maintainers scanning the thread: + +- **Start every line that references an issue or PR with a Markdown list marker + (`-` or `* `).** GitHub-flavored Markdown only autolinks `#NNNNN` inside a list, + and those autolinks are colored — **purple = merged, red = closed, green = open** — + so the maintainer can see merge/close progress at a glance. A bare `#14813` at the + start of a line does not autolink. +- **Merging a PR that closes an issue also closes the issue's other open PRs**, so + fold the duplicates into one autoclose statement rather than listing the winner + alone: + + ```text + * #14813 — 3x3 inverse returns the transpose → merge #14821 + (Closes #14813, Closes #14840, Closes #15045) + ``` + +Recommended shape: + +```text +## Confirmed bugs with an adequate open PR — merge +* # — → merge # (Closes #, Closes #, …) + +## Not a bug / not merge-ready +* # — + +## Summary — lines to add +* Closes # → # +``` + +### 5. When you must open your own PR + +If no attached PR is adequate, open one — but let the contributors keep the credit +where their work was close. Use autoclose keywords in the **commit message body** +so the merge closes the issue *and* the superseded PRs: + +```text +Fixes #12233 +Fixes #12262 +Fixes #13635 +``` + +Follow the [`new-pull-request`](../new-pull-request/SKILL.md) skill for the +mechanics (synced `master`, named branch, checked description box, untouched +`uv.lock`). + +## Tone + +Be specific and kind. Name the exact PR and the exact reason, credit the +contributor by handle, and offer runners-up detailed feedback so they learn — the +point of the repo is that people come back and fix the next one. diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000000..0939e1f223ff --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,63 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 30 + +# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 7 + +# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) +onlyLabels: [] + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: + - "Status: on hold" + +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: false + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: false + +# Set to true to ignore issues with an assignee (defaults to false) +exemptAssignees: false + +# Label to use when marking as stale +staleLabel: stale + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 5 + +# Comment to post when removing the stale label. +# unmarkComment: > +# Your comment here. + +# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': +pulls: + # Comment to post when marking as stale. Set to `false` to disable + markComment: > + This pull request has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + + # Comment to post when closing a stale Pull Request. + closeComment: > + Please reopen this pull request once you commit the changes requested + or make improvements on the code. If this is not the case and you need + some help, feel free to seek help from our [Gitter](https://gitter.im/TheAlgorithms/community) + or ping one of the reviewers. Thank you for your contributions! + +issues: + # Comment to post when marking as stale. Set to `false` to disable + markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + + # Comment to post when closing a stale Issue. + closeComment: > + Please reopen this issue once you add more information and updates here. + If this is not the case and you need some help, feel free to seek help + from our [Gitter](https://gitter.im/TheAlgorithms/community) or ping one of the + reviewers. Thank you for your contributions! diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000000..3dfe13d80c65 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,49 @@ +name: "build" + +on: + pull_request: + schedule: + - cron: "0 0 * * *" # Run everyday + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + cache-dependency-glob: uv.lock + - uses: actions/setup-python@v7 + with: + python-version-file: .python-version + allow-prereleases: true + - run: uv sync --group=test + - name: Run tests + # opencv-python is gated out on 3.15t (no cp315t wheel yet), so skip the + # files that import cv2. Pure-Python algorithms in computer_vision/ and + # data_compression/ still run; digital_image_processing/ is almost entirely + # cv2-based so it is skipped as a tree. Re-enable when a cp315t wheel ships. + # --ignore-gil-enabled: some compiled deps (sklearn, xgboost, ...) don't + # yet ship the Py_mod_gil slot, so importing them re-enables the GIL under + # 3.15t. That's an upstream-wheel gap, not our code; the flag lets the suite + # run anyway and pytest-run-parallel still reports which tests are not + # thread-safe. Drop the flag once the scientific stack ships free-threaded wheels. + run: uv run --with=pytest-run-parallel pytest + --iterations=8 --parallel-threads=auto --ignore-gil-enabled + --ignore=computer_vision/flip_augmentation.py + --ignore=computer_vision/harris_corner.py + --ignore=computer_vision/mosaic_augmentation.py + --ignore=data_compression/peak_signal_to_noise_ratio.py + --ignore=digital_image_processing/ + --ignore=docs/conf.py + --ignore=project_euler/ + --ignore=scripts/validate_solutions.py + --ignore=web_programming/current_stock_price.py + --ignore=web_programming/fetch_anime_and_play.py + --cov-report=term-missing:skip-covered + --cov=. . + - if: ${{ success() }} + run: scripts/build_directory_md.py 2>&1 | tee DIRECTORY.md diff --git a/.github/workflows/devcontainer_ci.yml b/.github/workflows/devcontainer_ci.yml new file mode 100644 index 000000000000..f521dad9e72b --- /dev/null +++ b/.github/workflows/devcontainer_ci.yml @@ -0,0 +1,23 @@ +name: Test DevContainer Build + +on: + push: + paths: + - ".devcontainer/**" + - ".github/workflows/devcontainer_ci.yml" + pull_request: + paths: + - ".devcontainer/**" + - ".github/workflows/devcontainer_ci.yml" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: devcontainers/ci@v0.3 + with: + push: never + runCmd: "true" diff --git a/.github/workflows/directory_writer.yml b/.github/workflows/directory_writer.yml new file mode 100644 index 000000000000..b58e0727801c --- /dev/null +++ b/.github/workflows/directory_writer.yml @@ -0,0 +1,27 @@ +# The objective of this GitHub Action is to update the DIRECTORY.md file (if needed) +# when doing a git push +name: directory_writer +on: [push] +jobs: + directory_writer: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-python@v7 + with: + python-version-file: .python-version + allow-prereleases: true + - name: Write DIRECTORY.md + run: | + scripts/build_directory_md.py 2>&1 | tee DIRECTORY.md + git config --global user.name "$GITHUB_ACTOR" + git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com" + git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY + - name: Update DIRECTORY.md + run: | + git add DIRECTORY.md + git commit -am "updating DIRECTORY.md" || true + git push --force origin HEAD:$GITHUB_REF || true diff --git a/.github/workflows/hacktoberfest_prep.yml b/.github/workflows/hacktoberfest_prep.yml new file mode 100644 index 000000000000..fc78d9b059b8 --- /dev/null +++ b/.github/workflows/hacktoberfest_prep.yml @@ -0,0 +1,109 @@ +# Daily refresh of the Hacktoberfest 2026 open-PR cleanup tracker. +# Ticks off any tracked pull request that has since been merged/closed, and +# rewrites the "Automated statistics" section (open issue/PR counts + the top +# three `awaiting reviews` directories). The job fails on purpose once +# Hacktoberfest 2026 has begun (>= 2026-10-01), which is the signal to retire it. +name: hacktoberfest_prep + +on: + push: + paths: + - ".github/workflows/hacktoberfest_prep.yml" + - "scripts/hacktoberfest_prep_update.py" + pull_request: + paths: + - ".github/workflows/hacktoberfest_prep.yml" + - "scripts/hacktoberfest_prep_update.py" + # schedule: + # - cron: "50 11 * * *" # 11:50 UTC every day + workflow_dispatch: # allow a manual run while testing + +permissions: + contents: write + pull-requests: write + +jobs: + hacktoberfest-prep: + # No point running on forks — this pushes to the repo's own docs file. + if: github.repository == 'TheAlgorithms/Python' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions/setup-python@v7 + with: + python-version-file: .python-version + allow-prereleases: true + - name: Install dependencies + run: python -m pip install --upgrade "httpx2>=2.0.1" + - name: Update the tracker + id: update + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + # Don't let the intentional post-Oct-1 failure stop the commit step; + # capture the exit code and re-raise it after pushing any changes. + run: | + set +e + python scripts/hacktoberfest_prep_update.py + echo "exit_code=$?" >> "$GITHUB_OUTPUT" + # Dry run on push / pull_request: show the diff the script produced but + # do NOT commit or push. This lets a PR prove the tracker still gathers + # its data and rewrites docs/hacktober_2026_prep.md correctly without + # leaving a permanent commit. Only the schedule/manual runs persist. + - name: Show changes (dry run) + if: github.event_name == 'push' || github.event_name == 'pull_request' + run: | + echo "Dry run (${{ github.event_name }}): showing git diff, not committing." + git --no-pager diff -- docs/hacktober_2026_prep.md + if git diff --quiet -- docs/hacktober_2026_prep.md; then + echo "No changes to docs/hacktober_2026_prep.md." + fi + # `master` is a protected branch: direct pushes are rejected with + # `GH006: Protected branch update failed ... Changes must be made through + # a pull request`. So instead of committing straight to master, persist + # the refreshed tracker on a single rolling branch and open — or, since + # re-pushing the branch updates the existing PR in place, leave open — one + # pull request. Uses the bundled `gh` CLI rather than a third-party + # action (see zizmor's "superfluous actions" audit). + - name: Open or update the tracker pull request + if: github.event_name != 'push' && github.event_name != 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: chore/hacktoberfest-2026-prep-refresh + run: | + set -euo pipefail + if git diff --quiet -- docs/hacktober_2026_prep.md; then + echo "No changes to docs/hacktober_2026_prep.md; nothing to persist." + exit 0 + fi + git config --global user.name "$GITHUB_ACTOR" + git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com" + # `actions/checkout` runs with `persist-credentials: false`, so git has + # no token to authenticate the push below (it fails with + # `fatal: could not read Username for 'https://github.com'`). Wire the + # bundled `gh` in as git's credential helper so `git push` reuses + # GH_TOKEN — no third-party action and no token baked into the remote URL. + gh auth setup-git + git switch -c "$BRANCH" + git add docs/hacktober_2026_prep.md + git commit -m "chore: refresh Hacktoberfest 2026 prep tracker" + # Force-push so the rolling branch always carries just the latest + # snapshot on top of master; this also updates any open PR in place. + git push --force origin "$BRANCH" + if [ -z "$(gh pr list --head "$BRANCH" --state open --json number --jq '.[].number')" ]; then + gh pr create \ + --base master \ + --head "$BRANCH" \ + --title "chore: refresh Hacktoberfest 2026 prep tracker" \ + --body "Automated daily refresh of the Hacktoberfest 2026 open-PR cleanup tracker (\`docs/hacktober_2026_prep.md\`): ticks off any tracked pull request that has since been merged/closed and rewrites the **Automated statistics** section. + + This PR is updated in place by the \`hacktoberfest_prep\` workflow, so it always reflects the latest scheduled run. Merge it whenever you want to capture the current snapshot." + else + echo "Open tracker PR already exists; force-push updated it in place." + fi + - name: Propagate the script's exit code + env: + UPDATE_EXIT_CODE: ${{ steps.update.outputs.exit_code }} + run: exit ${UPDATE_EXIT_CODE} diff --git a/.github/workflows/project_euler.yml b/.github/workflows/project_euler.yml new file mode 100644 index 000000000000..bbbeaa048943 --- /dev/null +++ b/.github/workflows/project_euler.yml @@ -0,0 +1,41 @@ +on: + pull_request: + # Run only if a file is changed within the project_euler directory and related files + paths: + - "project_euler/**" + - ".github/workflows/project_euler.yml" + - "scripts/validate_solutions.py" + schedule: + - cron: "0 0 * * *" # Run everyday + +name: "Project Euler" + +jobs: + project-euler: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@v7 + - uses: actions/setup-python@v7 + with: + python-version-file: .python-version + allow-prereleases: true + - run: uv sync --group=euler-validate --group=test + - run: uv run pytest --doctest-modules --cov-report=term-missing:skip-covered --cov=project_euler/ project_euler/ + validate-solutions: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@v7 + - uses: actions/setup-python@v7 + with: + python-version-file: .python-version + allow-prereleases: true + - run: uv sync --group=euler-validate --group=test + - run: uv run pytest scripts/validate_solutions.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 000000000000..338bae8d6470 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,18 @@ +# https://beta.ruff.rs +name: ruff +on: + push: + branches: + - master + pull_request: + branches: + - master +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@v7 + - run: uvx ruff check --output-format=github . diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml new file mode 100644 index 000000000000..1672bf69407f --- /dev/null +++ b/.github/workflows/sphinx.yml @@ -0,0 +1,52 @@ +name: sphinx + +on: + # Triggers the workflow on push or pull request events but only for the "master" branch + push: + branches: ["master"] + pull_request: + branches: ["master"] + # Or manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build_docs: + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@v7 + - uses: actions/setup-python@v7 + with: + python-version-file: .python-version + allow-prereleases: true + - run: uv sync --group=docs + - uses: actions/configure-pages@v6 + - run: uv run sphinx-build -c docs . docs/_build/html + - uses: actions/upload-pages-artifact@v5 + with: + path: docs/_build/html + + deploy_docs: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + if: github.event_name != 'pull_request' + needs: build_docs + runs-on: ubuntu-latest + steps: + - uses: actions/deploy-pages@v5 + id: deployment diff --git a/.github/workflows/ty.yml b/.github/workflows/ty.yml new file mode 100644 index 000000000000..15aface62dea --- /dev/null +++ b/.github/workflows/ty.yml @@ -0,0 +1,53 @@ +# https://docs.astral.sh/ty/ +name: ty +on: + push: + branches: + - master + pull_request: + branches: + - master +jobs: + ty: + runs-on: ubuntu-latest + # Informational only: ty is not yet a required gate. This job surfaces + # type-check findings in the logs so they can be triaged into gradual + # [tool.ty] rules without blocking merges. Flip `continue-on-error` to + # false once the baseline is clean. + continue-on-error: true + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + cache-dependency-glob: uv.lock + # Mirror the `build` job: `uv sync` installs the dependencies without + # building this flat-layout repo as a wheel. pyproject.toml has no + # [build-system], so uv treats the project as virtual and installs only + # its dependencies -- no "Multiple top-level packages discovered in a + # flat-layout" error and no need for a src-layout. (`uv pip install .` + # would force that wheel build and fail.) Pin a regular GIL 3.15 so typed + # third-party dependencies resolve; the free-threaded 3.15t interpreter the + # test matrix uses lacks wheels for several of them. ty itself is pulled in + # ephemerally with `--with` so it needn't be added to the lockfile. + - run: uv sync --python 3.15 + # Pin the *run* to the same regular-GIL 3.15 too. Without `--python 3.15` + # here, `uv run` re-resolved the project's default interpreter -- the + # free-threaded 3.15t used by the test matrix -- so ty type-checked against + # an env where many third-party stubs don't resolve, drowning real findings + # in `unresolved-import` / `unresolved-attribute` noise. Pinning 3.15 lets + # ty see the synced dependencies, so the `[tool.ty.rules]` severities in + # pyproject.toml apply to a meaningful baseline. + # + # `--exit-zero` is ty's native advisory switch (the analog of ruff's + # `ruff check --exit-zero`): the step always exits 0, yet with + # `--output-format=github` ty still emits ::error annotations that render + # inline on the diff, so findings stay visible without a red X. Promotion + # path: `--exit-zero` (informational, always green) -> `--exit-zero-on-warning` + # (fail only on error-severity, tolerate warnings) -> drop the flag entirely + # to make ty a required gate once the [tool.ty] baseline is clean. + # `continue-on-error: true` above stays as a belt-and-suspenders guard so a + # ty *crash* (not a diagnostic) can never block a merge either. + - run: uv run --python 3.15 --with ty ty check --output-format=github --exit-zero diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 000000000000..75a55f63776d --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,7 @@ +rules: + excessive-permissions: + disable: true + unpinned-uses: + config: + policies: + "*": ref-pin diff --git a/.gitignore b/.gitignore index 96422cb2ec94..192f37798f5d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ __pycache__/ # Distribution / packaging .Python -env/ build/ develop-eggs/ dist/ @@ -20,12 +19,14 @@ lib64/ parts/ sdist/ var/ +wheels/ *.egg-info/ .installed.cfg *.egg +MANIFEST # PyInstaller -# Usually these files are written by a python script from a template +# Usually these files are written by a Python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec @@ -42,8 +43,9 @@ htmlcov/ .cache nosetests.xml coverage.xml -*,cover +*.cover .hypothesis/ +.pytest_cache/ # Translations *.mo @@ -52,6 +54,7 @@ coverage.xml # Django stuff: *.log local_settings.py +db.sqlite3 # Flask stuff: instance/ @@ -66,26 +69,41 @@ docs/_build/ # PyBuilder target/ -# IPython Notebook +# Jupyter Notebook .ipynb_checkpoints # pyenv -.python-version # celery beat schedule file celerybeat-schedule -# dotenv -.env +# SageMath parsed files +*.sage.py -# virtualenv +# Environments +.env +.venv +env/ venv/ ENV/ +env.bak/ +venv.bak/ # Spyder project settings .spyderproject +.spyproject # Rope project settings .ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +.DS_Store .idea -.DS_Store \ No newline at end of file +.try +.vscode/ +.vs/ diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 000000000000..5975b8b8e983 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,2 @@ +tasks: + - init: pip3 install -r ./requirements.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000000..6b7ebcc77c15 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,71 @@ +ci: + autoupdate_schedule: monthly + # uv-lock resolves dependencies from PyPI, but pre-commit.ci runs hooks with no + # network access, so it fails there. Skip it on pre-commit.ci; it still runs locally. + skip: [uv-lock] + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-executables-have-shebangs + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + types: [python] + - id: trailing-whitespace + - id: requirements-txt-fixer + + - repo: https://github.com/MarcoGorelli/auto-walrus + rev: 0.4.1 + hooks: + - id: auto-walrus + + - repo: https://github.com/codespell-project/codespell + rev: v2.4.3 + hooks: + - id: codespell + additional_dependencies: + - tomli + + - repo: https://github.com/tox-dev/pyproject-fmt + rev: v2.30.1 + hooks: + - id: pyproject-fmt + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.16.10 + hooks: + - id: ruff-check + - id: ruff-format + + - repo: https://github.com/rvben/rumdl-pre-commit + rev: v0.2.78 + hooks: + - id: rumdl-fmt + - id: rumdl + args: [--fix] + + - repo: local + hooks: + - id: validate-filenames + name: Validate filenames + entry: ./scripts/validate_filenames.py + language: script + pass_filenames: false + + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.12.23 + hooks: + - id: uv-lock + + - repo: https://github.com/abravalheri/validate-pyproject + rev: "0.26" + hooks: + - id: validate-pyproject + + - repo: https://github.com/15r10nk/matchify + rev: v0.3.0 + hooks: + - id: matchify + exclude: ^data_structures/linked_list/doubly_linked_list\.py$ diff --git a/.python-version b/.python-version new file mode 100644 index 000000000000..d5629d4841ea --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.15t diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000000..1846b12d0efd --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,62 @@ +# AGENTS.md + +Guidance for AI coding agents (and their humans) contributing to +**TheAlgorithms/Python**. This complements — and never overrides — +[`CONTRIBUTING.md`](CONTRIBUTING.md). Read that first. + +This repository is educational: implementations should be clear and correct +rather than maximally optimized. Every change goes through CI and the +`algorithms-keeper` bot, both of which reject non-conforming PRs automatically. + +## Before opening a pull request + +- **Check at least one box in the PR description.** The `algorithms-keeper` + bot **closes any PR whose "Describe your change" section has no checked + box** (`* [x]`). Fill in the template that ships in + `.github/pull_request_template.md` and tick every item that applies before + you submit — this is the single most common reason automated PRs get closed. +- **One algorithm file per PR.** Split unrelated changes into separate PRs to + keep review focused. +- **Don't change code and its doctests in the same PR.** If you're only + updating tests, say so and touch nothing else. + +## Code conventions (enforced by CI) + +- **Formatting & linting:** `ruff` (`uvx ruff check .` and `uvx ruff format .`). + Run `uvx pre-commit run --all-files` locally to catch everything CI will. +- **Type hints:** annotate every function parameter and return value with + [type hints](https://docs.python.org/3/library/typing.html). +- **Doctests:** every function needs at least one + [doctest](https://docs.python.org/3/library/doctest.html) that passes under + `python -m doctest -v your_file.py` (and `pytest`). +- **Naming:** filenames are all-lowercase with underscores (no spaces or + dashes); functions and variables follow standard Python naming. +- **Placement:** new files go inside an existing directory. +- **References:** new algorithms include a URL to Wikipedia or a comparable + explanation. + +## Running the suite locally + +This project is managed with [`uv`](https://docs.astral.sh/uv/) — there is no +`requirements.txt`. Dependencies live in `pyproject.toml`/`uv.lock`, and `uvx` +runs a tool in a throwaway environment without polluting yours: + +```bash +uvx pre-commit run --all-files # ruff, formatting, hooks +uvx pytest your_module/your_file.py --doctest-modules +``` + +(`uv run pytest ...` works too if you'd rather use the project's locked +environment.) + +Some directories are intentionally skipped in CI (`--ignore` entries in +`.github/workflows/build.yml`), usually because a heavy dependency lacks a +wheel for the CPython version the repo currently targets. Check that list +before assuming a file is untested. + +## Good agent behavior + +- Keep diffs minimal and scoped to the stated change. +- Preserve existing style and structure; prefer clarity over cleverness. +- Never fabricate doctest output — run it and paste the real result. +- If CI is red, read the log and fix the cause rather than re-running blindly. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000000..2c66950b8a7c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,224 @@ +# Contributing Guidelines + +## Before Contributing + +Welcome to [TheAlgorithms/Python](https://github.com/TheAlgorithms/Python)! Before submitting your pull requests, please ensure that you __read the entire guidelines__. If you have any doubts about the contributing guide, please feel free to [state them clearly in an issue](https://github.com/TheAlgorithms/Python/issues/new) or ask the community on [Gitter](https://gitter.im/TheAlgorithms/community). + +## Contributing + +### Contributor + +We are delighted that you are considering implementing algorithms and data structures for others! This repository is referenced and used by learners from all over the globe. By being one of our contributors, you agree and confirm that: + +- You did your work — no plagiarism allowed. + - Any plagiarized work will not be merged. +- Your work will be distributed under the [MIT License](LICENSE.md) once your pull request is merged. +- Your submitted work fulfills or mostly fulfills our styles and standards. + +__New implementations__ are welcome! For example, new solutions for a problem, different representations for a graph data structure, or algorithm designs with different complexities. However, __identical implementations__ of an existing one are not allowed. Please check whether the solution is already implemented before submitting your pull request. + +__Improving comments__ and __writing proper tests__ are also highly welcome. + +### Contribution + +We appreciate any contribution — from fixing a grammar mistake in a comment to implementing complex algorithms. Please read this section if you are contributing your work. + +Your contribution will be tested by our [automated testing on GitHub Actions](https://github.com/TheAlgorithms/Python/actions) to save time and mental energy. After you have submitted your pull request, you should see the GitHub Actions tests start to run at the bottom of your submission page. + +If those tests fail, then click on the ___details___ button to read through the GitHub Actions output to understand the failure. If you do not understand, please leave a comment on your submission page, and a community member will try to help. + +#### Issues + +If you are interested in resolving an [open issue](https://github.com/TheAlgorithms/Python/issues), simply make a pull request with your proposed fix. __We do not assign issues in this repo__, so please do not ask for permission to work on an issue. + +__Do not__ create an issue to contribute an algorithm. Please submit a pull request instead. + +Please help us keep our issue list small by adding `Fixes #{$ISSUE_NUMBER}` to the description of pull requests that resolve open issues. +For example, if your pull request fixes issue #10, then please add the following to its description: + +```text +Fixes #10 +``` + +GitHub will use this tag to [auto-close the issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if and when the PR is merged. + +#### What is an Algorithm? + +An algorithm is one or more functions (or classes) that: + +- take one or more inputs, +- perform some internal calculations or data manipulations, +- return one or more outputs, +- have minimal side effects (e.g., `print()`, `plot()`, `read()`, `write()`). + +Algorithms should be packaged in a way that would make it easy for readers to integrate them into larger programs. + +Algorithms should: + +- have intuitive class and function names that make their purpose clear to readers, +- use Python naming conventions and intuitive variable names to ease comprehension, +- be flexible to take different input values, +- have Python type hints for their input parameters and return values, +- raise Python exceptions (`ValueError`, etc.) on erroneous input values, +- have docstrings with clear explanations and/or URLs to source materials, +- contain doctests that test both valid and erroneous input values, +- return all calculation results instead of printing or plotting them. + +Algorithms in this repo should not be simple how-to examples for existing Python packages. Instead, they should perform internal calculations or manipulations to convert input values into different output values. +These calculations or manipulations can use data types, classes, or functions of existing Python packages, but each algorithm in this repo should add unique value. + +#### Pre-Commit Plugin + +- have intuitive class and function names that make their purpose clear to readers +- use Python naming conventions and intuitive variable names to ease comprehension +- be flexible to take different input values +- have Python type hints for their input parameters and return values +- raise Python exceptions (`ValueError`, etc.) on erroneous input values +- have docstrings with clear explanations and/or URLs to source materials +- contain doctests that test both valid and erroneous input values +- return all calculation results instead of printing or plotting them + +Algorithms in this repo should not be how-to examples for existing Python packages. Instead, they should perform internal calculations or manipulations to convert input values into different output values. +Those calculations or manipulations can use data types, classes, or functions of existing Python packages, but each algorithm in this repo should add unique value. + +#### Pre-commit + +Use [pre-commit](https://pre-commit.com/#installation) to automatically format your code to match our coding style: + +```bash +python3 -m pip install pre-commit # only required the first time +pre-commit install +``` + +That's it! Pre-commit will run every time you commit any changes. If any errors are found during the run, fix them and commit those changes. You can even run the plugin manually on all files: + +```bash +pre-commit run --all-files --show-diff-on-failure +``` + +#### Coding Style + +We want your work to be readable by others; therefore, we encourage you to note the following: + +- Please write in free-threaded Python 3.15t+. For instance: `print()` is a function in Python 3, so `print "Hello"` will *not* work, but `print("Hello")` will. + +- Please focus hard on the naming of functions, classes, and variables. Help your reader by using __descriptive names__ that can help you to remove redundant comments. + - Single-letter variable names are *old school*, so please avoid them unless their life only spans a few lines. + - Expand acronyms because `gcd()` is hard to understand but `greatest_common_divisor()` is not. + - Please follow the [Python Naming Conventions](https://pep8.org/#prescriptive-naming-conventions) so variable_names and function_names should be lower_case, CONSTANTS in UPPERCASE, ClassNames should be CamelCase, etc. + +- We encourage the use of Python [f-strings](https://realpython.com/python-f-strings/#f-strings-a-new-and-improved-way-to-format-strings-in-python) where they make the code easier to read. + +- Please consider running [__ruff format__](https://docs.astral.sh/ruff/formatter/) on your Python file(s) before submitting your pull request. This is not yet a requirement, but it does make your code more readable and automatically aligns it with much of [PEP 8](https://peps.python.org/pep-0008/). To use it, + + ```bash + python3 -m pip install ruff # only required the first time + ruff format + ``` + +- All submissions will need to pass the test `ruff check` before they will be accepted, so if possible, try this test locally on your Python file(s) before submitting your pull request. + + ```bash + python3 -m pip install ruff # only required the first time + ruff check + ``` + +- Original code submissions require docstrings or comments to describe your work. + +- More on docstrings and comments: + + If you used a Wikipedia article or other source material to create your algorithm, please add the URL in a docstring or comment to help your reader. + + The following are considered to be bad and may be requested to be improved: + + ```python + x += 2 # increased by 2 + ``` + + This is too trivial. Comments should not merely repeat what the code already says. Comments should explain ___why___ we are doing things. Comments on the same line as code should never cause the line to wrap (> 88 characters per line). +Comments which are not on the same line as code should appear ___before___ the code they describe. "First tell the reader ___why___ with comments and then show them ___how___ with code." + + We require you to put docstrings inside your functions, but please pay attention to the indentation of docstrings. The following is a good example: + + ```python + def sum_ab(a, b): + """ + Return the sum of two integers a and b. + """ + return a + b + ``` + +- Write tests (especially [__doctests__](https://docs.python.org/3/library/doctest.html)) to illustrate and verify your work. We highly encourage the use of *doctests on all functions*. + + ```python + def sum_ab(a, b): + """ + Return the sum of two integers a and b + >>> sum_ab(2, 2) + 4 + >>> sum_ab(-2, 3) + 1 + >>> sum_ab(4.9, 5.1) + 10.0 + """ + return a + b + ``` + + These doctests will be run by pytest as part of our automated testing, so please try to run your doctests locally and make sure that they are found and pass: + + ```bash + python3 -m doctest -v my_submission.py + ``` + + The use of the Python built-in `input()` function is __not__ encouraged: + + ```python + input("Enter your input:") + # Or even worse... + input = eval(input("Enter your input: ")) + ``` + + However, if your code uses `input()` then we encourage you to gracefully deal with leading and trailing whitespace in user input by adding `.strip()` as in: + + ```python + starting_value = int(input("Please enter a starting value: ").strip()) + ``` + + The use of [Python type hints](https://docs.python.org/3/library/typing.html) is encouraged for function parameters and return values. Our CI runs [ty](https://docs.astral.sh/ty/) as an informational check that does not block merges yet, so you may want to run it locally before making your submission. + + ```bash + python3 -m pip install ty # only required the first time + ty check my_file_path.py + ``` + + ```python + def sum_ab(a: int, b: int) -> int: + return a + b + ``` + + [Install ty](https://docs.astral.sh/ty/installation/) and then please use the command `ty check` to test all files or `ty check path/to/file.py` to test a specific file. + +- [__List comprehensions and generators__](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions) are preferred over the use of `lambda`, `map`, `filter`, `reduce` but the important thing is to demonstrate the power of Python in code that is easy to read and maintain. + +- Avoid importing external libraries for basic algorithms. Only use those libraries for complicated algorithms. + +- If you need a third-party module that is not already listed in __pyproject.toml__, please add it to the `dependencies` there as part of your submission. The `uv-lock` pre-commit hook will update `uv.lock` to match. + +#### Other Requirements for Submissions + +- If you are submitting code in the `project_euler/` directory, please also read [the dedicated Guideline](https://github.com/TheAlgorithms/Python/blob/master/project_euler/README.md) before contributing to our Project Euler library. +- The file extension for code files should be `.py`. Jupyter Notebooks should be submitted to [TheAlgorithms/Jupyter](https://github.com/TheAlgorithms/Jupyter). +- Strictly use snake_case (underscore_separated) in your file_name, as it will be easy to parse in the future using scripts. +- Please avoid creating new directories if at all possible. Try to fit your work into the existing directory structure. +- If possible, follow the standard *within* the folder you are submitting to. +- If you have modified/added code, make sure the code compiles before submitting. +- If you have modified/added documentation work, ensure your language is concise and contains no grammar errors. +- Do not update the README.md or DIRECTORY.md file, which will be periodically autogenerated by our GitHub Actions processes. +- Add a corresponding explanation to [Algorithms-Explanation](https://github.com/TheAlgorithms/Algorithms-Explanation) (Optional but recommended). +- Our CI runs [__ty__](https://docs.astral.sh/ty/) on every pull request as an informational check that does not block merges yet, so we encourage you to add [__Python type hints__](https://docs.python.org/3/library/typing.html) where `ty` recommends to do so. +- Most importantly, + - __Be consistent in the use of these guidelines when submitting.__ + - __Join__ us on [Discord](https://discord.com/invite/c7MnfGFGa6) and [Gitter](https://gitter.im/TheAlgorithms/community) __now!__ + - Happy coding! + +Writer [@poyea](https://github.com/poyea), Jun 2019. diff --git a/DIRECTORY.md b/DIRECTORY.md new file mode 100644 index 000000000000..0418ee0974f4 --- /dev/null +++ b/DIRECTORY.md @@ -0,0 +1,1683 @@ +## Table of Contents +1. [Audio Filters](#audio-filters) +2. [Backtracking](#backtracking) +3. [Bit Manipulation](#bit-manipulation) +4. [Blockchain](#blockchain) +5. [Boolean Algebra](#boolean-algebra) +6. [Cellular Automata](#cellular-automata) +7. [Ciphers](#ciphers) +8. [Computer Vision](#computer-vision) +9. [Conversions](#conversions) +10. [Data Compression](#data-compression) +11. [Data Structures](#data-structures) +12. [Digital Image Processing](#digital-image-processing) +13. [Divide And Conquer](#divide-and-conquer) +14. [Docs](#docs) +15. [Dynamic Programming](#dynamic-programming) +16. [Electronics](#electronics) +17. [File Transfer](#file-transfer) +18. [Financial](#financial) +19. [Fractals](#fractals) +20. [Fuzzy Logic](#fuzzy-logic) +21. [Genetic Algorithm](#genetic-algorithm) +22. [Geodesy](#geodesy) +23. [Geometry](#geometry) +24. [Graphics](#graphics) +25. [Graphs](#graphs) +26. [Greedy Methods](#greedy-methods) +27. [Hashes](#hashes) +28. [Knapsack](#knapsack) +29. [Linear Algebra](#linear-algebra) +30. [Linear Programming](#linear-programming) +31. [Machine Learning](#machine-learning) +32. [Maths](#maths) +33. [Matrix](#matrix) +34. [Networking Flow](#networking-flow) +35. [Neural Network](#neural-network) +36. [Other](#other) +37. [Physics](#physics) +38. [Project Euler](#project-euler) +39. [Quantum](#quantum) +40. [Scheduling](#scheduling) +41. [Searches](#searches) +42. [Sorts](#sorts) +43. [Strings](#strings) +44. [Tests](#tests) +45. [Web Programming](#web-programming) + +## [Audio Filters](audio_filters) + * [Butterworth Filter](audio_filters/butterworth_filter.py) + * [Equal Loudness Filter](audio_filters/equal_loudness_filter.py) + * [Iir Filter](audio_filters/iir_filter.py) + * [Show Response](audio_filters/show_response.py) + +## [Backtracking](backtracking) + * [All Combinations](backtracking/all_combinations.py) + * [All Permutations](backtracking/all_permutations.py) + * [All Subsequences](backtracking/all_subsequences.py) + * [Coloring](backtracking/coloring.py) + * [Combination Sum](backtracking/combination_sum.py) + * [Crossword Puzzle Solver](backtracking/crossword_puzzle_solver.py) + * [Generate Parentheses](backtracking/generate_parentheses.py) + * [Generate Parentheses Iterative](backtracking/generate_parentheses_iterative.py) + * [Hamiltonian Cycle](backtracking/hamiltonian_cycle.py) + * [Knight Tour](backtracking/knight_tour.py) + * [M Coloring Problem](backtracking/m_coloring_problem.py) + * [Match Word Pattern](backtracking/match_word_pattern.py) + * [Minimax](backtracking/minimax.py) + * [N Queens](backtracking/n_queens.py) + * [N Queens Math](backtracking/n_queens_math.py) + * [Power Sum](backtracking/power_sum.py) + * [Rat In Maze](backtracking/rat_in_maze.py) + * [Sudoku](backtracking/sudoku.py) + * [Sum Of Subsets](backtracking/sum_of_subsets.py) + * [Word Break](backtracking/word_break.py) + * [Word Ladder](backtracking/word_ladder.py) + * [Word Search](backtracking/word_search.py) + +## [Bit Manipulation](bit_manipulation) + * [Binary And Operator](bit_manipulation/binary_and_operator.py) + * [Binary Coded Decimal](bit_manipulation/binary_coded_decimal.py) + * [Binary Count Setbits](bit_manipulation/binary_count_setbits.py) + * [Binary Count Trailing Zeros](bit_manipulation/binary_count_trailing_zeros.py) + * [Binary Or Operator](bit_manipulation/binary_or_operator.py) + * [Binary Shifts](bit_manipulation/binary_shifts.py) + * [Binary Twos Complement](bit_manipulation/binary_twos_complement.py) + * [Binary Xor Operator](bit_manipulation/binary_xor_operator.py) + * [Bitwise Addition Recursive](bit_manipulation/bitwise_addition_recursive.py) + * [Count 1S Brian Kernighan Method](bit_manipulation/count_1s_brian_kernighan_method.py) + * [Count Number Of One Bits](bit_manipulation/count_number_of_one_bits.py) + * [Excess 3 Code](bit_manipulation/excess_3_code.py) + * [Fast Walsh Hadamard Transform](bit_manipulation/fast_walsh_hadamard_transform.py) + * [Find Previous Power Of Two](bit_manipulation/find_previous_power_of_two.py) + * [Find Unique Number](bit_manipulation/find_unique_number.py) + * [Gray Code Sequence](bit_manipulation/gray_code_sequence.py) + * [Highest Set Bit](bit_manipulation/highest_set_bit.py) + * [Index Of Rightmost Set Bit](bit_manipulation/index_of_rightmost_set_bit.py) + * [Is Even](bit_manipulation/is_even.py) + * [Is Power Of Two](bit_manipulation/is_power_of_two.py) + * [Largest Pow Of Two Le Num](bit_manipulation/largest_pow_of_two_le_num.py) + * [Missing Number](bit_manipulation/missing_number.py) + * [Next Power Of Two](bit_manipulation/next_power_of_two.py) + * [Numbers Different Signs](bit_manipulation/numbers_different_signs.py) + * [Parity](bit_manipulation/parity.py) + * [Power Of 4](bit_manipulation/power_of_4.py) + * [Reverse Bits](bit_manipulation/reverse_bits.py) + * [Rotate Bits](bit_manipulation/rotate_bits.py) + * [Single Bit Manipulation Operations](bit_manipulation/single_bit_manipulation_operations.py) + * [Swap All Odd And Even Bits](bit_manipulation/swap_all_odd_and_even_bits.py) + * [Xor Swap Two Integers](bit_manipulation/xor_swap_two_integers.py) + +## [Blockchain](blockchain) + * [Diophantine Equation](blockchain/diophantine_equation.py) + * [Merkle Tree](blockchain/merkle_tree.py) + * [Proof Of Stake](blockchain/proof_of_stake.py) + * [Proof Of Work](blockchain/proof_of_work.py) + * [Simple Blockchain](blockchain/simple_blockchain.py) + * [Simple Proof Of Work](blockchain/simple_proof_of_work.py) + +## [Boolean Algebra](boolean_algebra) + * [And Gate](boolean_algebra/and_gate.py) + * [Imply Gate](boolean_algebra/imply_gate.py) + * [Karnaugh Map Simplification](boolean_algebra/karnaugh_map_simplification.py) + * [Multiplexer](boolean_algebra/multiplexer.py) + * [Nand Gate](boolean_algebra/nand_gate.py) + * [Nimply Gate](boolean_algebra/nimply_gate.py) + * [Nor Gate](boolean_algebra/nor_gate.py) + * [Not Gate](boolean_algebra/not_gate.py) + * [Or Gate](boolean_algebra/or_gate.py) + * [Quine Mc Cluskey](boolean_algebra/quine_mc_cluskey.py) + * [Xnor Gate](boolean_algebra/xnor_gate.py) + * [Xor Gate](boolean_algebra/xor_gate.py) + +## [Cellular Automata](cellular_automata) + * [Conways Game Of Life](cellular_automata/conways_game_of_life.py) + * [Elementary Cellular Automaton](cellular_automata/elementary_cellular_automaton.py) + * [Game Of Life](cellular_automata/game_of_life.py) + * [Langtons Ant](cellular_automata/langtons_ant.py) + * [Nagel Schrekenberg](cellular_automata/nagel_schrekenberg.py) + * [One Dimensional](cellular_automata/one_dimensional.py) + * [Wa Tor](cellular_automata/wa_tor.py) + +## [Ciphers](ciphers) + * [A1Z26](ciphers/a1z26.py) + * [Affine Cipher](ciphers/affine_cipher.py) + * [Atbash](ciphers/atbash.py) + * [Autokey](ciphers/autokey.py) + * [Baconian Cipher](ciphers/baconian_cipher.py) + * [Base16](ciphers/base16.py) + * [Base32](ciphers/base32.py) + * [Base58](ciphers/base58.py) + * [Base64 Cipher](ciphers/base64_cipher.py) + * [Base85](ciphers/base85.py) + * [Beaufort Cipher](ciphers/beaufort_cipher.py) + * [Bifid](ciphers/bifid.py) + * [Brute Force Caesar Cipher](ciphers/brute_force_caesar_cipher.py) + * [Caesar Cipher](ciphers/caesar_cipher.py) + * [Columnar Transposition](ciphers/columnar_transposition.py) + * [Cryptomath Module](ciphers/cryptomath_module.py) + * [Decrypt Caesar With Chi Squared](ciphers/decrypt_caesar_with_chi_squared.py) + * [Des Ecb](ciphers/des_ecb.py) + * [Deterministic Miller Rabin](ciphers/deterministic_miller_rabin.py) + * [Diffie](ciphers/diffie.py) + * [Diffie Hellman](ciphers/diffie_hellman.py) + * [Elgamal Key Generator](ciphers/elgamal_key_generator.py) + * [Enigma Machine2](ciphers/enigma_machine2.py) + * [Fractionated Morse Cipher](ciphers/fractionated_morse_cipher.py) + * [Gronsfeld Cipher](ciphers/gronsfeld_cipher.py) + * [Hill Cipher](ciphers/hill_cipher.py) + * [Mixed Keyword Cypher](ciphers/mixed_keyword_cypher.py) + * [Mono Alphabetic Ciphers](ciphers/mono_alphabetic_ciphers.py) + * [Morse Code](ciphers/morse_code.py) + * [Onepad Cipher](ciphers/onepad_cipher.py) + * [Permutation Cipher](ciphers/permutation_cipher.py) + * [Playfair Cipher](ciphers/playfair_cipher.py) + * [Polybius](ciphers/polybius.py) + * [Porta Cipher](ciphers/porta_cipher.py) + * [Rabin Miller](ciphers/rabin_miller.py) + * [Rail Fence Cipher](ciphers/rail_fence_cipher.py) + * [Rc4](ciphers/rc4.py) + * [Rot13](ciphers/rot13.py) + * [Rsa Cipher](ciphers/rsa_cipher.py) + * [Rsa Factorization](ciphers/rsa_factorization.py) + * [Rsa Key Generator](ciphers/rsa_key_generator.py) + * [Running Key Cipher](ciphers/running_key_cipher.py) + * [Shuffled Shift Cipher](ciphers/shuffled_shift_cipher.py) + * [Simple Keyword Cypher](ciphers/simple_keyword_cypher.py) + * [Simple Substitution Cipher](ciphers/simple_substitution_cipher.py) + * [Skytale Cipher](ciphers/skytale_cipher.py) + * [Transposition Cipher](ciphers/transposition_cipher.py) + * [Transposition Cipher Encrypt Decrypt File](ciphers/transposition_cipher_encrypt_decrypt_file.py) + * [Trifid Cipher](ciphers/trifid_cipher.py) + * [Vernam Cipher](ciphers/vernam_cipher.py) + * [Vigenere Cipher](ciphers/vigenere_cipher.py) + * [Xor Cipher](ciphers/xor_cipher.py) + * [Xtea](ciphers/xtea.py) + +## [Computer Vision](computer_vision) + * [Flip Augmentation](computer_vision/flip_augmentation.py) + * [Gramian](computer_vision/gramian.py) + * [Haralick Descriptors](computer_vision/haralick_descriptors.py) + * [Harris Corner](computer_vision/harris_corner.py) + * [Horn Schunck](computer_vision/horn_schunck.py) + * [Intensity Based Segmentation](computer_vision/intensity_based_segmentation.py) + * [Mean Threshold](computer_vision/mean_threshold.py) + * [Mosaic Augmentation](computer_vision/mosaic_augmentation.py) + * [Otsu Threshold](computer_vision/otsu_threshold.py) + * [Pooling Functions](computer_vision/pooling_functions.py) + * [Vision Transformer](computer_vision/vision_transformer.py) + +## [Conversions](conversions) + * [Astronomical Length Scale Conversion](conversions/astronomical_length_scale_conversion.py) + * [Base64 To Binary](conversions/base64_to_binary.py) + * [Binary To Base64](conversions/binary_to_base64.py) + * [Binary To Decimal](conversions/binary_to_decimal.py) + * [Binary To Excess3](conversions/binary_to_excess3.py) + * [Binary To Gray](conversions/binary_to_gray.py) + * [Binary To Gray Code](conversions/binary_to_gray_code.py) + * [Binary To Hexadecimal](conversions/binary_to_hexadecimal.py) + * [Binary To Octal](conversions/binary_to_octal.py) + * [Convert Number To Words](conversions/convert_number_to_words.py) + * [Decimal To Any](conversions/decimal_to_any.py) + * [Decimal To Binary](conversions/decimal_to_binary.py) + * [Decimal To Hexadecimal](conversions/decimal_to_hexadecimal.py) + * [Decimal To Octal](conversions/decimal_to_octal.py) + * [Endianness](conversions/endianness.py) + * [Energy Conversions](conversions/energy_conversions.py) + * [Excel Title To Column](conversions/excel_title_to_column.py) + * [Hex To Bin](conversions/hex_to_bin.py) + * [Hex To Rgb](conversions/hex_to_rgb.py) + * [Hexadecimal To Decimal](conversions/hexadecimal_to_decimal.py) + * [Int To Negative Binary Base](conversions/int_to_negative_binary_base.py) + * [Ipv4 Conversion](conversions/ipv4_conversion.py) + * [Length Conversion](conversions/length_conversion.py) + * [Molecular Chemistry](conversions/molecular_chemistry.py) + * [Negative Binary Base To Int](conversions/negative_binary_base_to_int.py) + * [Octal To Binary](conversions/octal_to_binary.py) + * [Octal To Decimal](conversions/octal_to_decimal.py) + * [Octal To Hexadecimal](conversions/octal_to_hexadecimal.py) + * [Prefix Conversions](conversions/prefix_conversions.py) + * [Prefix Conversions String](conversions/prefix_conversions_string.py) + * [Pressure Conversions](conversions/pressure_conversions.py) + * [Rectangular To Polar](conversions/rectangular_to_polar.py) + * [Rgb Cmyk Conversion](conversions/rgb_cmyk_conversion.py) + * [Rgb Hsv Conversion](conversions/rgb_hsv_conversion.py) + * [Roman Numerals](conversions/roman_numerals.py) + * [Speed Conversions](conversions/speed_conversions.py) + * [Temperature Conversions](conversions/temperature_conversions.py) + * [Time Conversions](conversions/time_conversions.py) + * [Volume Conversions](conversions/volume_conversions.py) + * [Weight Conversion](conversions/weight_conversion.py) + +## [Data Compression](data_compression) + * [Burrows Wheeler](data_compression/burrows_wheeler.py) + * [Coordinate Compression](data_compression/coordinate_compression.py) + * [Huffman](data_compression/huffman.py) + * [Lempel Ziv](data_compression/lempel_ziv.py) + * [Lempel Ziv Decompress](data_compression/lempel_ziv_decompress.py) + * [Lz77](data_compression/lz77.py) + * [Move To Front](data_compression/move_to_front.py) + * [Peak Signal To Noise Ratio](data_compression/peak_signal_to_noise_ratio.py) + * [Run Length Encoding](data_compression/run_length_encoding.py) + +## [Data Structures](data_structures) + * Arrays + * [Dynamic Array](data_structures/arrays/dynamic_array.py) + * [Equilibrium Index In Array](data_structures/arrays/equilibrium_index_in_array.py) + * [Find Triplets With 0 Sum](data_structures/arrays/find_triplets_with_0_sum.py) + * [Gas Station](data_structures/arrays/gas_station.py) + * [Index 2D Array In 1D](data_structures/arrays/index_2d_array_in_1d.py) + * [Kth Largest Element](data_structures/arrays/kth_largest_element.py) + * [Median Two Array](data_structures/arrays/median_two_array.py) + * [Merge Intervals](data_structures/arrays/merge_intervals.py) + * [Merge Sorted](data_structures/arrays/merge_sorted.py) + * [Monotonic Array](data_structures/arrays/monotonic_array.py) + * [Pairs With Given Sum](data_structures/arrays/pairs_with_given_sum.py) + * [Pairwise Iteration](data_structures/arrays/pairwise_iteration.py) + * [Permutations](data_structures/arrays/permutations.py) + * [Prefix Sum](data_structures/arrays/prefix_sum.py) + * [Product Sum](data_structures/arrays/product_sum.py) + * [Reverse Array](data_structures/arrays/reverse_array.py) + * [Rotate Array](data_structures/arrays/rotate_array.py) + * [Set Matrix Zeroes](data_structures/arrays/set_matrix_zeroes.py) + * [Sparse Table](data_structures/arrays/sparse_table.py) + * [Sudoku Solver](data_structures/arrays/sudoku_solver.py) + * Binary Tree + * [Avl Tree](data_structures/binary_tree/avl_tree.py) + * [Basic Binary Tree](data_structures/binary_tree/basic_binary_tree.py) + * [Binary Search Tree](data_structures/binary_tree/binary_search_tree.py) + * [Binary Search Tree Recursive](data_structures/binary_tree/binary_search_tree_recursive.py) + * [Binary Tree Maximum Path Sum](data_structures/binary_tree/binary_tree_maximum_path_sum.py) + * [Binary Tree Mirror](data_structures/binary_tree/binary_tree_mirror.py) + * [Binary Tree Node Sum](data_structures/binary_tree/binary_tree_node_sum.py) + * [Binary Tree Path Sum](data_structures/binary_tree/binary_tree_path_sum.py) + * [Binary Tree Traversals](data_structures/binary_tree/binary_tree_traversals.py) + * [Diameter Of Binary Tree](data_structures/binary_tree/diameter_of_binary_tree.py) + * [Diff Views Of Binary Tree](data_structures/binary_tree/diff_views_of_binary_tree.py) + * [Distribute Coins](data_structures/binary_tree/distribute_coins.py) + * [Fenwick Tree](data_structures/binary_tree/fenwick_tree.py) + * [Flatten Binarytree To Linkedlist](data_structures/binary_tree/flatten_binarytree_to_linkedlist.py) + * [Floor And Ceiling](data_structures/binary_tree/floor_and_ceiling.py) + * [Inorder Tree Traversal 2022](data_structures/binary_tree/inorder_tree_traversal_2022.py) + * [Is Sorted](data_structures/binary_tree/is_sorted.py) + * [Is Sum Tree](data_structures/binary_tree/is_sum_tree.py) + * [Lazy Segment Tree](data_structures/binary_tree/lazy_segment_tree.py) + * [Lowest Common Ancestor](data_structures/binary_tree/lowest_common_ancestor.py) + * [Maximum Fenwick Tree](data_structures/binary_tree/maximum_fenwick_tree.py) + * [Maximum Sum Bst](data_structures/binary_tree/maximum_sum_bst.py) + * [Merge Two Binary Trees](data_structures/binary_tree/merge_two_binary_trees.py) + * [Mirror Binary Tree](data_structures/binary_tree/mirror_binary_tree.py) + * [Non Recursive Segment Tree](data_structures/binary_tree/non_recursive_segment_tree.py) + * [Number Of Possible Binary Trees](data_structures/binary_tree/number_of_possible_binary_trees.py) + * [Persistent Segment Tree](data_structures/binary_tree/persistent_segment_tree.py) + * [Red Black Tree](data_structures/binary_tree/red_black_tree.py) + * [Segment Tree](data_structures/binary_tree/segment_tree.py) + * [Segment Tree Other](data_structures/binary_tree/segment_tree_other.py) + * [Serialize Deserialize Binary Tree](data_structures/binary_tree/serialize_deserialize_binary_tree.py) + * [Splay Tree](data_structures/binary_tree/splay_tree.py) + * [Symmetric Tree](data_structures/binary_tree/symmetric_tree.py) + * [Treap](data_structures/binary_tree/treap.py) + * [Wavelet Tree](data_structures/binary_tree/wavelet_tree.py) + * Disjoint Set + * [Alternate Disjoint Set](data_structures/disjoint_set/alternate_disjoint_set.py) + * [Disjoint Set](data_structures/disjoint_set/disjoint_set.py) + * Hashing + * [Bloom Filter](data_structures/hashing/bloom_filter.py) + * [Double Hash](data_structures/hashing/double_hash.py) + * [Hash Map](data_structures/hashing/hash_map.py) + * [Hash Table](data_structures/hashing/hash_table.py) + * [Hash Table With Linked List](data_structures/hashing/hash_table_with_linked_list.py) + * Number Theory + * [Prime Numbers](data_structures/hashing/number_theory/prime_numbers.py) + * [Quadratic Probing](data_structures/hashing/quadratic_probing.py) + * Tests + * [Test Hash Map](data_structures/hashing/tests/test_hash_map.py) + * Heap + * [Binomial Heap](data_structures/heap/binomial_heap.py) + * [Heap](data_structures/heap/heap.py) + * [Heap Generic](data_structures/heap/heap_generic.py) + * [Max Heap](data_structures/heap/max_heap.py) + * [Min Heap](data_structures/heap/min_heap.py) + * [Randomized Heap](data_structures/heap/randomized_heap.py) + * [Skew Heap](data_structures/heap/skew_heap.py) + * Kd Tree + * [Build Kdtree](data_structures/kd_tree/build_kdtree.py) + * Example + * [Example Usage](data_structures/kd_tree/example/example_usage.py) + * [Hypercube Points](data_structures/kd_tree/example/hypercube_points.py) + * [Kd Node](data_structures/kd_tree/kd_node.py) + * [Nearest Neighbour Search](data_structures/kd_tree/nearest_neighbour_search.py) + * Tests + * [Test Kdtree](data_structures/kd_tree/tests/test_kdtree.py) + * Linked List + * [Circular Linked List](data_structures/linked_list/circular_linked_list.py) + * [Deque Doubly](data_structures/linked_list/deque_doubly.py) + * [Doubly Linked List](data_structures/linked_list/doubly_linked_list.py) + * [Doubly Linked List Two](data_structures/linked_list/doubly_linked_list_two.py) + * [Floyds Cycle Detection](data_structures/linked_list/floyds_cycle_detection.py) + * [From Sequence](data_structures/linked_list/from_sequence.py) + * [Has Loop](data_structures/linked_list/has_loop.py) + * [Is Palindrome](data_structures/linked_list/is_palindrome.py) + * [Kth Element From End](data_structures/linked_list/kth_element_from_end.py) + * [Merge Sort Linked List](data_structures/linked_list/merge_sort_linked_list.py) + * [Merge Two Lists](data_structures/linked_list/merge_two_lists.py) + * [Middle Element Of Linked List](data_structures/linked_list/middle_element_of_linked_list.py) + * [Partition Linked List](data_structures/linked_list/partition_linked_list.py) + * [Print Reverse](data_structures/linked_list/print_reverse.py) + * [Remove Nth Node From End Of List](data_structures/linked_list/remove_nth_node_from_end_of_list.py) + * [Reverse K Group](data_structures/linked_list/reverse_k_group.py) + * [Rotate To The Right](data_structures/linked_list/rotate_to_the_right.py) + * [Singly Linked List](data_structures/linked_list/singly_linked_list.py) + * [Skip List](data_structures/linked_list/skip_list.py) + * [Sorted Linked List](data_structures/linked_list/sorted_linked_list.py) + * [Swap Nodes](data_structures/linked_list/swap_nodes.py) + * [Xor Linked List](data_structures/linked_list/xor_linked_list.py) + * Queues + * [Circular Queue](data_structures/queues/circular_queue.py) + * [Circular Queue Linked List](data_structures/queues/circular_queue_linked_list.py) + * [Double Ended Queue](data_structures/queues/double_ended_queue.py) + * [Linked Queue](data_structures/queues/linked_queue.py) + * [Monotonic Queue](data_structures/queues/monotonic_queue.py) + * [Priority Queue Using List](data_structures/queues/priority_queue_using_list.py) + * [Queue By List](data_structures/queues/queue_by_list.py) + * [Queue By Two Stacks](data_structures/queues/queue_by_two_stacks.py) + * [Queue On Pseudo Stack](data_structures/queues/queue_on_pseudo_stack.py) + * Stacks + * [Balanced Parentheses](data_structures/stacks/balanced_parentheses.py) + * [Dijkstras Two Stack Algorithm](data_structures/stacks/dijkstras_two_stack_algorithm.py) + * [Infix To Postfix Conversion](data_structures/stacks/infix_to_postfix_conversion.py) + * [Infix To Prefix Conversion](data_structures/stacks/infix_to_prefix_conversion.py) + * [Kth Next Greater Element](data_structures/stacks/kth_next_greater_element.py) + * [Largest Rectangle Histogram](data_structures/stacks/largest_rectangle_histogram.py) + * [Lexicographical Numbers](data_structures/stacks/lexicographical_numbers.py) + * [Next Greater Element](data_structures/stacks/next_greater_element.py) + * [Postfix Evaluation](data_structures/stacks/postfix_evaluation.py) + * [Postfix To Infix Conversion](data_structures/stacks/postfix_to_infix_conversion.py) + * [Prefix Evaluation](data_structures/stacks/prefix_evaluation.py) + * [Stack](data_structures/stacks/stack.py) + * [Stack Tracking Min Max](data_structures/stacks/stack_tracking_min_max.py) + * [Stack Using Two Queues](data_structures/stacks/stack_using_two_queues.py) + * [Stack With Doubly Linked List](data_structures/stacks/stack_with_doubly_linked_list.py) + * [Stack With Singly Linked List](data_structures/stacks/stack_with_singly_linked_list.py) + * [Stock Span Problem](data_structures/stacks/stock_span_problem.py) + * Suffix Tree + * Example + * [Example Usage](data_structures/suffix_tree/example/example_usage.py) + * [Suffix Tree](data_structures/suffix_tree/suffix_tree.py) + * [Suffix Tree Node](data_structures/suffix_tree/suffix_tree_node.py) + * Tests + * [Test Suffix Tree](data_structures/suffix_tree/tests/test_suffix_tree.py) + * Trie + * [Radix Tree](data_structures/trie/radix_tree.py) + * [Trie](data_structures/trie/trie.py) + +## [Digital Image Processing](digital_image_processing) + * [Change Brightness](digital_image_processing/change_brightness.py) + * [Change Contrast](digital_image_processing/change_contrast.py) + * [Convert To Negative](digital_image_processing/convert_to_negative.py) + * Dithering + * [Burkes](digital_image_processing/dithering/burkes.py) + * Edge Detection + * [Canny](digital_image_processing/edge_detection/canny.py) + * Filters + * [Bilateral Filter](digital_image_processing/filters/bilateral_filter.py) + * [Convolve](digital_image_processing/filters/convolve.py) + * [Gabor Filter](digital_image_processing/filters/gabor_filter.py) + * [Gaussian Filter](digital_image_processing/filters/gaussian_filter.py) + * [Laplacian Filter](digital_image_processing/filters/laplacian_filter.py) + * [Local Binary Pattern](digital_image_processing/filters/local_binary_pattern.py) + * [Median Filter](digital_image_processing/filters/median_filter.py) + * [Sobel Filter](digital_image_processing/filters/sobel_filter.py) + * Histogram Equalization + * [Histogram Stretch](digital_image_processing/histogram_equalization/histogram_stretch.py) + * [Index Calculation](digital_image_processing/index_calculation.py) + * Morphological Operations + * [Dilation Operation](digital_image_processing/morphological_operations/dilation_operation.py) + * [Erosion Operation](digital_image_processing/morphological_operations/erosion_operation.py) + * Resize + * [Resize](digital_image_processing/resize/resize.py) + * Rotation + * [Rotation](digital_image_processing/rotation/rotation.py) + * [Sepia](digital_image_processing/sepia.py) + * [Test Digital Image Processing](digital_image_processing/test_digital_image_processing.py) + +## [Divide And Conquer](divide_and_conquer) + * [Closest Pair Of Points](divide_and_conquer/closest_pair_of_points.py) + * [Convex Hull](divide_and_conquer/convex_hull.py) + * [Heaps Algorithm](divide_and_conquer/heaps_algorithm.py) + * [Heaps Algorithm Iterative](divide_and_conquer/heaps_algorithm_iterative.py) + * [Inversions](divide_and_conquer/inversions.py) + * [Kth Order Statistic](divide_and_conquer/kth_order_statistic.py) + * [Max Difference Pair](divide_and_conquer/max_difference_pair.py) + * [Max Subarray](divide_and_conquer/max_subarray.py) + * [Mergesort](divide_and_conquer/mergesort.py) + * [Minimum Element Of Array](divide_and_conquer/minimum_element_of_array.py) + * [Peak](divide_and_conquer/peak.py) + * [Power](divide_and_conquer/power.py) + * [Strassen Matrix Multiplication](divide_and_conquer/strassen_matrix_multiplication.py) + * Tests + * [Test Strassen Matrix Multiplication](divide_and_conquer/tests/test_strassen_matrix_multiplication.py) + +## [Docs](docs) + * [Conf](docs/conf.py) + +## [Dynamic Programming](dynamic_programming) + * [Abbreviation](dynamic_programming/abbreviation.py) + * [All Construct](dynamic_programming/all_construct.py) + * [Bitmask](dynamic_programming/bitmask.py) + * [Catalan Numbers](dynamic_programming/catalan_numbers.py) + * [Climbing Stairs](dynamic_programming/climbing_stairs.py) + * [Combination Sum Iv](dynamic_programming/combination_sum_iv.py) + * [Edit Distance](dynamic_programming/edit_distance.py) + * [Egg Dropping](dynamic_programming/egg_dropping.py) + * [Factorial](dynamic_programming/factorial.py) + * [Fast Fibonacci](dynamic_programming/fast_fibonacci.py) + * [Fibonacci](dynamic_programming/fibonacci.py) + * [Fizz Buzz](dynamic_programming/fizz_buzz.py) + * [Floyd Warshall](dynamic_programming/floyd_warshall.py) + * [Integer Partition](dynamic_programming/integer_partition.py) + * [Iterating Through Submasks](dynamic_programming/iterating_through_submasks.py) + * [Knapsack](dynamic_programming/knapsack.py) + * [Largest Divisible Subset](dynamic_programming/largest_divisible_subset.py) + * [Longest Common Subsequence](dynamic_programming/longest_common_subsequence.py) + * [Longest Common Substring](dynamic_programming/longest_common_substring.py) + * [Longest Increasing Subsequence](dynamic_programming/longest_increasing_subsequence.py) + * [Longest Increasing Subsequence Iterative](dynamic_programming/longest_increasing_subsequence_iterative.py) + * [Longest Increasing Subsequence O Nlogn](dynamic_programming/longest_increasing_subsequence_o_nlogn.py) + * [Longest Palindromic Subsequence](dynamic_programming/longest_palindromic_subsequence.py) + * [Longest Repeating Subsequence](dynamic_programming/longest_repeating_subsequence.py) + * [Matrix Chain Multiplication](dynamic_programming/matrix_chain_multiplication.py) + * [Matrix Chain Order](dynamic_programming/matrix_chain_order.py) + * [Max Non Adjacent Sum](dynamic_programming/max_non_adjacent_sum.py) + * [Max Product Subarray](dynamic_programming/max_product_subarray.py) + * [Max Subarray Sum](dynamic_programming/max_subarray_sum.py) + * [Min Distance Up Bottom](dynamic_programming/min_distance_up_bottom.py) + * [Minimum Coin Change](dynamic_programming/minimum_coin_change.py) + * [Minimum Cost Path](dynamic_programming/minimum_cost_path.py) + * [Minimum Partition](dynamic_programming/minimum_partition.py) + * [Minimum Size Subarray Sum](dynamic_programming/minimum_size_subarray_sum.py) + * [Minimum Squares To Represent A Number](dynamic_programming/minimum_squares_to_represent_a_number.py) + * [Minimum Steps To One](dynamic_programming/minimum_steps_to_one.py) + * [Minimum Tickets Cost](dynamic_programming/minimum_tickets_cost.py) + * [Narcissistic Number](dynamic_programming/narcissistic_number.py) + * [Needleman Wunsch](dynamic_programming/needleman_wunsch.py) + * [Optimal Binary Search Tree](dynamic_programming/optimal_binary_search_tree.py) + * [Palindrome Partitioning](dynamic_programming/palindrome_partitioning.py) + * [Range Sum Query](dynamic_programming/range_sum_query.py) + * [Regex Match](dynamic_programming/regex_match.py) + * [Rod Cutting](dynamic_programming/rod_cutting.py) + * [Smith Waterman](dynamic_programming/smith_waterman.py) + * [Subset Generation](dynamic_programming/subset_generation.py) + * [Sum Of Subset](dynamic_programming/sum_of_subset.py) + * [Trapped Water](dynamic_programming/trapped_water.py) + * [Tribonacci](dynamic_programming/tribonacci.py) + * [Viterbi](dynamic_programming/viterbi.py) + * [Wildcard Matching](dynamic_programming/wildcard_matching.py) + * [Word Break](dynamic_programming/word_break.py) + +## [Electronics](electronics) + * [Apparent Power](electronics/apparent_power.py) + * [Builtin Voltage](electronics/builtin_voltage.py) + * [Capacitor Equivalence](electronics/capacitor_equivalence.py) + * [Carrier Concentration](electronics/carrier_concentration.py) + * [Charging Capacitor](electronics/charging_capacitor.py) + * [Charging Inductor](electronics/charging_inductor.py) + * [Circular Convolution](electronics/circular_convolution.py) + * [Coulombs Law](electronics/coulombs_law.py) + * [Electric Conductivity](electronics/electric_conductivity.py) + * [Electric Power](electronics/electric_power.py) + * [Electrical Impedance](electronics/electrical_impedance.py) + * [Full Wave Rectifier](electronics/full_wave_rectifier.py) + * [Half Wave Rectifier](electronics/half_wave_rectifier.py) + * [Ic 555 Timer](electronics/ic_555_timer.py) + * [Ind Reactance](electronics/ind_reactance.py) + * [Ohms Law](electronics/ohms_law.py) + * [Power Factor Correction](electronics/power_factor_correction.py) + * [Real And Reactive Power](electronics/real_and_reactive_power.py) + * [Resistor Color Code](electronics/resistor_color_code.py) + * [Resistor Equivalence](electronics/resistor_equivalence.py) + * [Resonant Frequency](electronics/resonant_frequency.py) + * [Star Delta Conversions](electronics/star_delta_conversions.py) + * [Wheatstone Bridge](electronics/wheatstone_bridge.py) + +## [File Transfer](file_transfer) + * [Receive File](file_transfer/receive_file.py) + * [Send File](file_transfer/send_file.py) + * Tests + * [Test Send File](file_transfer/tests/test_send_file.py) + +## [Financial](financial) + * [Annuity Due Future Value](financial/annuity_due_future_value.py) + * [Equated Monthly Installments](financial/equated_monthly_installments.py) + * [Expected Shortfall](financial/expected_shortfall.py) + * [Exponential Moving Average](financial/exponential_moving_average.py) + * [Fractional Differentiation](financial/fractional_differentiation.py) + * [Interest](financial/interest.py) + * [Kelly Criterion](financial/kelly_criterion.py) + * [Macaulay Duration](financial/macaulay_duration.py) + * [Ordinary Annuity Future Value](financial/ordinary_annuity_future_value.py) + * [Present Value](financial/present_value.py) + * [Price Plus Tax](financial/price_plus_tax.py) + * [Sharpe Ratio](financial/sharpe_ratio.py) + * [Simple Moving Average](financial/simple_moving_average.py) + * [Straight Line Depreciation](financial/straight_line_depreciation.py) + * [Streaming Moving Average](financial/streaming_moving_average.py) + * [Time And Half Pay](financial/time_and_half_pay.py) + * [Value At Risk](financial/value_at_risk.py) + +## [Fractals](fractals) + * [Barnsley Fern](fractals/barnsley_fern.py) + * [Hilbert Curve](fractals/hilbert_curve.py) + * [Julia Sets](fractals/julia_sets.py) + * [Koch Snowflake](fractals/koch_snowflake.py) + * [Mandelbrot](fractals/mandelbrot.py) + * [Sierpinski Carpet](fractals/sierpinski_carpet.py) + * [Sierpinski Triangle](fractals/sierpinski_triangle.py) + * [Vicsek](fractals/vicsek.py) + +## [Fuzzy Logic](fuzzy_logic) + * [Fuzzy Operations](fuzzy_logic/fuzzy_operations.py) + * [Fuzzy Set Operations](fuzzy_logic/fuzzy_set_operations.py) + +## [Genetic Algorithm](genetic_algorithm) + * [Basic String](genetic_algorithm/basic_string.py) + * [Travelling Salesman Problem](genetic_algorithm/travelling_salesman_problem.py) + +## [Geodesy](geodesy) + * [Haversine Distance](geodesy/haversine_distance.py) + * [Lamberts Ellipsoidal Distance](geodesy/lamberts_ellipsoidal_distance.py) + * [Radar Target Calculation](geodesy/radar_target_calculation.py) + +## [Geometry](geometry) + * [Braik Mac Construction](geometry/braik_mac_construction.py) + * [Geometry](geometry/geometry.py) + * [Graham Scan](geometry/graham_scan.py) + * [Jarvis March](geometry/jarvis_march.py) + * [Monotone Chain](geometry/monotone_chain.py) + * [Ramer Douglas Peucker](geometry/ramer_douglas_peucker.py) + * [Rotating Calipers](geometry/rotating_calipers.py) + * [Segment Intersection](geometry/segment_intersection.py) + * [Shoelace](geometry/shoelace.py) + * Tests + * [Test Graham Scan](geometry/tests/test_graham_scan.py) + * [Test Jarvis March](geometry/tests/test_jarvis_march.py) + * [Triangle](geometry/triangle.py) + +## [Graphics](graphics) + * [Bezier Curve](graphics/bezier_curve.py) + * [Bresenham Line Basic](graphics/bresenham_line_basic.py) + * [Bresenham Line Generalized](graphics/bresenham_line_generalized.py) + * [Butterfly Pattern](graphics/butterfly_pattern.py) + * [Digital Differential Analyzer Line](graphics/digital_differential_analyzer_line.py) + * [Vector3 For 2D Rendering](graphics/vector3_for_2d_rendering.py) + +## [Graphs](graphs) + * [A Star](graphs/a_star.py) + * [Ant Colony Optimization Algorithms](graphs/ant_colony_optimization_algorithms.py) + * [Articulation Points](graphs/articulation_points.py) + * [Basic Graphs](graphs/basic_graphs.py) + * [Bellman Ford](graphs/bellman_ford.py) + * [Bi Directional Dijkstra](graphs/bi_directional_dijkstra.py) + * [Bidirectional A Star](graphs/bidirectional_a_star.py) + * [Bidirectional Breadth First Search](graphs/bidirectional_breadth_first_search.py) + * [Bidirectional Search](graphs/bidirectional_search.py) + * [Boruvka](graphs/boruvka.py) + * [Breadth First Search](graphs/breadth_first_search.py) + * [Breadth First Search 2](graphs/breadth_first_search_2.py) + * [Breadth First Search Shortest Path](graphs/breadth_first_search_shortest_path.py) + * [Breadth First Search Shortest Path 2](graphs/breadth_first_search_shortest_path_2.py) + * [Breadth First Search Zero One Shortest Path](graphs/breadth_first_search_zero_one_shortest_path.py) + * [Check Bipatrite](graphs/check_bipatrite.py) + * [Check Cycle](graphs/check_cycle.py) + * [Connected Components](graphs/connected_components.py) + * [Deep Clone Graph](graphs/deep_clone_graph.py) + * [Depth First Search](graphs/depth_first_search.py) + * [Depth First Search 2](graphs/depth_first_search_2.py) + * [Dijkstra](graphs/dijkstra.py) + * [Dijkstra 2](graphs/dijkstra_2.py) + * [Dijkstra Algorithm](graphs/dijkstra_algorithm.py) + * [Dijkstra Alternate](graphs/dijkstra_alternate.py) + * [Dijkstra Binary Grid](graphs/dijkstra_binary_grid.py) + * [Dinic](graphs/dinic.py) + * [Directed And Undirected Weighted Graph](graphs/directed_and_undirected_weighted_graph.py) + * [Edmonds Blossom Algorithm](graphs/edmonds_blossom_algorithm.py) + * [Edmonds Karp Multiple Source And Sink](graphs/edmonds_karp_multiple_source_and_sink.py) + * [Eulerian Path And Circuit For Undirected Graph](graphs/eulerian_path_and_circuit_for_undirected_graph.py) + * [Even Tree](graphs/even_tree.py) + * [Finding Bridges](graphs/finding_bridges.py) + * [Frequent Pattern Graph Miner](graphs/frequent_pattern_graph_miner.py) + * [G Topological Sort](graphs/g_topological_sort.py) + * [Gale Shapley Bigraph](graphs/gale_shapley_bigraph.py) + * [Graph Adjacency List](graphs/graph_adjacency_list.py) + * [Graph Adjacency Matrix](graphs/graph_adjacency_matrix.py) + * [Graph Centrality](graphs/graph_centrality.py) + * [Graph List](graphs/graph_list.py) + * [Graph Peripherality](graphs/graph_peripherality.py) + * [Graphs Floyd Warshall](graphs/graphs_floyd_warshall.py) + * [Greedy Best First](graphs/greedy_best_first.py) + * [Greedy Min Vertex Cover](graphs/greedy_min_vertex_cover.py) + * [Hopcroft Karp](graphs/hopcroft_karp.py) + * [Johnson](graphs/johnson.py) + * [Kahns Algorithm Long](graphs/kahns_algorithm_long.py) + * [Kahns Algorithm Topo](graphs/kahns_algorithm_topo.py) + * [Karger](graphs/karger.py) + * [Lanczos Eigenvectors](graphs/lanczos_eigenvectors.py) + * [Markov Chain](graphs/markov_chain.py) + * [Matching Min Vertex Cover](graphs/matching_min_vertex_cover.py) + * [Minimum Path Sum](graphs/minimum_path_sum.py) + * [Minimum Spanning Tree Boruvka](graphs/minimum_spanning_tree_boruvka.py) + * [Minimum Spanning Tree Kruskal](graphs/minimum_spanning_tree_kruskal.py) + * [Minimum Spanning Tree Kruskal2](graphs/minimum_spanning_tree_kruskal2.py) + * [Minimum Spanning Tree Prims](graphs/minimum_spanning_tree_prims.py) + * [Minimum Spanning Tree Prims2](graphs/minimum_spanning_tree_prims2.py) + * [Multi Heuristic Astar](graphs/multi_heuristic_astar.py) + * [Page Rank](graphs/page_rank.py) + * [Prim](graphs/prim.py) + * [Random Graph Generator](graphs/random_graph_generator.py) + * [Scc Kosaraju](graphs/scc_kosaraju.py) + * [Strongly Connected Components](graphs/strongly_connected_components.py) + * [Tarjans Scc](graphs/tarjans_scc.py) + * Tests + * [Test Edmonds Blossom Algorithm](graphs/tests/test_edmonds_blossom_algorithm.py) + * [Test Graphs Floyd Warshall](graphs/tests/test_graphs_floyd_warshall.py) + * [Test Johnson](graphs/tests/test_johnson.py) + * [Test Min Spanning Tree Kruskal](graphs/tests/test_min_spanning_tree_kruskal.py) + * [Test Min Spanning Tree Prim](graphs/tests/test_min_spanning_tree_prim.py) + * [Travelling Salesman Problem](graphs/travelling_salesman_problem.py) + +## [Greedy Methods](greedy_methods) + * [Activity Selection](greedy_methods/activity_selection.py) + * [Best Time To Buy And Sell Stock](greedy_methods/best_time_to_buy_and_sell_stock.py) + * [Fractional Cover Problem](greedy_methods/fractional_cover_problem.py) + * [Fractional Knapsack](greedy_methods/fractional_knapsack.py) + * [Fractional Knapsack 2](greedy_methods/fractional_knapsack_2.py) + * [Gas Station](greedy_methods/gas_station.py) + * [Minimize Deviation In Array](greedy_methods/minimize_deviation_in_array.py) + * [Minimum Coin Change](greedy_methods/minimum_coin_change.py) + * [Minimum Waiting Time](greedy_methods/minimum_waiting_time.py) + * [Optimal Merge Pattern](greedy_methods/optimal_merge_pattern.py) + * [Sliding Window](greedy_methods/sliding_window.py) + * [Smallest Range](greedy_methods/smallest_range.py) + +## [Hashes](hashes) + * [Adler32](hashes/adler32.py) + * [Chaos Machine](hashes/chaos_machine.py) + * [Crc32](hashes/crc32.py) + * [Djb2](hashes/djb2.py) + * [Elf](hashes/elf.py) + * [Enigma Machine](hashes/enigma_machine.py) + * [Fletcher16](hashes/fletcher16.py) + * [Hamming Code](hashes/hamming_code.py) + * [Jenkins One At A Time](hashes/jenkins_one_at_a_time.py) + * [Luhn](hashes/luhn.py) + * [Md5](hashes/md5.py) + * [Sdbm](hashes/sdbm.py) + * [Sha1](hashes/sha1.py) + * [Sha256](hashes/sha256.py) + +## [Knapsack](knapsack) + * [Greedy Knapsack](knapsack/greedy_knapsack.py) + * [Knapsack](knapsack/knapsack.py) + * [Recursive Approach Knapsack](knapsack/recursive_approach_knapsack.py) + * Tests + * [Test Greedy Knapsack](knapsack/tests/test_greedy_knapsack.py) + * [Test Knapsack](knapsack/tests/test_knapsack.py) + +## [Linear Algebra](linear_algebra) + * [Gauss Jordan](linear_algebra/gauss_jordan.py) + * [Gaussian Elimination](linear_algebra/gaussian_elimination.py) + * [Jacobi Iteration Method](linear_algebra/jacobi_iteration_method.py) + * [Lu Decomposition](linear_algebra/lu_decomposition.py) + * [Matrix Inversion](linear_algebra/matrix_inversion.py) + * Src + * [Conjugate Gradient](linear_algebra/src/conjugate_gradient.py) + * [Gaussian Elimination Pivoting](linear_algebra/src/gaussian_elimination_pivoting.py) + * [Lib](linear_algebra/src/lib.py) + * [Polynom For Points](linear_algebra/src/polynom_for_points.py) + * [Power Iteration](linear_algebra/src/power_iteration.py) + * [Rank Of Matrix](linear_algebra/src/rank_of_matrix.py) + * [Rayleigh Quotient](linear_algebra/src/rayleigh_quotient.py) + * [Schur Complement](linear_algebra/src/schur_complement.py) + * [Test Linear Algebra](linear_algebra/src/test_linear_algebra.py) + * [Transformations 2D](linear_algebra/src/transformations_2d.py) + +## [Linear Programming](linear_programming) + * [Simplex](linear_programming/simplex.py) + +## [Machine Learning](machine_learning) + * [Apriori Algorithm](machine_learning/apriori_algorithm.py) + * [Astar](machine_learning/astar.py) + * [Automatic Differentiation](machine_learning/automatic_differentiation.py) + * [Data Transformations](machine_learning/data_transformations.py) + * [Dbscan](machine_learning/dbscan.py) + * [Decision Tree](machine_learning/decision_tree.py) + * [Dimensionality Reduction](machine_learning/dimensionality_reduction.py) + * [Federated Averaging](machine_learning/federated_averaging.py) + * Forecasting + * [Run](machine_learning/forecasting/run.py) + * [Frequent Pattern Growth](machine_learning/frequent_pattern_growth.py) + * [Gaussian Mixture Model](machine_learning/gaussian_mixture_model.py) + * [Gaussian Naive Bayes](machine_learning/gaussian_naive_bayes.py) + * [Gradient Boosting Classifier](machine_learning/gradient_boosting_classifier.py) + * [Gradient Boosting Regressor](machine_learning/gradient_boosting_regressor.py) + * [Gradient Descent](machine_learning/gradient_descent.py) + * [K Means Clust](machine_learning/k_means_clust.py) + * [K Medoids](machine_learning/k_medoids.py) + * [K Nearest Neighbors](machine_learning/k_nearest_neighbors.py) + * [Linear Discriminant Analysis](machine_learning/linear_discriminant_analysis.py) + * [Linear Regression](machine_learning/linear_regression.py) + * [Linear Regression Vectorized](machine_learning/linear_regression_vectorized.py) + * Local Weighted Learning + * [Local Weighted Learning](machine_learning/local_weighted_learning/local_weighted_learning.py) + * [Logistic Regression](machine_learning/logistic_regression.py) + * [Loss Functions](machine_learning/loss_functions.py) + * [Mab](machine_learning/mab.py) + * [Mean Shift](machine_learning/mean_shift.py) + * [Mfcc](machine_learning/mfcc.py) + * [Mini Batch Gradient Descent](machine_learning/mini_batch_gradient_descent.py) + * [Multilayer Perceptron Classifier](machine_learning/multilayer_perceptron_classifier.py) + * [Naive Bayes Text Classification](machine_learning/naive_bayes_text_classification.py) + * [Ordinary Least Squares Regression](machine_learning/ordinary_least_squares_regression.py) + * [Polynomial Regression](machine_learning/polynomial_regression.py) + * [Principle Component Analysis](machine_learning/principle_component_analysis.py) + * [Q Learning](machine_learning/q_learning.py) + * [Random Forest Classifier](machine_learning/random_forest_classifier.py) + * [Random Forest Regressor](machine_learning/random_forest_regressor.py) + * [Ridge Regression](machine_learning/ridge_regression.py) + * [Rmsprop](machine_learning/rmsprop.py) + * [Scoring Functions](machine_learning/scoring_functions.py) + * [Self Organizing Map](machine_learning/self_organizing_map.py) + * [Sequential Minimum Optimization](machine_learning/sequential_minimum_optimization.py) + * [Similarity Search](machine_learning/similarity_search.py) + * [Support Vector Machines](machine_learning/support_vector_machines.py) + * [T Stochastic Neighbour Embedding](machine_learning/t_stochastic_neighbour_embedding.py) + * [Word Frequency Functions](machine_learning/word_frequency_functions.py) + +## [Maths](maths) + * [Abs](maths/abs.py) + * [Addition Without Arithmetic](maths/addition_without_arithmetic.py) + * [Aliquot Sum](maths/aliquot_sum.py) + * [Allocation Number](maths/allocation_number.py) + * [Arc Length](maths/arc_length.py) + * [Area](maths/area.py) + * [Area Under Curve](maths/area_under_curve.py) + * [Autocorrelation](maths/autocorrelation.py) + * [Average Absolute Deviation](maths/average_absolute_deviation.py) + * [Average Mean](maths/average_mean.py) + * [Average Median](maths/average_median.py) + * [Average Mode](maths/average_mode.py) + * [Bailey Borwein Plouffe](maths/bailey_borwein_plouffe.py) + * [Base Neg2 Conversion](maths/base_neg2_conversion.py) + * [Basic Maths](maths/basic_maths.py) + * [Bearing](maths/bearing.py) + * [Binary Exponentiation](maths/binary_exponentiation.py) + * [Binary Multiplication](maths/binary_multiplication.py) + * [Binomial Coefficient](maths/binomial_coefficient.py) + * [Binomial Distribution](maths/binomial_distribution.py) + * [Binomial Expansion](maths/binomial_expansion.py) + * [Ceil](maths/ceil.py) + * [Chebyshev Distance](maths/chebyshev_distance.py) + * [Check Polygon](maths/check_polygon.py) + * [Chinese Remainder Theorem](maths/chinese_remainder_theorem.py) + * [Cholesky Decomposition](maths/cholesky_decomposition.py) + * [Chudnovsky Algorithm](maths/chudnovsky_algorithm.py) + * [Collatz Sequence](maths/collatz_sequence.py) + * [Combinations](maths/combinations.py) + * [Continued Fraction](maths/continued_fraction.py) + * [Convolve 1D](maths/convolve_1d.py) + * [Decimal Isolate](maths/decimal_isolate.py) + * [Decimal To Fraction](maths/decimal_to_fraction.py) + * [Derangement](maths/derangement.py) + * [Digital Root](maths/digital_root.py) + * [Disarium](maths/disarium.py) + * [Dodecahedron](maths/dodecahedron.py) + * [Double Factorial](maths/double_factorial.py) + * [Dual Number Automatic Differentiation](maths/dual_number_automatic_differentiation.py) + * [Ear Clipping Polygon Triangulation](maths/ear_clipping_polygon_triangulation.py) + * [Entropy](maths/entropy.py) + * [Euclidean Distance](maths/euclidean_distance.py) + * [Euler Method](maths/euler_method.py) + * [Euler Modified](maths/euler_modified.py) + * [Eulers Totient](maths/eulers_totient.py) + * [Extended Euclidean Algorithm](maths/extended_euclidean_algorithm.py) + * [Factorial](maths/factorial.py) + * [Factors](maths/factors.py) + * [Fast Inverse Sqrt](maths/fast_inverse_sqrt.py) + * [Fermat Little Theorem](maths/fermat_little_theorem.py) + * [Fibonacci](maths/fibonacci.py) + * [Find Max](maths/find_max.py) + * [Find Min](maths/find_min.py) + * [First Fundamental Form](maths/first_fundamental_form.py) + * [Floor](maths/floor.py) + * [Gamma](maths/gamma.py) + * [Gaussian](maths/gaussian.py) + * [Gcd Of N Numbers](maths/gcd_of_n_numbers.py) + * [Geometric Mean](maths/geometric_mean.py) + * [Germain Primes](maths/germain_primes.py) + * [Greatest Common Divisor](maths/greatest_common_divisor.py) + * [Hardy Ramanujanalgo](maths/hardy_ramanujanalgo.py) + * [Integer Square Root](maths/integer_square_root.py) + * [Interquartile Range](maths/interquartile_range.py) + * [Is Int Palindrome](maths/is_int_palindrome.py) + * [Is Ip V4 Address Valid](maths/is_ip_v4_address_valid.py) + * [Is Ipv6 Address Valid](maths/is_ipv6_address_valid.py) + * [Is Square Free](maths/is_square_free.py) + * [Jaccard Similarity](maths/jaccard_similarity.py) + * [Joint Probability Distribution](maths/joint_probability_distribution.py) + * [Josephus Problem](maths/josephus_problem.py) + * [Juggler Sequence](maths/juggler_sequence.py) + * [Karatsuba](maths/karatsuba.py) + * [Kth Lexicographic Permutation](maths/kth_lexicographic_permutation.py) + * [Laplace Transformation](maths/laplace_transformation.py) + * [Largest Of Very Large Numbers](maths/largest_of_very_large_numbers.py) + * [Least Common Multiple](maths/least_common_multiple.py) + * [Line Intersection](maths/line_intersection.py) + * [Line Length](maths/line_length.py) + * [Liouville Lambda](maths/liouville_lambda.py) + * [Lucas Lehmer Primality Test](maths/lucas_lehmer_primality_test.py) + * [Lucas Series](maths/lucas_series.py) + * [Maclaurin Series](maths/maclaurin_series.py) + * [Manhattan Distance](maths/manhattan_distance.py) + * [Matrix Exponentiation](maths/matrix_exponentiation.py) + * [Max Sum Sliding Window](maths/max_sum_sliding_window.py) + * [Minkowski Distance](maths/minkowski_distance.py) + * [Mobius Function](maths/mobius_function.py) + * [Modular Division](maths/modular_division.py) + * [Modular Exponential](maths/modular_exponential.py) + * [Monte Carlo](maths/monte_carlo.py) + * [Monte Carlo Dice](maths/monte_carlo_dice.py) + * [Ncr Combinations](maths/ncr_combinations.py) + * [Next Prime Number](maths/next_prime_number.py) + * [Number Of Digits](maths/number_of_digits.py) + * Numerical Analysis + * [Adams Bashforth](maths/numerical_analysis/adams_bashforth.py) + * [Bisection](maths/numerical_analysis/bisection.py) + * [Bisection 2](maths/numerical_analysis/bisection_2.py) + * [Brent Method](maths/numerical_analysis/brent_method.py) + * [Gauss Seidel Method](maths/numerical_analysis/gauss_seidel_method.py) + * [Integration By Simpson Approx](maths/numerical_analysis/integration_by_simpson_approx.py) + * [Intersection](maths/numerical_analysis/intersection.py) + * [Nevilles Method](maths/numerical_analysis/nevilles_method.py) + * [Newton Forward Interpolation](maths/numerical_analysis/newton_forward_interpolation.py) + * [Newton Raphson](maths/numerical_analysis/newton_raphson.py) + * [Nth Root](maths/numerical_analysis/nth_root.py) + * [Numerical Integration](maths/numerical_analysis/numerical_integration.py) + * [Proper Fractions](maths/numerical_analysis/proper_fractions.py) + * [Runge Kutta](maths/numerical_analysis/runge_kutta.py) + * [Runge Kutta Fehlberg 45](maths/numerical_analysis/runge_kutta_fehlberg_45.py) + * [Runge Kutta Gills](maths/numerical_analysis/runge_kutta_gills.py) + * [Secant Method](maths/numerical_analysis/secant_method.py) + * [Simpson Rule](maths/numerical_analysis/simpson_rule.py) + * [Square Root](maths/numerical_analysis/square_root.py) + * [Weierstrass Method](maths/numerical_analysis/weierstrass_method.py) + * [Odd Sieve](maths/odd_sieve.py) + * [Padovan Sequence](maths/padovan_sequence.py) + * [Pell Number](maths/pell_number.py) + * [Perfect Cube](maths/perfect_cube.py) + * [Perfect Number](maths/perfect_number.py) + * [Perfect Square](maths/perfect_square.py) + * [Persistence](maths/persistence.py) + * [Pi Generator](maths/pi_generator.py) + * [Pi Monte Carlo Estimation](maths/pi_monte_carlo_estimation.py) + * [Points Are Collinear 3D](maths/points_are_collinear_3d.py) + * [Pollard Rho](maths/pollard_rho.py) + * [Polynomial Evaluation](maths/polynomial_evaluation.py) + * Polynomials + * [Single Indeterminate Operations](maths/polynomials/single_indeterminate_operations.py) + * [Power Using Recursion](maths/power_using_recursion.py) + * [Prime Check](maths/prime_check.py) + * [Prime Factors](maths/prime_factors.py) + * [Prime Numbers](maths/prime_numbers.py) + * [Prime Sieve Eratosthenes](maths/prime_sieve_eratosthenes.py) + * [Primelib](maths/primelib.py) + * [Print Multiplication Table](maths/print_multiplication_table.py) + * [Pythagoras](maths/pythagoras.py) + * [Qr Decomposition](maths/qr_decomposition.py) + * [Quadratic Equations Complex Numbers](maths/quadratic_equations_complex_numbers.py) + * [Radians](maths/radians.py) + * [Radix2 Fft](maths/radix2_fft.py) + * [Recursive Digit Sum](maths/recursive_digit_sum.py) + * [Remove Digit](maths/remove_digit.py) + * [Reverse Factorial Recursive](maths/reverse_factorial_recursive.py) + * [Segmented Sieve](maths/segmented_sieve.py) + * Series + * [Alternate Harmonic Series](maths/series/alternate_harmonic_series.py) + * [Alternating Harmonic Series](maths/series/alternating_harmonic_series.py) + * [Arithmetic](maths/series/arithmetic.py) + * [Geometric](maths/series/geometric.py) + * [Geometric Series](maths/series/geometric_series.py) + * [Harmonic](maths/series/harmonic.py) + * [Harmonic Series](maths/series/harmonic_series.py) + * [Hexagonal Numbers](maths/series/hexagonal_numbers.py) + * [Logarithmic Series](maths/series/logarithmic_series.py) + * [P Series](maths/series/p_series.py) + * [Shoelace Area](maths/shoelace_area.py) + * [Sieve Of Atkin](maths/sieve_of_atkin.py) + * [Sieve Of Eratosthenes](maths/sieve_of_eratosthenes.py) + * [Sigmoid](maths/sigmoid.py) + * [Signum](maths/signum.py) + * [Simultaneous Linear Equation Solver](maths/simultaneous_linear_equation_solver.py) + * [Sin](maths/sin.py) + * [Sock Merchant](maths/sock_merchant.py) + * [Softmax](maths/softmax.py) + * [Solovay Strassen Primality Test](maths/solovay_strassen_primality_test.py) + * [Spearman Rank Correlation Coefficient](maths/spearman_rank_correlation_coefficient.py) + * Special Numbers + * [Abundant Numbers](maths/special_numbers/abundant_numbers.py) + * [Armstrong Numbers](maths/special_numbers/armstrong_numbers.py) + * [Automorphic Number](maths/special_numbers/automorphic_number.py) + * [Bell Numbers](maths/special_numbers/bell_numbers.py) + * [Carmichael Number](maths/special_numbers/carmichael_number.py) + * [Catalan Number](maths/special_numbers/catalan_number.py) + * [Deficient Numbers](maths/special_numbers/deficient_numbers.py) + * [Disarum Number](maths/special_numbers/disarum_number.py) + * [Evil Number](maths/special_numbers/evil_number.py) + * [Hamming Numbers](maths/special_numbers/hamming_numbers.py) + * [Happy Number](maths/special_numbers/happy_number.py) + * [Harshad Numbers](maths/special_numbers/harshad_numbers.py) + * [Hexagonal Number](maths/special_numbers/hexagonal_number.py) + * [Jacobsthal Number](maths/special_numbers/jacobsthal_number.py) + * [Kaprekar Constant](maths/special_numbers/kaprekar_constant.py) + * [Kaprekar Number](maths/special_numbers/kaprekar_number.py) + * [Krishnamurthy Number](maths/special_numbers/krishnamurthy_number.py) + * [Perfect Number](maths/special_numbers/perfect_number.py) + * [Polygonal Numbers](maths/special_numbers/polygonal_numbers.py) + * [Pronic Number](maths/special_numbers/pronic_number.py) + * [Proth Number](maths/special_numbers/proth_number.py) + * [Spy Number](maths/special_numbers/spy_number.py) + * [Triangular Numbers](maths/special_numbers/triangular_numbers.py) + * [Trimorphic Number](maths/special_numbers/trimorphic_number.py) + * [Ugly Numbers](maths/special_numbers/ugly_numbers.py) + * [Weird Number](maths/special_numbers/weird_number.py) + * [Sum Of Arithmetic Series](maths/sum_of_arithmetic_series.py) + * [Sum Of Digits](maths/sum_of_digits.py) + * [Sum Of Geometric Progression](maths/sum_of_geometric_progression.py) + * [Sum Of Harmonic Series](maths/sum_of_harmonic_series.py) + * [Sumset](maths/sumset.py) + * [Sylvester Sequence](maths/sylvester_sequence.py) + * [Tanh](maths/tanh.py) + * [Test Abs](maths/test_abs.py) + * [Test Factorial](maths/test_factorial.py) + * [Test Prime Check](maths/test_prime_check.py) + * [Three Sum](maths/three_sum.py) + * [Tonelli Shanks](maths/tonelli_shanks.py) + * [Trailing Zeroes](maths/trailing_zeroes.py) + * [Trapezoidal Rule](maths/trapezoidal_rule.py) + * [Triplet Sum](maths/triplet_sum.py) + * [Twin Prime](maths/twin_prime.py) + * [Two Pointer](maths/two_pointer.py) + * [Two Sum](maths/two_sum.py) + * [Volume](maths/volume.py) + * [Weddles Rule](maths/weddles_rule.py) + * [Weighted Average](maths/weighted_average.py) + * [Zellers Congruence](maths/zellers_congruence.py) + +## [Matrix](matrix) + * [Binary Search Matrix](matrix/binary_search_matrix.py) + * [Count Islands In Matrix](matrix/count_islands_in_matrix.py) + * [Count Negative Numbers In Sorted Matrix](matrix/count_negative_numbers_in_sorted_matrix.py) + * [Count Paths](matrix/count_paths.py) + * [Cramers Rule 2X2](matrix/cramers_rule_2x2.py) + * [Inverse Of Matrix](matrix/inverse_of_matrix.py) + * [Kronecker Product](matrix/kronecker_product.py) + * [Largest Square Area In Matrix](matrix/largest_square_area_in_matrix.py) + * [Matrix Based Game](matrix/matrix_based_game.py) + * [Matrix Class](matrix/matrix_class.py) + * [Matrix Equalization](matrix/matrix_equalization.py) + * [Matrix Multiplication Recursion](matrix/matrix_multiplication_recursion.py) + * [Matrix Operation](matrix/matrix_operation.py) + * [Max Area Of Island](matrix/max_area_of_island.py) + * [Median Matrix](matrix/median_matrix.py) + * [Nth Fibonacci Using Matrix Exponentiation](matrix/nth_fibonacci_using_matrix_exponentiation.py) + * [Pascal Triangle](matrix/pascal_triangle.py) + * [Rotate Matrix](matrix/rotate_matrix.py) + * [Searching In Sorted Matrix](matrix/searching_in_sorted_matrix.py) + * [Sherman Morrison](matrix/sherman_morrison.py) + * [Similar Matrices](matrix/similar_matrices.py) + * [Spiral Print](matrix/spiral_print.py) + * Tests + * [Test Matrix Operation](matrix/tests/test_matrix_operation.py) + * [Transitive Closure](matrix/transitive_closure.py) + * [Validate Sudoku Board](matrix/validate_sudoku_board.py) + +## [Networking Flow](networking_flow) + * [Dinic](networking_flow/dinic.py) + * [Ford Fulkerson](networking_flow/ford_fulkerson.py) + * [Minimum Cut](networking_flow/minimum_cut.py) + * [Push Relabel](networking_flow/push_relabel.py) + +## [Neural Network](neural_network) + * Activation Functions + * [Binary Step](neural_network/activation_functions/binary_step.py) + * [Exponential Linear Unit](neural_network/activation_functions/exponential_linear_unit.py) + * [Gaussian Error Linear Unit](neural_network/activation_functions/gaussian_error_linear_unit.py) + * [Leaky Rectified Linear Unit](neural_network/activation_functions/leaky_rectified_linear_unit.py) + * [Mish](neural_network/activation_functions/mish.py) + * [Rectified Linear Unit](neural_network/activation_functions/rectified_linear_unit.py) + * [Scaled Exponential Linear Unit](neural_network/activation_functions/scaled_exponential_linear_unit.py) + * [Soboleva Modified Hyperbolic Tangent](neural_network/activation_functions/soboleva_modified_hyperbolic_tangent.py) + * [Softplus](neural_network/activation_functions/softplus.py) + * [Softsign](neural_network/activation_functions/softsign.py) + * [Squareplus](neural_network/activation_functions/squareplus.py) + * [Swish](neural_network/activation_functions/swish.py) + * [Back Propagation Neural Network](neural_network/back_propagation_neural_network.py) + * [Convolution Neural Network](neural_network/convolution_neural_network.py) + * [Lstm](neural_network/lstm.py) + * Optimizers + * [Adagrad](neural_network/optimizers/adagrad.py) + * [Adam Optimizer](neural_network/optimizers/adam_optimizer.py) + * [Momentum Sgd](neural_network/optimizers/momentum_sgd.py) + * [Muon Optimizer](neural_network/optimizers/muon_optimizer.py) + * [Nesterov Accelerated Sgd](neural_network/optimizers/nesterov_accelerated_sgd.py) + * [Perceptron](neural_network/perceptron.py) + * [Simple Neural Network](neural_network/simple_neural_network.py) + * [Sliding Window Attention](neural_network/sliding_window_attention.py) + * [Two Hidden Layers Neural Network](neural_network/two_hidden_layers_neural_network.py) + +## [Other](other) + * [Activity Selection](other/activity_selection.py) + * [Alternative List Arrange](other/alternative_list_arrange.py) + * [Bankers Algorithm](other/bankers_algorithm.py) + * [Cheap Progress](other/cheap_progress.py) + * [Dancing Links](other/dancing_links.py) + * [Davis Putnam Logemann Loveland](other/davis_putnam_logemann_loveland.py) + * [Doomsday](other/doomsday.py) + * [Fischer Yates Shuffle](other/fischer_yates_shuffle.py) + * [Gauss Easter](other/gauss_easter.py) + * [Graham Scan](other/graham_scan.py) + * [Greedy](other/greedy.py) + * [Guess The Number Search](other/guess_the_number_search.py) + * [H Index](other/h_index.py) + * [Least Recently Used](other/least_recently_used.py) + * [Lfu Cache](other/lfu_cache.py) + * [Linear Congruential Generator](other/linear_congruential_generator.py) + * [Lru Cache](other/lru_cache.py) + * [Magicdiamondpattern](other/magicdiamondpattern.py) + * [Majority Vote Algorithm](other/majority_vote_algorithm.py) + * [Maximum Subsequence](other/maximum_subsequence.py) + * [Nested Brackets](other/nested_brackets.py) + * [Number Container System](other/number_container_system.py) + * [Password](other/password.py) + * [Pipeline](other/pipeline.py) + * [Quine](other/quine.py) + * [Scoring Algorithm](other/scoring_algorithm.py) + * [Sdes](other/sdes.py) + * [Sliding Window Maximum](other/sliding_window_maximum.py) + * [Token Bucket](other/token_bucket.py) + * [Tower Of Hanoi](other/tower_of_hanoi.py) + * [Word Search](other/word_search.py) + +## [Physics](physics) + * [Altitude Pressure](physics/altitude_pressure.py) + * [Archimedes Principle Of Buoyant Force](physics/archimedes_principle_of_buoyant_force.py) + * [Basic Orbital Capture](physics/basic_orbital_capture.py) + * [Boyles Law](physics/boyles_law.py) + * [Bragg Angle](physics/bragg_angle.py) + * [Casimir Effect](physics/casimir_effect.py) + * [Center Of Mass](physics/center_of_mass.py) + * [Centripetal Force](physics/centripetal_force.py) + * [Coulombs Law](physics/coulombs_law.py) + * [Doppler Frequency](physics/doppler_frequency.py) + * [Escape Velocity](physics/escape_velocity.py) + * [Faraday Lenz Law](physics/faraday_lenz_law.py) + * [First Law Of Thermodynamics](physics/first_law_of_thermodynamics.py) + * [Fresnel Diffract](physics/fresnel_diffract.py) + * [Grahams Law](physics/grahams_law.py) + * [Hamiltonian](physics/hamiltonian.py) + * [Hookes Law](physics/hookes_law.py) + * [Horizontal Projectile Motion](physics/horizontal_projectile_motion.py) + * [Hubble Parameter](physics/hubble_parameter.py) + * [Ideal Gas Law](physics/ideal_gas_law.py) + * [In Static Equilibrium](physics/in_static_equilibrium.py) + * [Kinetic Energy](physics/kinetic_energy.py) + * [Lens Formulae](physics/lens_formulae.py) + * [Light Aberration](physics/light_aberration.py) + * [Lorentz Transformation Four Vector](physics/lorentz_transformation_four_vector.py) + * [Magnetic Flux](physics/magnetic_flux.py) + * [Malus Law](physics/malus_law.py) + * [Mass Energy Equivalence](physics/mass_energy_equivalence.py) + * [Maxwells Equations](physics/maxwells_equations.py) + * [Mirror Formulae](physics/mirror_formulae.py) + * [N Body Simulation](physics/n_body_simulation.py) + * [Newtons Law Of Gravitation](physics/newtons_law_of_gravitation.py) + * [Newtons Second Law Of Motion](physics/newtons_second_law_of_motion.py) + * [Orbital Transfer Work](physics/orbital_transfer_work.py) + * [Period Of Pendulum](physics/period_of_pendulum.py) + * [Photoelectric Effect](physics/photoelectric_effect.py) + * [Potential Energy](physics/potential_energy.py) + * [Rainfall Intensity](physics/rainfall_intensity.py) + * [Relativistic Kinetic Energy](physics/relativistic_kinetic_energy.py) + * [Relativistic Velocity Summation](physics/relativistic_velocity_summation.py) + * [Reynolds Number](physics/reynolds_number.py) + * [Rms Speed Of Molecule](physics/rms_speed_of_molecule.py) + * [Shear Stress](physics/shear_stress.py) + * [Snells Law](physics/snells_law.py) + * [Speed Of Sound](physics/speed_of_sound.py) + * [Speeds Of Gas Molecules](physics/speeds_of_gas_molecules.py) + * [Terminal Velocity](physics/terminal_velocity.py) + +## [Project Euler](project_euler) + * Problem 001 + * [Sol1](project_euler/problem_001/sol1.py) + * [Sol2](project_euler/problem_001/sol2.py) + * [Sol3](project_euler/problem_001/sol3.py) + * [Sol4](project_euler/problem_001/sol4.py) + * [Sol5](project_euler/problem_001/sol5.py) + * [Sol6](project_euler/problem_001/sol6.py) + * [Sol7](project_euler/problem_001/sol7.py) + * Problem 002 + * [Sol1](project_euler/problem_002/sol1.py) + * [Sol2](project_euler/problem_002/sol2.py) + * [Sol3](project_euler/problem_002/sol3.py) + * [Sol4](project_euler/problem_002/sol4.py) + * [Sol5](project_euler/problem_002/sol5.py) + * Problem 003 + * [Sol1](project_euler/problem_003/sol1.py) + * [Sol2](project_euler/problem_003/sol2.py) + * [Sol3](project_euler/problem_003/sol3.py) + * Problem 004 + * [Sol1](project_euler/problem_004/sol1.py) + * [Sol2](project_euler/problem_004/sol2.py) + * Problem 005 + * [Sol1](project_euler/problem_005/sol1.py) + * [Sol2](project_euler/problem_005/sol2.py) + * Problem 006 + * [Sol1](project_euler/problem_006/sol1.py) + * [Sol2](project_euler/problem_006/sol2.py) + * [Sol3](project_euler/problem_006/sol3.py) + * [Sol4](project_euler/problem_006/sol4.py) + * Problem 007 + * [Sol1](project_euler/problem_007/sol1.py) + * [Sol2](project_euler/problem_007/sol2.py) + * [Sol3](project_euler/problem_007/sol3.py) + * Problem 008 + * [Sol1](project_euler/problem_008/sol1.py) + * [Sol2](project_euler/problem_008/sol2.py) + * [Sol3](project_euler/problem_008/sol3.py) + * Problem 009 + * [Sol1](project_euler/problem_009/sol1.py) + * [Sol2](project_euler/problem_009/sol2.py) + * [Sol3](project_euler/problem_009/sol3.py) + * [Sol4](project_euler/problem_009/sol4.py) + * Problem 010 + * [Sol1](project_euler/problem_010/sol1.py) + * [Sol2](project_euler/problem_010/sol2.py) + * [Sol3](project_euler/problem_010/sol3.py) + * Problem 011 + * [Sol1](project_euler/problem_011/sol1.py) + * [Sol2](project_euler/problem_011/sol2.py) + * Problem 012 + * [Sol1](project_euler/problem_012/sol1.py) + * [Sol2](project_euler/problem_012/sol2.py) + * Problem 013 + * [Sol1](project_euler/problem_013/sol1.py) + * [Sol2](project_euler/problem_013/sol2.py) + * Problem 014 + * [Sol1](project_euler/problem_014/sol1.py) + * [Sol2](project_euler/problem_014/sol2.py) + * Problem 015 + * [Sol1](project_euler/problem_015/sol1.py) + * [Sol2](project_euler/problem_015/sol2.py) + * Problem 016 + * [Sol1](project_euler/problem_016/sol1.py) + * [Sol2](project_euler/problem_016/sol2.py) + * Problem 017 + * [Sol1](project_euler/problem_017/sol1.py) + * Problem 018 + * [Solution](project_euler/problem_018/solution.py) + * Problem 019 + * [Sol1](project_euler/problem_019/sol1.py) + * Problem 020 + * [Sol1](project_euler/problem_020/sol1.py) + * [Sol2](project_euler/problem_020/sol2.py) + * [Sol3](project_euler/problem_020/sol3.py) + * [Sol4](project_euler/problem_020/sol4.py) + * Problem 021 + * [Sol1](project_euler/problem_021/sol1.py) + * Problem 022 + * [Sol1](project_euler/problem_022/sol1.py) + * [Sol2](project_euler/problem_022/sol2.py) + * Problem 023 + * [Sol1](project_euler/problem_023/sol1.py) + * Problem 024 + * [Sol1](project_euler/problem_024/sol1.py) + * Problem 025 + * [Sol1](project_euler/problem_025/sol1.py) + * [Sol2](project_euler/problem_025/sol2.py) + * [Sol3](project_euler/problem_025/sol3.py) + * Problem 026 + * [Sol1](project_euler/problem_026/sol1.py) + * Problem 027 + * [Sol1](project_euler/problem_027/sol1.py) + * Problem 028 + * [Sol1](project_euler/problem_028/sol1.py) + * Problem 029 + * [Sol1](project_euler/problem_029/sol1.py) + * Problem 030 + * [Sol1](project_euler/problem_030/sol1.py) + * Problem 031 + * [Sol1](project_euler/problem_031/sol1.py) + * [Sol2](project_euler/problem_031/sol2.py) + * Problem 032 + * [Sol32](project_euler/problem_032/sol32.py) + * Problem 033 + * [Sol1](project_euler/problem_033/sol1.py) + * Problem 034 + * [Sol1](project_euler/problem_034/sol1.py) + * Problem 035 + * [Sol1](project_euler/problem_035/sol1.py) + * Problem 036 + * [Sol1](project_euler/problem_036/sol1.py) + * Problem 037 + * [Sol1](project_euler/problem_037/sol1.py) + * Problem 038 + * [Sol1](project_euler/problem_038/sol1.py) + * Problem 039 + * [Sol1](project_euler/problem_039/sol1.py) + * Problem 040 + * [Sol1](project_euler/problem_040/sol1.py) + * Problem 041 + * [Sol1](project_euler/problem_041/sol1.py) + * Problem 042 + * [Solution42](project_euler/problem_042/solution42.py) + * Problem 043 + * [Sol1](project_euler/problem_043/sol1.py) + * Problem 044 + * [Sol1](project_euler/problem_044/sol1.py) + * Problem 045 + * [Sol1](project_euler/problem_045/sol1.py) + * Problem 046 + * [Sol1](project_euler/problem_046/sol1.py) + * Problem 047 + * [Sol1](project_euler/problem_047/sol1.py) + * Problem 048 + * [Sol1](project_euler/problem_048/sol1.py) + * Problem 049 + * [Sol1](project_euler/problem_049/sol1.py) + * Problem 050 + * [Sol1](project_euler/problem_050/sol1.py) + * Problem 051 + * [Sol1](project_euler/problem_051/sol1.py) + * Problem 052 + * [Sol1](project_euler/problem_052/sol1.py) + * Problem 053 + * [Sol1](project_euler/problem_053/sol1.py) + * Problem 054 + * [Sol1](project_euler/problem_054/sol1.py) + * [Test Poker Hand](project_euler/problem_054/test_poker_hand.py) + * Problem 055 + * [Sol1](project_euler/problem_055/sol1.py) + * Problem 056 + * [Sol1](project_euler/problem_056/sol1.py) + * Problem 057 + * [Sol1](project_euler/problem_057/sol1.py) + * Problem 058 + * [Sol1](project_euler/problem_058/sol1.py) + * Problem 059 + * [Sol1](project_euler/problem_059/sol1.py) + * Problem 060 + * [Sol1](project_euler/problem_060/sol1.py) + * Problem 062 + * [Sol1](project_euler/problem_062/sol1.py) + * Problem 063 + * [Sol1](project_euler/problem_063/sol1.py) + * Problem 064 + * [Sol1](project_euler/problem_064/sol1.py) + * Problem 065 + * [Sol1](project_euler/problem_065/sol1.py) + * Problem 067 + * [Sol1](project_euler/problem_067/sol1.py) + * [Sol2](project_euler/problem_067/sol2.py) + * Problem 068 + * [Sol1](project_euler/problem_068/sol1.py) + * Problem 069 + * [Sol1](project_euler/problem_069/sol1.py) + * Problem 070 + * [Sol1](project_euler/problem_070/sol1.py) + * Problem 071 + * [Sol1](project_euler/problem_071/sol1.py) + * Problem 072 + * [Sol1](project_euler/problem_072/sol1.py) + * [Sol2](project_euler/problem_072/sol2.py) + * Problem 073 + * [Sol1](project_euler/problem_073/sol1.py) + * Problem 074 + * [Sol1](project_euler/problem_074/sol1.py) + * [Sol2](project_euler/problem_074/sol2.py) + * Problem 075 + * [Sol1](project_euler/problem_075/sol1.py) + * Problem 076 + * [Sol1](project_euler/problem_076/sol1.py) + * Problem 077 + * [Sol1](project_euler/problem_077/sol1.py) + * Problem 078 + * [Sol1](project_euler/problem_078/sol1.py) + * Problem 079 + * [Sol1](project_euler/problem_079/sol1.py) + * Problem 080 + * [Sol1](project_euler/problem_080/sol1.py) + * Problem 081 + * [Sol1](project_euler/problem_081/sol1.py) + * Problem 082 + * [Sol1](project_euler/problem_082/sol1.py) + * Problem 085 + * [Sol1](project_euler/problem_085/sol1.py) + * Problem 086 + * [Sol1](project_euler/problem_086/sol1.py) + * Problem 087 + * [Sol1](project_euler/problem_087/sol1.py) + * Problem 089 + * [Sol1](project_euler/problem_089/sol1.py) + * Problem 091 + * [Sol1](project_euler/problem_091/sol1.py) + * Problem 092 + * [Sol1](project_euler/problem_092/sol1.py) + * Problem 094 + * [Sol1](project_euler/problem_094/sol1.py) + * Problem 095 + * [Sol1](project_euler/problem_095/sol1.py) + * Problem 097 + * [Sol1](project_euler/problem_097/sol1.py) + * Problem 099 + * [Sol1](project_euler/problem_099/sol1.py) + * Problem 100 + * [Sol1](project_euler/problem_100/sol1.py) + * Problem 101 + * [Sol1](project_euler/problem_101/sol1.py) + * Problem 102 + * [Sol1](project_euler/problem_102/sol1.py) + * Problem 104 + * [Sol1](project_euler/problem_104/sol1.py) + * Problem 107 + * [Sol1](project_euler/problem_107/sol1.py) + * Problem 109 + * [Sol1](project_euler/problem_109/sol1.py) + * Problem 111 + * [Sol1](project_euler/problem_111/sol1.py) + * Problem 112 + * [Sol1](project_euler/problem_112/sol1.py) + * Problem 113 + * [Sol1](project_euler/problem_113/sol1.py) + * Problem 114 + * [Sol1](project_euler/problem_114/sol1.py) + * Problem 115 + * [Sol1](project_euler/problem_115/sol1.py) + * Problem 116 + * [Sol1](project_euler/problem_116/sol1.py) + * Problem 117 + * [Sol1](project_euler/problem_117/sol1.py) + * Problem 119 + * [Sol1](project_euler/problem_119/sol1.py) + * Problem 120 + * [Sol1](project_euler/problem_120/sol1.py) + * Problem 121 + * [Sol1](project_euler/problem_121/sol1.py) + * Problem 122 + * [Sol1](project_euler/problem_122/sol1.py) + * Problem 123 + * [Sol1](project_euler/problem_123/sol1.py) + * Problem 124 + * [Sol1](project_euler/problem_124/sol1.py) + * Problem 125 + * [Sol1](project_euler/problem_125/sol1.py) + * Problem 129 + * [Sol1](project_euler/problem_129/sol1.py) + * Problem 131 + * [Sol1](project_euler/problem_131/sol1.py) + * Problem 135 + * [Sol1](project_euler/problem_135/sol1.py) + * Problem 136 + * [Sol1](project_euler/problem_136/sol1.py) + * Problem 137 + * [Sol1](project_euler/problem_137/sol1.py) + * Problem 138 + * [Sol1](project_euler/problem_138/sol1.py) + * Problem 142 + * [Sol1](project_euler/problem_142/sol1.py) + * Problem 144 + * [Sol1](project_euler/problem_144/sol1.py) + * Problem 145 + * [Sol1](project_euler/problem_145/sol1.py) + * Problem 164 + * [Sol1](project_euler/problem_164/sol1.py) + * Problem 173 + * [Sol1](project_euler/problem_173/sol1.py) + * Problem 174 + * [Sol1](project_euler/problem_174/sol1.py) + * Problem 180 + * [Sol1](project_euler/problem_180/sol1.py) + * Problem 187 + * [Sol1](project_euler/problem_187/sol1.py) + * Problem 188 + * [Sol1](project_euler/problem_188/sol1.py) + * Problem 190 + * [Sol1](project_euler/problem_190/sol1.py) + * Problem 191 + * [Sol1](project_euler/problem_191/sol1.py) + * Problem 203 + * [Sol1](project_euler/problem_203/sol1.py) + * Problem 205 + * [Sol1](project_euler/problem_205/sol1.py) + * Problem 206 + * [Sol1](project_euler/problem_206/sol1.py) + * Problem 207 + * [Sol1](project_euler/problem_207/sol1.py) + * Problem 234 + * [Sol1](project_euler/problem_234/sol1.py) + * Problem 301 + * [Sol1](project_euler/problem_301/sol1.py) + * Problem 345 + * [Sol1](project_euler/problem_345/sol1.py) + * Problem 493 + * [Sol1](project_euler/problem_493/sol1.py) + * Problem 551 + * [Sol1](project_euler/problem_551/sol1.py) + * Problem 587 + * [Sol1](project_euler/problem_587/sol1.py) + * Problem 686 + * [Sol1](project_euler/problem_686/sol1.py) + * Problem 800 + * [Sol1](project_euler/problem_800/sol1.py) + +## [Quantum](quantum) + * [Shor Algorithm](quantum/shor_algorithm.py) + +## [Scheduling](scheduling) + * [Cpuschedulingalgorithms](scheduling/cpuschedulingalgorithms.py) + * [First Come First Served](scheduling/first_come_first_served.py) + * [Highest Response Ratio Next](scheduling/highest_response_ratio_next.py) + * [Job Sequence With Deadline](scheduling/job_sequence_with_deadline.py) + * [Job Sequencing With Deadline](scheduling/job_sequencing_with_deadline.py) + * [Multi Level Feedback Queue](scheduling/multi_level_feedback_queue.py) + * [Non Preemptive Shortest Job First](scheduling/non_preemptive_shortest_job_first.py) + * [Round Robin](scheduling/round_robin.py) + * [Shortest Job First](scheduling/shortest_job_first.py) + +## [Searches](searches) + * [Binary Search](searches/binary_search.py) + * [Binary Tree Traversal](searches/binary_tree_traversal.py) + * [Double Linear Search](searches/double_linear_search.py) + * [Double Linear Search Recursion](searches/double_linear_search_recursion.py) + * [Exponential Search](searches/exponential_search.py) + * [Fibonacci Search](searches/fibonacci_search.py) + * [Hill Climbing](searches/hill_climbing.py) + * [Interpolation Search](searches/interpolation_search.py) + * [Jump Search](searches/jump_search.py) + * [Linear Search](searches/linear_search.py) + * [Median Of Medians](searches/median_of_medians.py) + * [Quick Select](searches/quick_select.py) + * [Sentinel Linear Search](searches/sentinel_linear_search.py) + * [Simple Binary Search](searches/simple_binary_search.py) + * [Simulated Annealing](searches/simulated_annealing.py) + * [Tabu Search](searches/tabu_search.py) + * [Ternary Search](searches/ternary_search.py) + +## [Sorts](sorts) + * [Adaptive Merge Sort](sorts/adaptive_merge_sort.py) + * [Bead Sort](sorts/bead_sort.py) + * [Benchmark Sorts](sorts/benchmark_sorts.py) + * [Binary Insertion Sort](sorts/binary_insertion_sort.py) + * [Bitonic Sort](sorts/bitonic_sort.py) + * [Bogo Sort](sorts/bogo_sort.py) + * [Bubble Sort](sorts/bubble_sort.py) + * [Bubble Sort Recursive](sorts/bubble_sort_recursive.py) + * [Bucket Sort](sorts/bucket_sort.py) + * [Circle Sort](sorts/circle_sort.py) + * [Cocktail Shaker Sort](sorts/cocktail_shaker_sort.py) + * [Comb Sort](sorts/comb_sort.py) + * [Counting Sort](sorts/counting_sort.py) + * [Cycle Sort](sorts/cycle_sort.py) + * [Cyclic Sort](sorts/cyclic_sort.py) + * [Double Sort](sorts/double_sort.py) + * [Dutch National Flag Sort](sorts/dutch_national_flag_sort.py) + * [Exchange Sort](sorts/exchange_sort.py) + * [External Sort](sorts/external_sort.py) + * [Flash Sort](sorts/flash_sort.py) + * [Gnome Sort](sorts/gnome_sort.py) + * [Heap Sort](sorts/heap_sort.py) + * [Insertion Sort](sorts/insertion_sort.py) + * [Intro Sort](sorts/intro_sort.py) + * [Iterative Merge Sort](sorts/iterative_merge_sort.py) + * [Kirkpatrick Reisch Sort](sorts/kirkpatrick_reisch_sort.py) + * [Merge Insertion Sort](sorts/merge_insertion_sort.py) + * [Merge Sort](sorts/merge_sort.py) + * [Msd Radix Sort](sorts/msd_radix_sort.py) + * [Natural Sort](sorts/natural_sort.py) + * [Odd Even Sort](sorts/odd_even_sort.py) + * [Odd Even Transposition Parallel](sorts/odd_even_transposition_parallel.py) + * [Odd Even Transposition Single Threaded](sorts/odd_even_transposition_single_threaded.py) + * [Pancake Sort](sorts/pancake_sort.py) + * [Patience Sort](sorts/patience_sort.py) + * [Pigeon Sort](sorts/pigeon_sort.py) + * [Pigeonhole Sort](sorts/pigeonhole_sort.py) + * [Power Sort](sorts/power_sort.py) + * [Quick Sort](sorts/quick_sort.py) + * [Quick Sort 3 Partition](sorts/quick_sort_3_partition.py) + * [Radix Sort](sorts/radix_sort.py) + * [Recursive Insertion Sort](sorts/recursive_insertion_sort.py) + * [Recursive Mergesort Array](sorts/recursive_mergesort_array.py) + * [Recursive Quick Sort](sorts/recursive_quick_sort.py) + * [Reverse Selection](sorts/reverse_selection.py) + * [Reversort](sorts/reversort.py) + * [Selection Sort](sorts/selection_sort.py) + * [Shell Sort](sorts/shell_sort.py) + * [Shrink Shell Sort](sorts/shrink_shell_sort.py) + * [Slowsort](sorts/slowsort.py) + * [Smoothsort](sorts/smoothsort.py) + * [Stalin Sort](sorts/stalin_sort.py) + * [Stooge Sort](sorts/stooge_sort.py) + * [Strand Sort](sorts/strand_sort.py) + * [Tim Sort](sorts/tim_sort.py) + * [Topological Sort](sorts/topological_sort.py) + * [Tree Sort](sorts/tree_sort.py) + * [Unknown Sort](sorts/unknown_sort.py) + * [Wiggle Sort](sorts/wiggle_sort.py) + +## [Strings](strings) + * [Aho Corasick](strings/aho_corasick.py) + * [Alternative String Arrange](strings/alternative_string_arrange.py) + * [Anagrams](strings/anagrams.py) + * [Autocomplete Using Trie](strings/autocomplete_using_trie.py) + * [Barcode Validator](strings/barcode_validator.py) + * [Bitap String Match](strings/bitap_string_match.py) + * [Booths Algorithm](strings/booths_algorithm.py) + * [Boyer Moore Horspool](strings/boyer_moore_horspool.py) + * [Boyer Moore Search](strings/boyer_moore_search.py) + * [Bpe Tokenizer](strings/bpe_tokenizer.py) + * [Camel Case To Snake Case](strings/camel_case_to_snake_case.py) + * [Can String Be Rearranged As Palindrome](strings/can_string_be_rearranged_as_palindrome.py) + * [Capitalize](strings/capitalize.py) + * [Check Anagrams](strings/check_anagrams.py) + * [Count Vowels](strings/count_vowels.py) + * [Count Vowels Consonants](strings/count_vowels_consonants.py) + * [Credit Card Validator](strings/credit_card_validator.py) + * [Damerau Levenshtein Distance](strings/damerau_levenshtein_distance.py) + * [Detecting English Programmatically](strings/detecting_english_programmatically.py) + * [Dna](strings/dna.py) + * [Edit Distance](strings/edit_distance.py) + * [Frequency Finder](strings/frequency_finder.py) + * [Hamming Distance](strings/hamming_distance.py) + * [Indian Phone Validator](strings/indian_phone_validator.py) + * [Is Contains Unique Chars](strings/is_contains_unique_chars.py) + * [Is Isogram](strings/is_isogram.py) + * [Is Isomorphic](strings/is_isomorphic.py) + * [Is Pangram](strings/is_pangram.py) + * [Is Polish National Id](strings/is_polish_national_id.py) + * [Is Spain National Id](strings/is_spain_national_id.py) + * [Is Srilankan Phone Number](strings/is_srilankan_phone_number.py) + * [Is Valid Email Address](strings/is_valid_email_address.py) + * [Jaro Winkler](strings/jaro_winkler.py) + * [Join](strings/join.py) + * [Knuth Morris Pratt](strings/knuth_morris_pratt.py) + * [Largest Smallest Words](strings/largest_smallest_words.py) + * [Levenshtein Distance](strings/levenshtein_distance.py) + * [Longest Word In Sentence](strings/longest_word_in_sentence.py) + * [Lower](strings/lower.py) + * [Manacher](strings/manacher.py) + * [Min Cost String Conversion](strings/min_cost_string_conversion.py) + * [Min Window Substring](strings/min_window_substring.py) + * [Naive String Search](strings/naive_string_search.py) + * [Ngram](strings/ngram.py) + * [Palindrome](strings/palindrome.py) + * [Pig Latin](strings/pig_latin.py) + * [Prefix Function](strings/prefix_function.py) + * [Rabin Karp](strings/rabin_karp.py) + * [Remove Duplicate](strings/remove_duplicate.py) + * [Reverse Letters](strings/reverse_letters.py) + * [Reverse Words](strings/reverse_words.py) + * [Snake Case To Camel Pascal Case](strings/snake_case_to_camel_pascal_case.py) + * [Split](strings/split.py) + * [String Is Valid Number](strings/string_is_valid_number.py) + * [String Switch Case](strings/string_switch_case.py) + * [Strip](strings/strip.py) + * [Suffix Automaton](strings/suffix_automaton.py) + * [Text Justification](strings/text_justification.py) + * [Title](strings/title.py) + * [Top K Frequent Words](strings/top_k_frequent_words.py) + * [Upper](strings/upper.py) + * [Wave String](strings/wave_string.py) + * [Wildcard Pattern Matching](strings/wildcard_pattern_matching.py) + * [Word Occurrence](strings/word_occurrence.py) + * [Word Patterns](strings/word_patterns.py) + * [Z Function](strings/z_function.py) + +## [Tests](tests) + * [Test Sorts](tests/test_sorts.py) + +## [Web Programming](web_programming) + * [Co2 Emission](web_programming/co2_emission.py) + * [Covid Stats Via Xpath](web_programming/covid_stats_via_xpath.py) + * [Crawl Google Results](web_programming/crawl_google_results.py) + * [Crawl Google Scholar Citation](web_programming/crawl_google_scholar_citation.py) + * [Crypto Price](web_programming/crypto_price.py) + * [Crypto Price Tracker](web_programming/crypto_price_tracker.py) + * [Currency Converter](web_programming/currency_converter.py) + * [Current Stock Price](web_programming/current_stock_price.py) + * [Current Weather](web_programming/current_weather.py) + * [Daily Horoscope](web_programming/daily_horoscope.py) + * [Download Images From Google Query](web_programming/download_images_from_google_query.py) + * [Emails From Url](web_programming/emails_from_url.py) + * [Fetch Anime And Play](web_programming/fetch_anime_and_play.py) + * [Fetch Bbc News](web_programming/fetch_bbc_news.py) + * [Fetch Github Info](web_programming/fetch_github_info.py) + * [Fetch Jobs](web_programming/fetch_jobs.py) + * [Fetch Quotes](web_programming/fetch_quotes.py) + * [Fetch Well Rx Price](web_programming/fetch_well_rx_price.py) + * [Get Amazon Product Data](web_programming/get_amazon_product_data.py) + * [Get Imdb Top 250 Movies Csv](web_programming/get_imdb_top_250_movies_csv.py) + * [Get Ip Geolocation](web_programming/get_ip_geolocation.py) + * [Get Top Billionaires](web_programming/get_top_billionaires.py) + * [Get Top Hn Posts](web_programming/get_top_hn_posts.py) + * [Giphy](web_programming/giphy.py) + * [Instagram Crawler](web_programming/instagram_crawler.py) + * [Instagram Pic](web_programming/instagram_pic.py) + * [Instagram Video](web_programming/instagram_video.py) + * [Nasa Data](web_programming/nasa_data.py) + * [Open Google Results](web_programming/open_google_results.py) + * [Random Anime Character](web_programming/random_anime_character.py) + * [Recaptcha Verification](web_programming/recaptcha_verification.py) + * [Reddit](web_programming/reddit.py) + * [Search Books By Isbn](web_programming/search_books_by_isbn.py) + * [Slack Message](web_programming/slack_message.py) + * [Test Fetch Github Info](web_programming/test_fetch_github_info.py) + * [World Covid19 Stats](web_programming/world_covid19_stats.py) diff --git a/License b/LICENSE.md similarity index 93% rename from License rename to LICENSE.md index c84ae570c084..de631c3ef333 100644 --- a/License +++ b/LICENSE.md @@ -1,6 +1,6 @@ -MIT License +## MIT License -Copyright (c) 2016 The Algorithms +Copyright (c) 2016-2022 TheAlgorithms and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Neural_Network/neuralnetwork_bp3.py b/Neural_Network/neuralnetwork_bp3.py deleted file mode 100644 index 896411a5df2a..000000000000 --- a/Neural_Network/neuralnetwork_bp3.py +++ /dev/null @@ -1,152 +0,0 @@ -#-*- coding:utf-8 -*- -''' -Author: Stephen Lee -Date: 2017.9.21 - -BP neural network with three layers -''' - -import numpy as np -import matplotlib.pyplot as plt - -class Bpnn(): - - def __init__(self,n_layer1,n_layer2,n_layer3,rate_w=0.3,rate_t=0.3): - ''' - :param n_layer1: number of input layer - :param n_layer2: number of hiden layer - :param n_layer3: number of output layer - :param rate_w: rate of weight learning - :param rate_t: rate of threshold learning - ''' - self.num1 = n_layer1 - self.num2 = n_layer2 - self.num3 = n_layer3 - self.rate_weight = rate_w - self.rate_thre = rate_t - self.thre2 = -2*np.random.rand(self.num2)+1 - self.thre3 = -2*np.random.rand(self.num3)+1 - self.vji = np.mat(-2*np.random.rand(self.num2, self.num1)+1) - self.wkj = np.mat(-2*np.random.rand(self.num3, self.num2)+1) - - def sig(self,x): - return 1 / (1 + np.exp(-1*x)) - - def sig_plain(self,x): - return 1 / (1 + np.exp(-1*x)) - - def do_round(self,x): - return round(x, 3) - - def trian(self,patterns,data_train, data_teach, n_repeat, error_accuracy, draw_e=False): - ''' - :param patterns: the number of patterns - :param data_train: training data x; numpy.ndarray - :param data_teach: training data y; numpy.ndarray - :param n_repeat: echoes - :param error_accuracy: error accuracy - :return: None - ''' - data_train = np.asarray(data_train) - data_teach = np.asarray(data_teach) - # print('-------------------Start Training-------------------------') - # print(' - - Shape: Train_Data ',np.shape(data_train)) - # print(' - - Shape: Teach_Data ',np.shape(data_teach)) - rp = 0 - all_mse = [] - mse = 10000 - while rp < n_repeat and mse >= error_accuracy: - alle = 0 - final_out = [] - for g in range(np.shape(data_train)[0]): - net_i = data_train[g] - out1 = net_i - - net_j = out1 * self.vji.T - self.thre2 - out2=self.sig(net_j) - - net_k = out2 * self.wkj.T - self.thre3 - out3 = self.sig(net_k) - - # learning process - pd_k_all = np.multiply(np.multiply(out3,(1 - out3)),(data_teach[g]-out3)) - pd_j_all = np.multiply(pd_k_all * self.wkj,np.multiply(out2,1-out2)) - #upgrade weight - self.wkj = self.wkj + pd_k_all.T * out2 *self.rate_weight - self.vji = self.vji + pd_j_all.T * out1 * self.rate_weight - #upgrade threshold - self.thre3 = self.thre3 - pd_k_all * self.rate_thre - self.thre2 = self.thre2 - pd_j_all * self.rate_thre - #calculate sum of error - errors = np.sum(abs((data_teach[g] - out3))) - - alle = alle + errors - final_out.extend(out3.getA().tolist()) - final_out3 = [list(map(self.do_round,each)) for each in final_out] - - rp = rp + 1 - mse = alle/patterns - all_mse.append(mse) - def draw_error(): - yplot = [error_accuracy for i in range(int(n_repeat * 1.2))] - plt.plot(all_mse, '+-') - plt.plot(yplot, 'r--') - plt.xlabel('Learning Times') - plt.ylabel('All_mse') - plt.grid(True,alpha = 0.7) - plt.show() - # print('------------------Training Complished---------------------') - # print(' - - Training epoch: ', rp, ' - - Mse: %.6f'%mse) - # print(' - - Last Output: ', final_out3) - if draw_e: - draw_error() - - def predict(self,data_test): - ''' - :param data_test: data test, numpy.ndarray - :return: predict output data - ''' - data_test = np.asarray(data_test) - produce_out = [] - # print('-------------------Start Testing-------------------------') - # print(' - - Shape: Test_Data ',np.shape(data_test)) - # print(np.shape(data_test)) - for g in range(np.shape(data_test)[0]): - - net_i = data_test[g] - out1 = net_i - - net_j = out1 * self.vji.T - self.thre2 - out2 = self.sig(net_j) - - net_k = out2 * self.wkj.T - self.thre3 - out3 = self.sig(net_k) - produce_out.extend(out3.getA().tolist()) - res = [list(map(self.do_round,each)) for each in produce_out] - return np.asarray(res) - - -def main(): - #example data - data_x = [[1,2,3,4], - [5,6,7,8], - [2,2,3,4], - [7,7,8,8]] - data_y = [[1,0,0,0], - [0,1,0,0], - [0,0,1,0], - [0,0,0,1]] - - test_x = [[1,2,3,4], - [3,2,3,4]] - - #building network model - model = Bpnn(4,10,4) - #training the model - model.trian(patterns=4,data_train=data_x,data_teach=data_y, - n_repeat=100,error_accuracy=0.1,draw_e=True) - #predicting data - model.predict(test_x) - -if __name__ == '__main__': - main() diff --git a/Neural_Network/perceptron.py b/Neural_Network/perceptron.py deleted file mode 100644 index 0cd89df1688f..000000000000 --- a/Neural_Network/perceptron.py +++ /dev/null @@ -1,123 +0,0 @@ -''' - - Perceptron - w = w + N * (d(k) - y) * x(k) - - Using perceptron network for oil analysis, - with Measuring of 3 parameters that represent chemical characteristics we can classify the oil, in p1 or p2 - p1 = -1 - p2 = 1 - -''' - -import random - - -class Perceptron: - def __init__(self, sample, exit, learn_rate=0.01, epoch_number=1000, bias=-1): - self.sample = sample - self.exit = exit - self.learn_rate = learn_rate - self.epoch_number = epoch_number - self.bias = bias - self.number_sample = len(sample) - self.col_sample = len(sample[0]) - self.weight = [] - - def trannig(self): - for sample in self.sample: - sample.insert(0, self.bias) - - for i in range(self.col_sample): - self.weight.append(random.random()) - - self.weight.insert(0, self.bias) - - epoch_count = 0 - - while True: - erro = False - for i in range(self.number_sample): - u = 0 - for j in range(self.col_sample + 1): - u = u + self.weight[j] * self.sample[i][j] - y = self.sign(u) - if y != self.exit[i]: - - for j in range(self.col_sample + 1): - - self.weight[j] = self.weight[j] + self.learn_rate * (self.exit[i] - y) * self.sample[i][j] - erro = True - #print('Epoch: \n',epoch_count) - epoch_count = epoch_count + 1 - # if you want controle the epoch or just by erro - if erro == False: - print('\nEpoch:\n',epoch_count) - print('------------------------\n') - #if epoch_count > self.epoch_number or not erro: - break - - def sort(self, sample): - sample.insert(0, self.bias) - u = 0 - for i in range(self.col_sample + 1): - u = u + self.weight[i] * sample[i] - - y = self.sign(u) - - if y == -1: - print('Sample: ', sample) - print('classification: P1') - else: - print('Sample: ', sample) - print('classification: P2') - - def sign(self, u): - return 1 if u >= 0 else -1 - - -samples = [ - [-0.6508, 0.1097, 4.0009], - [-1.4492, 0.8896, 4.4005], - [2.0850, 0.6876, 12.0710], - [0.2626, 1.1476, 7.7985], - [0.6418, 1.0234, 7.0427], - [0.2569, 0.6730, 8.3265], - [1.1155, 0.6043, 7.4446], - [0.0914, 0.3399, 7.0677], - [0.0121, 0.5256, 4.6316], - [-0.0429, 0.4660, 5.4323], - [0.4340, 0.6870, 8.2287], - [0.2735, 1.0287, 7.1934], - [0.4839, 0.4851, 7.4850], - [0.4089, -0.1267, 5.5019], - [1.4391, 0.1614, 8.5843], - [-0.9115, -0.1973, 2.1962], - [0.3654, 1.0475, 7.4858], - [0.2144, 0.7515, 7.1699], - [0.2013, 1.0014, 6.5489], - [0.6483, 0.2183, 5.8991], - [-0.1147, 0.2242, 7.2435], - [-0.7970, 0.8795, 3.8762], - [-1.0625, 0.6366, 2.4707], - [0.5307, 0.1285, 5.6883], - [-1.2200, 0.7777, 1.7252], - [0.3957, 0.1076, 5.6623], - [-0.1013, 0.5989, 7.1812], - [2.4482, 0.9455, 11.2095], - [2.0149, 0.6192, 10.9263], - [0.2012, 0.2611, 5.4631] - -] - -exit = [-1, -1, -1, 1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, 1, 1, 1, 1, -1, 1, 1, 1, 1, -1, -1, 1, -1, 1] - -network = Perceptron(sample=samples, exit = exit, learn_rate=0.01, epoch_number=1000, bias=-1) - -network.trannig() - -while True: - sample = [] - for i in range(3): - sample.insert(i, float(input('value: '))) - network.sort(sample) \ No newline at end of file diff --git a/README.md b/README.md index 9589bccd9307..e9a19fa15499 100644 --- a/README.md +++ b/README.md @@ -1,167 +1,49 @@ -# The Algorithms - Python [![Build Status](https://travis-ci.org/TheAlgorithms/Python.svg)](https://travis-ci.org/TheAlgorithms/Python) - -### All algorithms implemented in Python (for education) - -These are for demonstration purposes only. There are many implementations of sorts in the Python standard library that are much better for performance reasons. - -## Sort Algorithms - - -### Bubble -![alt text][bubble-image] - -From [Wikipedia][bubble-wiki]: Bubble sort, sometimes referred to as sinking sort, is a simple sorting algorithm that repeatedly steps through the list to be sorted, compares each pair of adjacent items and swaps them if they are in the wrong order. The pass through the list is repeated until no swaps are needed, which indicates that the list is sorted. - -__Properties__ -* Worst case performance O(n^2) -* Best case performance O(n) -* Average case performance O(n^2) - -###### View the algorithm in [action][bubble-toptal] - - - -### Insertion -![alt text][insertion-image] - -From [Wikipedia][insertion-wiki]: Insertion sort is a simple sorting algorithm that builds the final sorted array (or list) one item at a time. It is much less efficient on large lists than more advanced algorithms such as quicksort, heapsort, or merge sort. - -__Properties__ -* Worst case performance O(n^2) -* Best case performance O(n) -* Average case performance O(n^2) - -###### View the algorithm in [action][insertion-toptal] - - -### Merge -![alt text][merge-image] - -From [Wikipedia][merge-wiki]: In computer science, merge sort (also commonly spelled mergesort) is an efficient, general-purpose, comparison-based sorting algorithm. Most implementations produce a stable sort, which means that the implementation preserves the input order of equal elements in the sorted output. Mergesort is a divide and conquer algorithm that was invented by John von Neumann in 1945. - -__Properties__ -* Worst case performance O(n log n) -* Best case performance O(n) -* Average case performance O(n) - - -###### View the algorithm in [action][merge-toptal] - -### Quick -![alt text][quick-image] - -From [Wikipedia][quick-wiki]: Quicksort (sometimes called partition-exchange sort) is an efficient sorting algorithm, serving as a systematic method for placing the elements of an array in order. - -__Properties__ -* Worst case performance O(n^2) -* Best case performance O(n log n) or O(n) with three-way partition -* Average case performance O(n^2) - -###### View the algorithm in [action][quick-toptal] - -### Selection -![alt text][selection-image] - -From [Wikipedia][selection-wiki]: The algorithm divides the input list into two parts: the sublist of items already sorted, which is built up from left to right at the front (left) of the list, and the sublist of items remaining to be sorted that occupy the rest of the list. Initially, the sorted sublist is empty and the unsorted sublist is the entire input list. The algorithm proceeds by finding the smallest (or largest, depending on sorting order) element in the unsorted sublist, exchanging (swapping) it with the leftmost unsorted element (putting it in sorted order), and moving the sublist boundaries one element to the right. - -__Properties__ -* Worst case performance O(n^2) -* Best case performance O(n^2) -* Average case performance O(n^2) - -###### View the algorithm in [action][selection-toptal] - -### Shell -![alt text][shell-image] - -From [Wikipedia][shell-wiki]: Shellsort is a generalization of insertion sort that allows the exchange of items that are far apart. The idea is to arrange the list of elements so that, starting anywhere, considering every nth element gives a sorted list. Such a list is said to be h-sorted. Equivalently, it can be thought of as h interleaved lists, each individually sorted. - -__Properties__ -* Worst case performance O(nlog2 2n) -* Best case performance O(n log n) -* Average case performance depends on gap sequence - -###### View the algorithm in [action][shell-toptal] - -### Time-Compexity Graphs - -Comparing the complexity of sorting algorithms (Bubble Sort, Insertion Sort, Selection Sort) - -[Complexity Graphs](https://github.com/prateekiiest/Python/blob/master/sorts/sortinggraphs.png) - ----------------------------------------------------------------------------------- - -## Search Algorithms - -### Linear -![alt text][linear-image] - -From [Wikipedia][linear-wiki]: linear search or sequential search is a method for finding a target value within a list. It sequentially checks each element of the list for the target value until a match is found or until all the elements have been searched. - Linear search runs in at worst linear time and makes at most n comparisons, where n is the length of the list. - -__Properties__ -* Worst case performance O(n) -* Best case performance O(1) -* Average case performance O(n) -* Worst case space complexity O(1) iterative - -### Binary -![alt text][binary-image] - -From [Wikipedia][binary-wiki]: Binary search, also known as half-interval search or logarithmic search, is a search algorithm that finds the position of a target value within a sorted array. It compares the target value to the middle element of the array; 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. - -__Properties__ -* Worst case performance O(log n) -* Best case performance O(1) -* Average case performance O(log n) -* Worst case space complexity O(1) - ----------------------------------------------------------------------------------------------------------------------- - -## Ciphers - -### Caesar -![alt text][caesar]
-In cryptography, a **Caesar cipher**, also known as Caesar's cipher, the shift cipher, Caesar's code or Caesar shift, is one of the simplest and most widely known encryption techniques.
-It is **a type of substitution cipher** in which each letter in the plaintext is replaced by a letter some fixed number of positions down the alphabet. For example, with a left shift of 3, D would be replaced by A, E would become B, and so on.
-The method is named after **Julius Caesar**, who used it in his private correspondence.
-The encryption step performed by a Caesar cipher is often incorporated as part of more complex schemes, such as the Vigenère cipher, and still has modern application in the ROT13 system. As with all single-alphabet substitution ciphers, the Caesar cipher is easily broken and in modern practice offers essentially no communication security. -###### Source: [Wikipedia](https://en.wikipedia.org/wiki/Caesar_cipher) - -### Transposition -In cryptography, a **transposition cipher** is a method of encryption by which the positions held by units of plaintext (which are commonly characters or groups of characters) are shifted according to a regular system, so that the ciphertext constitutes a permutation of the plaintext. That is, the order of the units is changed (the plaintext is reordered).
-Mathematically a bijective function is used on the characters' positions to encrypt and an inverse function to decrypt. -###### Source: [Wikipedia](https://en.wikipedia.org/wiki/Transposition_cipher) - -[bubble-toptal]: https://www.toptal.com/developers/sorting-algorithms/bubble-sort -[bubble-wiki]: https://en.wikipedia.org/wiki/Bubble_sort -[bubble-image]: https://upload.wikimedia.org/wikipedia/commons/thumb/8/83/Bubblesort-edited-color.svg/220px-Bubblesort-edited-color.svg.png "Bubble Sort" - -[insertion-toptal]: https://www.toptal.com/developers/sorting-algorithms/insertion-sort -[insertion-wiki]: https://en.wikipedia.org/wiki/Insertion_sort -[insertion-image]: https://upload.wikimedia.org/wikipedia/commons/7/7e/Insertionsort-edited.png "Insertion Sort" - -[quick-toptal]: https://www.toptal.com/developers/sorting-algorithms/quick-sort -[quick-wiki]: https://en.wikipedia.org/wiki/Quicksort -[quick-image]: https://upload.wikimedia.org/wikipedia/commons/6/6a/Sorting_quicksort_anim.gif "Quick Sort" - -[merge-toptal]: https://www.toptal.com/developers/sorting-algorithms/merge-sort -[merge-wiki]: https://en.wikipedia.org/wiki/Merge_sort -[merge-image]: https://upload.wikimedia.org/wikipedia/commons/c/cc/Merge-sort-example-300px.gif "Merge Sort" - -[selection-toptal]: https://www.toptal.com/developers/sorting-algorithms/selection-sort -[selection-wiki]: https://en.wikipedia.org/wiki/Selection_sort -[selection-image]: https://upload.wikimedia.org/wikipedia/commons/thumb/b/b0/Selection_sort_animation.gif/250px-Selection_sort_animation.gif "Selection Sort Sort" - -[shell-toptal]: https://www.toptal.com/developers/sorting-algorithms/shell-sort -[shell-wiki]: https://en.wikipedia.org/wiki/Shellsort -[shell-image]: https://upload.wikimedia.org/wikipedia/commons/d/d8/Sorting_shellsort_anim.gif "Shell Sort" - -[linear-wiki]: https://en.wikipedia.org/wiki/Linear_search -[linear-image]: http://www.tutorialspoint.com/data_structures_algorithms/images/linear_search.gif - -[binary-wiki]: https://en.wikipedia.org/wiki/Binary_search_algorithm -[binary-image]: https://upload.wikimedia.org/wikipedia/commons/f/f7/Binary_search_into_array.png - - -[caesar]: https://upload.wikimedia.org/wikipedia/commons/4/4a/Caesar_cipher_left_shift_of_3.svg +
+ + + + +

The Algorithms - Python

+ + + + + Gitpod Ready-to-Code + + + Contributions Welcome + + + + Discord chat + + + +
+ + GitHub Workflow Status + + + pre-commit + + + code style: black + + + +

All algorithms implemented in Python - for education 📚

+
+ +Implementations are for learning purposes only. They may be less efficient than the implementations in the Python standard library. Use them at your discretion. + +## 🚀 Getting Started + +📋 Read through our [Contribution Guidelines](CONTRIBUTING.md) before you contribute. + +## � Community Channels + +We are on [Discord](https://the-algorithms.com/discord)! Community channels are a great way for you to ask questions and get help. Please join us! + +## 📜 List of Algorithms + +See our [directory](DIRECTORY.md) for easier navigation and a better overview of the project. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000000..f4ad026e4c71 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| latest | :white_check_mark: | + +## Reporting a Vulnerability + +If you discover a security vulnerability in this project, please report it responsibly. + +**Please do NOT open a public issue for security vulnerabilities.** + +Instead, please send an email to the project maintainers or use GitHub's private vulnerability reporting feature. + +### What to include + +- A description of the vulnerability +- Steps to reproduce the issue +- Possible impact of the vulnerability +- Any suggested fixes (if applicable) + +### Response Timeline + +- **Acknowledgment**: Within 48 hours +- **Initial assessment**: Within 1 week +- **Fix or mitigation**: Depends on severity, typically within 2-4 weeks + +### After Reporting + +- The security team will validate the vulnerability +- We will work on a fix and coordinate disclosure +- Credit will be given to the reporter (unless anonymity is requested) + +## Security Best Practices for Contributors + +- Keep dependencies up to date +- Avoid committing sensitive information (API keys, passwords, tokens) +- Follow secure coding practices +- Review code changes for potential security implications diff --git a/audio_filters/README.md b/audio_filters/README.md new file mode 100644 index 000000000000..a28f2d6a9949 --- /dev/null +++ b/audio_filters/README.md @@ -0,0 +1,58 @@ +# Audio Filters + +Audio filters work on the frequency of an audio signal to attenuate unwanted +frequencies and amplify wanted ones. They are used within anything related to +sound, whether it is radio communication or a hi-fi system. If you have ever +turned up the bass or cut the treble on a stereo, tuned a radio to a station, or +removed the background hum from a recording, you have used an audio filter. + +Curious to learn more? These are great starting points: + +* +* +* +* +* + +## What's in this directory + +| File | Description | +| ---- | ----------- | +| [`iir_filter.py`](iir_filter.py) | A generic N-order [Infinite Impulse Response (IIR)](https://en.wikipedia.org/wiki/Infinite_impulse_response) filter. This is the engine every filter below runs on: give it a set of coefficients and it processes a stream of samples one at a time. | +| [`butterworth_filter.py`](butterworth_filter.py) | A collection of second-order [Butterworth](https://en.wikipedia.org/wiki/Butterworth_filter) / biquad filter designs from the RBJ Audio EQ Cookbook. Each function returns a ready-to-use `IIRFilter`. | +| [`equal_loudness_filter.py`](equal_loudness_filter.py) | An [equal-loudness](https://en.wikipedia.org/wiki/Equal-loudness_contour) filter that compensates for the human ear's non-linear response to sound by cascading a Yule-Walker filter and a Butterworth high-pass filter. Includes a dependency-free `yulewalk` implementation. | +| [`show_response.py`](show_response.py) | Helpers to plot the [magnitude and phase response](https://en.wikipedia.org/wiki/Frequency_response) of any filter so you can *see* what it does. | +| [`loudness_curve.json`](loudness_curve.json) | The Robinson-Dadson equal-loudness contour data used by the equal-loudness filter. | + +## Filter designs in `butterworth_filter.py` + +| Function | Effect | +| -------- | ------ | +| `make_lowpass` | Passes frequencies below the cutoff, attenuates those above it. | +| `make_highpass` | Passes frequencies above the cutoff, attenuates those below it. | +| `make_bandpass` | Passes a band of frequencies around the center (constant skirt gain). | +| `make_bandpass_peak` | Passes a band of frequencies around the center (constant 0 dB peak gain). | +| `make_notch` | Rejects a narrow band around the center — great for removing mains hum. | +| `make_allpass` | Passes all frequencies but changes their phase relationship. | +| `make_peak` | Boosts or cuts a band around the center by a given gain (parametric EQ). | +| `make_lowshelf` | Boosts or cuts everything below the cutoff. | +| `make_highshelf` | Boosts or cuts everything above the cutoff. | + +## Try it out + +```python +from audio_filters.butterworth_filter import make_lowpass +from audio_filters.show_response import show_frequency_response + +# A 5 kHz low-pass filter for CD-quality audio (44.1 kHz sample rate) +filt = make_lowpass(5000, 44100) + +# Process samples one at a time... +filtered = [filt.process(sample) for sample in my_audio_samples] + +# ...or visualise what the filter does to the spectrum: +show_frequency_response(make_lowpass(5000, 44100), 44100) +``` + +Every module has runnable doctests — read them for concrete, copy-pasteable +examples of each filter in action. diff --git a/data_structures/Queue/__init__.py b/audio_filters/__init__.py similarity index 100% rename from data_structures/Queue/__init__.py rename to audio_filters/__init__.py diff --git a/audio_filters/butterworth_filter.py b/audio_filters/butterworth_filter.py new file mode 100644 index 000000000000..1830b74e6b89 --- /dev/null +++ b/audio_filters/butterworth_filter.py @@ -0,0 +1,361 @@ +from math import cos, sin, sqrt, tau + +from audio_filters.iir_filter import IIRFilter + +""" +Create 2nd-order IIR filters with Butterworth design. + +Code based on https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html +Alternatively you can use scipy.signal.butter, which should yield the same results. + +https://en.wikipedia.org/wiki/Butterworth_filter + +Notation used throughout this module (from the RBJ Audio EQ Cookbook): + w0 -- normalised angular frequency, ``2 * pi * frequency / samplerate`` + alpha -- bandwidth parameter, ``sin(w0) / (2 * q_factor)`` + b0..b2 -- feed-forward (numerator) coefficients of the biquad + a0..a2 -- feed-back (denominator) coefficients of the biquad +The a/b coefficient names match ``IIRFilter.set_coefficients`` and the standard +biquad transfer function, so they are kept consistent across every filter here. +""" + + +def _validate_frequency(frequency: int, samplerate: int, q_factor: float) -> None: + """ + Validate arguments shared by the Butterworth filter factories. + + >>> _validate_frequency(1000, 48000, 1 / sqrt(2)) + >>> _validate_frequency(0, 48000, 1 / sqrt(2)) + Traceback (most recent call last): + ... + ValueError: frequency must be a positive integer + >>> _validate_frequency(24000, 48000, 1 / sqrt(2)) + Traceback (most recent call last): + ... + ValueError: frequency must be less than half the samplerate + >>> _validate_frequency(1000, 0, 1 / sqrt(2)) + Traceback (most recent call last): + ... + ValueError: samplerate must be a positive integer + >>> _validate_frequency(1000, 48000, 0) + Traceback (most recent call last): + ... + ValueError: q_factor must be positive + """ + if not isinstance(frequency, int) or frequency <= 0: + raise ValueError("frequency must be a positive integer") + if not isinstance(samplerate, int) or samplerate <= 0: + raise ValueError("samplerate must be a positive integer") + if frequency >= samplerate / 2: + raise ValueError("frequency must be less than half the samplerate") + if q_factor <= 0: + raise ValueError("q_factor must be positive") + + +def make_lowpass( + frequency: int, + samplerate: int, + q_factor: float = 1 / sqrt(2), +) -> IIRFilter: + """ + Creates a low-pass filter + + >>> filter = make_lowpass(1000, 48000) + >>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE + [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.004277569313094809, + 0.008555138626189618, 0.004277569313094809] + """ + _validate_frequency(frequency, samplerate, q_factor) + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + + b0 = (1 - _cos) / 2 + b1 = 1 - _cos + + a0 = 1 + alpha + a1 = -2 * _cos + a2 = 1 - alpha + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b0]) + return filt + + +def make_highpass( + frequency: int, + samplerate: int, + q_factor: float = 1 / sqrt(2), +) -> IIRFilter: + """ + Creates a high-pass filter + + >>> filter = make_highpass(1000, 48000) + >>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE + [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.9957224306869052, + -1.9914448613738105, 0.9957224306869052] + """ + _validate_frequency(frequency, samplerate, q_factor) + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + + b0 = (1 + _cos) / 2 + b1 = -1 - _cos + + a0 = 1 + alpha + a1 = -2 * _cos + a2 = 1 - alpha + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b0]) + return filt + + +def make_bandpass( + frequency: int, + samplerate: int, + q_factor: float = 1 / sqrt(2), +) -> IIRFilter: + """ + Creates a band-pass filter + + >>> filter = make_bandpass(1000, 48000) + >>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE + [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.06526309611002579, + 0, -0.06526309611002579] + """ + _validate_frequency(frequency, samplerate, q_factor) + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + + b0 = _sin / 2 + b1 = 0 + b2 = -b0 + + a0 = 1 + alpha + a1 = -2 * _cos + a2 = 1 - alpha + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) + return filt + + +def make_allpass( + frequency: int, + samplerate: int, + q_factor: float = 1 / sqrt(2), +) -> IIRFilter: + """ + Creates an all-pass filter + + >>> filter = make_allpass(1000, 48000) + >>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE + [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.9077040443587427, + -1.9828897227476208, 1.0922959556412573] + """ + _validate_frequency(frequency, samplerate, q_factor) + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + + b0 = 1 - alpha + b1 = -2 * _cos + b2 = 1 + alpha + + filt = IIRFilter(2) + filt.set_coefficients([b2, b1, b0], [b0, b1, b2]) + return filt + + +def make_peak( + frequency: int, + samplerate: int, + gain_db: float, + q_factor: float = 1 / sqrt(2), +) -> IIRFilter: + """ + Creates a peak filter + + >>> filter = make_peak(1000, 48000, 6) + >>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE + [1.0653405327119334, -1.9828897227476208, 0.9346594672880666, 1.1303715025601122, + -1.9828897227476208, 0.8696284974398878] + """ + _validate_frequency(frequency, samplerate, q_factor) + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + big_a = 10 ** (gain_db / 40) + + b0 = 1 + alpha * big_a + b1 = -2 * _cos + b2 = 1 - alpha * big_a + a0 = 1 + alpha / big_a + a1 = -2 * _cos + a2 = 1 - alpha / big_a + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) + return filt + + +def make_lowshelf( + frequency: int, + samplerate: int, + gain_db: float, + q_factor: float = 1 / sqrt(2), +) -> IIRFilter: + """ + Creates a low-shelf filter + + >>> filter = make_lowshelf(1000, 48000, 6) + >>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE + [3.0409336710888786, -5.608870992220748, 2.602157875636628, 3.139954022810743, + -5.591841778072785, 2.5201667380627257] + """ + _validate_frequency(frequency, samplerate, q_factor) + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + big_a = 10 ** (gain_db / 40) + pmc = (big_a + 1) - (big_a - 1) * _cos + ppmc = (big_a + 1) + (big_a - 1) * _cos + mpc = (big_a - 1) - (big_a + 1) * _cos + pmpc = (big_a - 1) + (big_a + 1) * _cos + aa2 = 2 * sqrt(big_a) * alpha + + b0 = big_a * (pmc + aa2) + b1 = 2 * big_a * mpc + b2 = big_a * (pmc - aa2) + a0 = ppmc + aa2 + a1 = -2 * pmpc + a2 = ppmc - aa2 + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) + return filt + + +def make_highshelf( + frequency: int, + samplerate: int, + gain_db: float, + q_factor: float = 1 / sqrt(2), +) -> IIRFilter: + """ + Creates a high-shelf filter + + >>> filter = make_highshelf(1000, 48000, 6) + >>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE + [2.2229172136088806, -3.9587208137297303, 1.7841414181566304, 4.295432981120543, + -7.922740859457287, 3.6756456963725253] + """ + _validate_frequency(frequency, samplerate, q_factor) + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + big_a = 10 ** (gain_db / 40) + pmc = (big_a + 1) - (big_a - 1) * _cos + ppmc = (big_a + 1) + (big_a - 1) * _cos + mpc = (big_a - 1) - (big_a + 1) * _cos + pmpc = (big_a - 1) + (big_a + 1) * _cos + aa2 = 2 * sqrt(big_a) * alpha + + b0 = big_a * (ppmc + aa2) + b1 = -2 * big_a * pmpc + b2 = big_a * (ppmc - aa2) + a0 = pmc + aa2 + a1 = 2 * mpc + a2 = pmc - aa2 + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) + return filt + + +def make_notch( + frequency: int, + samplerate: int, + q_factor: float = 1 / sqrt(2), +) -> IIRFilter: + """ + Creates a notch (band-reject) filter that strongly attenuates a narrow band + of frequencies around ``frequency`` while leaving the rest of the spectrum + unchanged. It is the complement of the band-pass filter and is commonly used + to remove a single tone such as 50/60 Hz mains hum. + + https://en.wikipedia.org/wiki/Band-stop_filter + + >>> filter = make_notch(1000, 48000) + >>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE + [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 1.0, + -1.9828897227476208, 1.0] + """ + w0 = tau * frequency / samplerate # centre frequency, in radians/sample + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) # controls how narrow the rejected band is + + # Feed-forward: a pair of zeros placed exactly on the notch frequency, so + # that frequency is fully cancelled while the rest of the spectrum passes. + b0 = 1.0 + b1 = -2 * _cos + b2 = 1.0 + + # Feed-back: matching poles just inside the unit circle keep the notch + # narrow and the surrounding gain flat. + a0 = 1 + alpha + a1 = -2 * _cos + a2 = 1 - alpha + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) + return filt + + +def make_bandpass_peak( + frequency: int, + samplerate: int, + q_factor: float = 1 / sqrt(2), +) -> IIRFilter: + """ + Creates a band-pass filter with constant 0 dB peak gain. + + Unlike :func:`make_bandpass`, whose skirt (edge) gain is held constant so the + peak gain grows with ``q_factor``, this variant normalises the response so + the peak always reaches 0 dB regardless of the chosen ``q_factor``. Both + forms come from the RBJ Audio EQ Cookbook. + + https://en.wikipedia.org/wiki/Band-pass_filter + + >>> filter = make_bandpass_peak(1000, 48000) + >>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE + [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, + 0.09229595564125725, 0, -0.09229595564125725] + """ + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + + b0 = alpha + b1 = 0 + b2 = -alpha + + a0 = 1 + alpha + a1 = -2 * _cos + a2 = 1 - alpha + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) + return filt diff --git a/audio_filters/equal_loudness_filter.py b/audio_filters/equal_loudness_filter.py new file mode 100644 index 000000000000..387772aa1f50 --- /dev/null +++ b/audio_filters/equal_loudness_filter.py @@ -0,0 +1,198 @@ +from __future__ import annotations + +from json import loads +from pathlib import Path + +import numpy as np +from scipy.linalg import toeplitz +from scipy.signal import lfilter, unit_impulse + +from audio_filters.butterworth_filter import make_highpass +from audio_filters.iir_filter import IIRFilter + +data = loads((Path(__file__).resolve().parent / "loudness_curve.json").read_text()) + + +def _polystab(poly: np.ndarray) -> np.ndarray: + """ + Stabilize a polynomial by reflecting any roots that lie outside the unit + circle back inside it. This keeps the resulting IIR filter stable without + changing its magnitude response. + + https://en.wikipedia.org/wiki/Minimum_phase + + >>> np.round(_polystab(np.array([1.0, 2.0, 1.0])), 6) + array([1., 2., 1.]) + >>> np.round(_polystab(np.array([1.0, 2.0, 1.01])), 6) + array([1. , 1.980198, 0.990099]) + """ + if poly.size <= 1: + return poly + roots = np.roots(poly) + nonzero = np.where(roots != 0)[0] + outside = 0.5 * (np.sign(np.abs(roots[nonzero]) - 1) + 1) + roots[nonzero] = (1 - outside) * roots[nonzero] + outside / np.conj(roots[nonzero]) + stabilized = np.poly(roots) + if not np.imag(poly).any(): + stabilized = np.real(stabilized) + return stabilized + + +def _numerator( + impulse_response: np.ndarray, denominator: np.ndarray, numerator_order: int +) -> np.ndarray: + """ + Least-squares estimate of the numerator polynomial of a transfer function + given its impulse response and (already known) denominator polynomial. + + >>> num = _numerator(np.array([1.0, 0.0, 0.0]), np.array([1.0, 0.0, 0.0]), 1) + >>> np.round(num, 6) + array([1., 0.]) + """ + length = impulse_response.size + impulse = lfilter([1.0], denominator.ravel(), unit_impulse(length)) + toep = toeplitz(impulse, unit_impulse(numerator_order + 1)) + return np.linalg.lstsq(toep.conj(), impulse_response.ravel().conj(), rcond=None)[ + 0 + ].conj() + + +def yulewalk( + order: int, frequencies: np.ndarray, magnitudes: np.ndarray, npt: int = 512 +) -> tuple[np.ndarray, np.ndarray]: + """ + Design a recursive (IIR) digital filter that approximates an arbitrary + frequency response using the modified Yule-Walker method. This is a + dependency-free re-implementation of MATLAB/Octave's ``yulewalk`` so that + the equal-loudness filter below no longer relies on a third-party package. + + https://en.wikipedia.org/wiki/Autoregressive_model#Yule%E2%80%93Walker_equations + + :param order: order of the filter to design + :param frequencies: sample points on ``[0, 1]`` where 1 is the Nyquist + frequency, in increasing order and starting at 0 + :param magnitudes: desired (linear) magnitude at each point in ``frequencies`` + :param npt: number of points used to estimate the frequency response + :return: ``(a_coeffs, b_coeffs)``, the denominator and numerator polynomials + + >>> a, b = yulewalk(4, np.array([0.0, 0.5, 1.0]), np.array([1.0, 0.5, 0.0])) + >>> len(a), len(b) + (5, 5) + >>> bool(np.all(np.abs(np.roots(a)) < 1)) # the designed filter is stable + True + + Mismatched inputs and non-increasing frequencies are rejected: + + >>> yulewalk(4, np.array([0.0, 1.0]), np.array([1.0])) + Traceback (most recent call last): + ... + ValueError: frequencies and magnitudes must have the same length + >>> yulewalk(4, np.array([0.0, 1.0, 0.5]), np.array([1.0, 0.5, 0.0])) + Traceback (most recent call last): + ... + ValueError: frequencies must be in increasing order + """ + frequencies = np.asarray(frequencies, dtype=float).ravel() + magnitudes = np.asarray(magnitudes, dtype=float).ravel() + if frequencies.size != magnitudes.size: + msg = "frequencies and magnitudes must have the same length" + raise ValueError(msg) + if np.any(np.diff(frequencies) < 0): + msg = "frequencies must be in increasing order" + raise ValueError(msg) + + npt = npt + 1 + # Linearly interpolate the target response onto a dense grid, then mirror it + # to build the full (symmetric) magnitude spectrum. + response = np.interp(np.linspace(0, 1, npt), frequencies, magnitudes) + response = np.concatenate([response, response[-2:0:-1]]) + + total = response.size + half = (total + 1) // 2 + window_len = 4 * order + index = np.arange(window_len) + + # Autocorrelation from the power spectrum, tapered with a Hamming window. + correlation = np.real(np.fft.ifft(response * response)) + correlation = correlation[:window_len] * ( + 0.54 + 0.46 * np.cos(np.pi * index / (window_len - 1)) + ) + cepstral_window = np.concatenate([[0.5], np.ones(half - 1), np.zeros(total - half)]) + + # Solve the Yule-Walker normal equations for the denominator coefficients. + rmat = toeplitz(correlation[order : window_len - 1], correlation[order:0:-1]) + rhs = -correlation[order + 1 : window_len] + denominator = np.concatenate([[1.0], np.linalg.lstsq(rmat, rhs, rcond=None)[0]]) + denominator = _polystab(denominator) + + half_correlation = correlation.copy() + half_correlation[0] = correlation[0] / 2 + numerator = _numerator(half_correlation, denominator, order) + + padded_num = np.zeros(total) + padded_num[: numerator.size] = numerator + padded_den = np.zeros(total) + padded_den[: denominator.size] = denominator + + spectrum = 2 * np.real(np.fft.fft(padded_num) / np.fft.fft(padded_den)) + complex_log = np.log(np.abs(spectrum)) + 1j * np.angle(spectrum) + cepstrum = np.fft.ifft( + np.exp(np.fft.fft(cepstral_window * np.fft.ifft(complex_log))) + ) + numerator = np.real(_numerator(cepstrum[:window_len], denominator, order)) + return denominator, numerator + + +class EqualLoudnessFilter: + r""" + An equal-loudness filter which compensates for the human ear's non-linear + response to sound. This filter corrects this by cascading a Yule-Walker + filter and a Butterworth filter. + + Designed for use with samplerate of 44.1kHz and above. If you're using a + lower samplerate, use with caution. + + Code based on the matlab implementation at https://bit.ly/3eqh2HU + (url shortened for ruff) + + Target curve: https://i.imgur.com/3g2VfaM.png + Yulewalk response: https://i.imgur.com/J9LnJ4C.png + Butterworth and overall response: https://i.imgur.com/3g2VfaM.png + + Images and original matlab implementation by David Robinson, 2001 + + https://en.wikipedia.org/wiki/Equal-loudness_contour + + >>> filt = EqualLoudnessFilter() + >>> isinstance(filt.yulewalk_filter, IIRFilter) + True + """ + + def __init__(self, samplerate: int = 44100) -> None: + self.yulewalk_filter = IIRFilter(10) + self.butterworth_filter = make_highpass(150, samplerate) + + # pad the data to nyquist + curve_freqs = np.array(data["frequencies"] + [max(20000.0, samplerate / 2)]) + curve_gains = np.array(data["gains"] + [140]) + + # Convert to angular frequency + freqs_normalized = curve_freqs / samplerate * 2 + # Invert the curve and normalize to 0dB + gains_normalized = np.power(10, (np.min(curve_gains) - curve_gains) / 20) + + # Compute the coefficients using a least-squares fit to the curve with + # the built-in ``yulewalk`` implementation above (no third-party deps). + ya, yb = yulewalk(10, freqs_normalized, gains_normalized) + self.yulewalk_filter.set_coefficients(ya.tolist(), yb.tolist()) + + def process(self, sample: float) -> float: + """ + Process a single sample through both filters + + >>> filt = EqualLoudnessFilter() + >>> filt.process(0.0) + 0.0 + """ + tmp = self.yulewalk_filter.process(sample) + return self.butterworth_filter.process(tmp) diff --git a/audio_filters/iir_filter.py b/audio_filters/iir_filter.py new file mode 100644 index 000000000000..2144e6bd4c7a --- /dev/null +++ b/audio_filters/iir_filter.py @@ -0,0 +1,129 @@ +from __future__ import annotations + + +class IIRFilter: + r""" + N-Order IIR filter + Assumes working with float samples normalized on [-1, 1] + + --- + + Implementation details: + Based on the 2nd-order function from + https://en.wikipedia.org/wiki/Digital_biquad_filter, + this generalized N-order function was made. + + Using the following transfer function + .. math:: H(z)=\frac{b_{0}+b_{1}z^{-1}+b_{2}z^{-2}+...+b_{k}z^{-k}} + {a_{0}+a_{1}z^{-1}+a_{2}z^{-2}+...+a_{k}z^{-k}} + + we can rewrite this to + .. math:: y[n]={\frac{1}{a_{0}}} + \left(\left(b_{0}x[n]+b_{1}x[n-1]+b_{2}x[n-2]+...+b_{k}x[n-k]\right)- + \left(a_{1}y[n-1]+a_{2}y[n-2]+...+a_{k}y[n-k]\right)\right) + """ + + def __init__(self, order: int) -> None: + self.order = order + + # a_{0} ... a_{k} + self.a_coeffs = [1.0] + [0.0] * order + # b_{0} ... b_{k} + self.b_coeffs = [1.0] + [0.0] * order + + # x[n-1] ... x[n-k] + self.input_history = [0.0] * self.order + # y[n-1] ... y[n-k] + self.output_history = [0.0] * self.order + + def set_coefficients(self, a_coeffs: list[float], b_coeffs: list[float]) -> None: + """ + Set the coefficients for the IIR filter. + These should both be of size `order` + 1. + :math:`a_0` may be left out, and it will use 1.0 as default value. + + This method works well with scipy's filter design functions + + >>> # Make a 2nd-order 1000Hz butterworth lowpass filter + >>> import scipy.signal + >>> b_coeffs, a_coeffs = scipy.signal.butter(2, 1000, + ... btype='lowpass', + ... fs=48000) + >>> filt = IIRFilter(2) + >>> filt.set_coefficients(a_coeffs, b_coeffs) + + The leading :math:`a_0` coefficient may be omitted and defaults to 1.0: + + >>> filt = IIRFilter(2) + >>> filt.set_coefficients([-1.9, 0.9], [1.0, -2.0, 1.0]) + >>> filt.a_coeffs + [1.0, -1.9, 0.9] + + Passing the wrong number of coefficients raises a ``ValueError``: + + >>> IIRFilter(2).set_coefficients([1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0]) + Traceback (most recent call last): + ... + ValueError: Expected a_coeffs to have 3 elements for 2-order filter, got 4 + >>> IIRFilter(2).set_coefficients([1.0, 2.0, 3.0], [1.0, 2.0]) + Traceback (most recent call last): + ... + ValueError: Expected b_coeffs to have 3 elements for 2-order filter, got 2 + + A genuinely too-short ``a_coeffs`` is reported with its real length + rather than after the optional ``a_0`` has been filled in: + + >>> IIRFilter(2).set_coefficients([1.0], [1.0, 2.0, 3.0]) + Traceback (most recent call last): + ... + ValueError: Expected a_coeffs to have 3 elements for 2-order filter, got 1 + """ + if len(a_coeffs) == self.order: + # The leading a_0 coefficient is optional; default it to 1.0. + # Only a single missing coefficient is filled in this way, so that + # genuinely too-short inputs are reported with their real length. + a_coeffs = [1.0, *a_coeffs] + + if len(a_coeffs) != self.order + 1: + msg = ( + f"Expected a_coeffs to have {self.order + 1} elements " + f"for {self.order}-order filter, got {len(a_coeffs)}" + ) + raise ValueError(msg) + + if len(b_coeffs) != self.order + 1: + msg = ( + f"Expected b_coeffs to have {self.order + 1} elements " + f"for {self.order}-order filter, got {len(b_coeffs)}" + ) + raise ValueError(msg) + + self.a_coeffs = a_coeffs + self.b_coeffs = b_coeffs + + def process(self, sample: float) -> float: + """ + Calculate :math:`y[n]` + + >>> filt = IIRFilter(2) + >>> filt.process(0) + 0.0 + """ + result = 0.0 + + # Start at index 1 and do index 0 at the end. + for i in range(1, self.order + 1): + result += ( + self.b_coeffs[i] * self.input_history[i - 1] + - self.a_coeffs[i] * self.output_history[i - 1] + ) + + result = (result + self.b_coeffs[0] * sample) / self.a_coeffs[0] + + self.input_history[1:] = self.input_history[:-1] + self.output_history[1:] = self.output_history[:-1] + + self.input_history[0] = sample + self.output_history[0] = result + + return result diff --git a/audio_filters/loudness_curve.json b/audio_filters/loudness_curve.json new file mode 100644 index 000000000000..fc066a0810fc --- /dev/null +++ b/audio_filters/loudness_curve.json @@ -0,0 +1,76 @@ +{ + "_comment": "The following is a representative average of the Equal Loudness Contours as measured by Robinson and Dadson, 1956", + "_doi": "10.1088/0508-3443/7/5/302", + "frequencies": [ + 0, + 20, + 30, + 40, + 50, + 60, + 70, + 80, + 90, + 100, + 200, + 300, + 400, + 500, + 600, + 700, + 800, + 900, + 1000, + 1500, + 2000, + 2500, + 3000, + 3700, + 4000, + 5000, + 6000, + 7000, + 8000, + 9000, + 10000, + 12000, + 15000, + 20000 + ], + "gains": [ + 120, + 113, + 103, + 97, + 93, + 91, + 89, + 87, + 86, + 85, + 78, + 76, + 76, + 76, + 76, + 77, + 78, + 79.5, + 80, + 79, + 77, + 74, + 71.5, + 70, + 70.5, + 74, + 79, + 84, + 86, + 86, + 85, + 95, + 110, + 125 + ] +} diff --git a/audio_filters/show_response.py b/audio_filters/show_response.py new file mode 100644 index 000000000000..5830db0a2a38 --- /dev/null +++ b/audio_filters/show_response.py @@ -0,0 +1,101 @@ +""" +Plot the magnitude and phase response of an audio filter. + +https://en.wikipedia.org/wiki/Frequency_response +""" + +from __future__ import annotations + +from abc import abstractmethod +from math import pi +from typing import Protocol + +import matplotlib.pyplot as plt +import numpy as np + + +class FilterType(Protocol): + @abstractmethod + def process(self, sample: float) -> float: + """ + Calculate y[n] + + >>> issubclass(FilterType, Protocol) + True + """ + + +def get_bounds( + fft_results: np.ndarray, samplerate: int +) -> tuple[int | float, int | float]: + """ + Get bounds for printing fft results + + >>> import numpy + >>> array = numpy.linspace(-20.0, 20.0, 1000) + >>> get_bounds(array, 1000) + (-20, 20) + """ + lowest = min([-20, np.min(fft_results[1 : samplerate // 2 - 1])]) + highest = max([20, np.max(fft_results[1 : samplerate // 2 - 1])]) + return lowest, highest + + +def show_frequency_response(filter_type: FilterType, samplerate: int) -> None: + """ + Show frequency response of a filter + + >>> from audio_filters.iir_filter import IIRFilter + >>> filt = IIRFilter(4) + >>> show_frequency_response(filt, 48000) + """ + + size = 512 + inputs = [1] + [0] * (size - 1) + outputs = [filter_type.process(item) for item in inputs] + + filler = [0] * (samplerate - size) # zero-padding + outputs += filler + fft_out = np.abs(np.fft.fft(outputs)) + fft_db = 20 * np.log10(fft_out) + + # Frequencies on log scale from 24 to nyquist frequency + plt.xlim(24, samplerate / 2 - 1) + plt.xlabel("Frequency (Hz)") + plt.xscale("log") + + # Display within reasonable bounds + bounds = get_bounds(fft_db, samplerate) + plt.ylim(max([-80, bounds[0]]), min([80, bounds[1]])) + plt.ylabel("Gain (dB)") + + plt.plot(fft_db) + plt.show() + + +def show_phase_response(filter_type: FilterType, samplerate: int) -> None: + """ + Show phase response of a filter + + >>> from audio_filters.iir_filter import IIRFilter + >>> filt = IIRFilter(4) + >>> show_phase_response(filt, 48000) + """ + + size = 512 + inputs = [1] + [0] * (size - 1) + outputs = [filter_type.process(item) for item in inputs] + + filler = [0] * (samplerate - size) # zero-padding + outputs += filler + fft_out = np.angle(np.fft.fft(outputs)) + + # Frequencies on log scale from 24 to nyquist frequency + plt.xlim(24, samplerate / 2 - 1) + plt.xlabel("Frequency (Hz)") + plt.xscale("log") + + plt.ylim(-2 * pi, 2 * pi) + plt.ylabel("Phase shift (Radians)") + plt.plot(np.unwrap(fft_out, -2 * pi)) + plt.show() diff --git a/backtracking/README.md b/backtracking/README.md new file mode 100644 index 000000000000..d4975dfb5ad7 --- /dev/null +++ b/backtracking/README.md @@ -0,0 +1,8 @@ +# Backtracking + +Backtracking is a way to speed up the search process by removing candidates when they can't be the solution of a problem. + +* +* +* +* diff --git a/backtracking/__init__.py b/backtracking/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/backtracking/all_combinations.py b/backtracking/all_combinations.py new file mode 100644 index 000000000000..1d15c6263e14 --- /dev/null +++ b/backtracking/all_combinations.py @@ -0,0 +1,116 @@ +""" +In this problem, we want to determine all possible combinations of k +numbers out of 1 ... n. We use backtracking to solve this problem. + +Time complexity: O(C(n,k)) which is O(n choose k) = O((n!/(k! * (n - k)!))), +""" + +from __future__ import annotations + +from itertools import combinations + + +def combination_lists(n: int, k: int) -> list[list[int]]: + """ + Generates all possible combinations of k numbers out of 1 ... n using itertools. + + >>> combination_lists(n=4, k=2) + [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]] + """ + return [list(x) for x in combinations(range(1, n + 1), k)] + + +def generate_all_combinations(n: int, k: int) -> list[list[int]]: + """ + Generates all possible combinations of k numbers out of 1 ... n using backtracking. + + >>> generate_all_combinations(n=4, k=2) + [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]] + >>> generate_all_combinations(n=0, k=0) + [[]] + >>> generate_all_combinations(n=10, k=-1) + Traceback (most recent call last): + ... + ValueError: k must not be negative + >>> generate_all_combinations(n=-1, k=10) + Traceback (most recent call last): + ... + ValueError: n must not be negative + >>> generate_all_combinations(n=5, k=4) + [[1, 2, 3, 4], [1, 2, 3, 5], [1, 2, 4, 5], [1, 3, 4, 5], [2, 3, 4, 5]] + >>> generate_all_combinations(n=3, k=3) + [[1, 2, 3]] + >>> generate_all_combinations(n=3, k=1) + [[1], [2], [3]] + >>> generate_all_combinations(n=1, k=0) + [[]] + >>> generate_all_combinations(n=1, k=1) + [[1]] + >>> from itertools import combinations + >>> all(generate_all_combinations(n, k) == combination_lists(n, k) + ... for n in range(1, 6) for k in range(1, 6)) + True + """ + if k < 0: + raise ValueError("k must not be negative") + if n < 0: + raise ValueError("n must not be negative") + + result: list[list[int]] = [] + create_all_state(1, n, k, [], result) + return result + + +def create_all_state( + increment: int, + total_number: int, + level: int, + current_list: list[int], + total_list: list[list[int]], +) -> None: + """ + Helper function to recursively build all combinations. + + >>> create_all_state(1, 4, 2, [], result := []) + >>> result + [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]] + >>> create_all_state(1, 3, 3, [], result := []) + >>> result + [[1, 2, 3]] + >>> create_all_state(2, 2, 1, [1], result := []) + >>> result + [[1, 2]] + >>> create_all_state(1, 0, 0, [], result := []) + >>> result + [[]] + >>> create_all_state(1, 4, 0, [1, 2], result := []) + >>> result + [[1, 2]] + >>> create_all_state(5, 4, 2, [1, 2], result := []) + >>> result + [] + """ + if level == 0: + total_list.append(current_list[:]) + return + + for i in range(increment, total_number - level + 2): + current_list.append(i) + create_all_state(i + 1, total_number, level - 1, current_list, total_list) + current_list.pop() + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + print(generate_all_combinations(n=4, k=2)) + tests = ((n, k) for n in range(1, 5) for k in range(1, 5)) + for n, k in tests: + print(n, k, generate_all_combinations(n, k) == combination_lists(n, k)) + + print("Benchmark:") + from timeit import timeit + + for func in ("combination_lists", "generate_all_combinations"): + print(f"{func:>25}(): {timeit(f'{func}(n=4, k = 2)', globals=globals())}") diff --git a/backtracking/all_permutations.py b/backtracking/all_permutations.py new file mode 100644 index 000000000000..691b2a2b7592 --- /dev/null +++ b/backtracking/all_permutations.py @@ -0,0 +1,98 @@ +""" +In this problem, we want to determine all possible permutations +of the given sequence. We use backtracking to solve this problem. + +Time complexity: O(n! * n), +where n denotes the length of the given sequence. +""" + +from __future__ import annotations + + +def generate_all_permutations(sequence: list[int | str]) -> None: + """ + Generate and print all possible permutations of the given sequence. + + >>> generate_all_permutations([1, 2]) + [1, 2] + [2, 1] + >>> generate_all_permutations(["A", "B"]) + ['A', 'B'] + ['B', 'A'] + """ + create_state_space_tree(sequence, [], 0, [0 for i in range(len(sequence))]) + + +def create_state_space_tree( + sequence: list[int | str], + current_sequence: list[int | str], + index: int, + index_used: list[int], +) -> None: + """ + Creates a state space tree to iterate through each branch using DFS. + We know that each state has exactly len(sequence) - index children. + It terminates when it reaches the end of the given sequence. + + :param sequence: The input sequence for which permutations are generated. + :param current_sequence: The current permutation being built. + :param index: The current index in the sequence. + :param index_used: list to track which elements are used in permutation. + + Example 1: + >>> sequence = [1, 2, 3] + >>> current_sequence = [] + >>> index_used = [False, False, False] + >>> create_state_space_tree(sequence, current_sequence, 0, index_used) + [1, 2, 3] + [1, 3, 2] + [2, 1, 3] + [2, 3, 1] + [3, 1, 2] + [3, 2, 1] + + Example 2: + >>> sequence = ["A", "B", "C"] + >>> current_sequence = [] + >>> index_used = [False, False, False] + >>> create_state_space_tree(sequence, current_sequence, 0, index_used) + ['A', 'B', 'C'] + ['A', 'C', 'B'] + ['B', 'A', 'C'] + ['B', 'C', 'A'] + ['C', 'A', 'B'] + ['C', 'B', 'A'] + + Example 3: + >>> sequence = [1] + >>> current_sequence = [] + >>> index_used = [False] + >>> create_state_space_tree(sequence, current_sequence, 0, index_used) + [1] + """ + + if index == len(sequence): + print(current_sequence) + return + + for i in range(len(sequence)): + if not index_used[i]: + current_sequence.append(sequence[i]) + index_used[i] = True + create_state_space_tree(sequence, current_sequence, index + 1, index_used) + current_sequence.pop() + index_used[i] = False + + +""" +remove the comment to take an input from the user + +print("Enter the elements") +sequence = list(map(int, input().split())) +""" + +sequence: list[int | str] = [3, 1, 2, 4] +generate_all_permutations(sequence) + +sequence_2: list[int | str] = ["A", "B", "C"] +generate_all_permutations(sequence_2) diff --git a/backtracking/all_subsequences.py b/backtracking/all_subsequences.py new file mode 100644 index 000000000000..43ae4fed747c --- /dev/null +++ b/backtracking/all_subsequences.py @@ -0,0 +1,108 @@ +""" +In this problem, we want to determine all possible subsequences +of the given sequence. We use backtracking to solve this problem. + +Time complexity: O(2^n), +where n denotes the length of the given sequence. +""" + +from __future__ import annotations + +from typing import Any + + +def generate_all_subsequences(sequence: list[Any]) -> None: + """ + Generate and print all possible subsequences of the given sequence. + + >>> generate_all_subsequences([1, 2]) + [] + [2] + [1] + [1, 2] + >>> generate_all_subsequences(["A", "B"]) + [] + ['B'] + ['A'] + ['A', 'B'] + """ + + create_state_space_tree(sequence, [], 0) + + +def create_state_space_tree( + sequence: list[Any], current_subsequence: list[Any], index: int +) -> None: + """ + Creates a state space tree to iterate through each branch using DFS. + We know that each state has exactly two children. + It terminates when it reaches the end of the given sequence. + + :param sequence: The input sequence for which subsequences are generated. + :param current_subsequence: The current subsequence being built. + :param index: The current index in the sequence. + + Example: + >>> sequence = [3, 2, 1] + >>> current_subsequence = [] + >>> create_state_space_tree(sequence, current_subsequence, 0) + [] + [1] + [2] + [2, 1] + [3] + [3, 1] + [3, 2] + [3, 2, 1] + + >>> sequence = ["A", "B"] + >>> current_subsequence = [] + >>> create_state_space_tree(sequence, current_subsequence, 0) + [] + ['B'] + ['A'] + ['A', 'B'] + + >>> sequence = [] + >>> current_subsequence = [] + >>> create_state_space_tree(sequence, current_subsequence, 0) + [] + + >>> sequence = [1, 2, 3, 4] + >>> current_subsequence = [] + >>> create_state_space_tree(sequence, current_subsequence, 0) + [] + [4] + [3] + [3, 4] + [2] + [2, 4] + [2, 3] + [2, 3, 4] + [1] + [1, 4] + [1, 3] + [1, 3, 4] + [1, 2] + [1, 2, 4] + [1, 2, 3] + [1, 2, 3, 4] + """ + + if index == len(sequence): + print(current_subsequence) + return + + create_state_space_tree(sequence, current_subsequence, index + 1) + current_subsequence.append(sequence[index]) + create_state_space_tree(sequence, current_subsequence, index + 1) + current_subsequence.pop() + + +if __name__ == "__main__": + seq: list[Any] = [1, 2, 3] + generate_all_subsequences(seq) + + seq.clear() + seq.extend(["A", "B", "C"]) + generate_all_subsequences(seq) diff --git a/backtracking/coloring.py b/backtracking/coloring.py new file mode 100644 index 000000000000..923f8bb65927 --- /dev/null +++ b/backtracking/coloring.py @@ -0,0 +1,186 @@ +""" +Graph Coloring (also called the "m coloring problem") is the problem of +assigning at most 'm' colors to the vertices of a graph such that +no two adjacent vertices share the same color. + +Wikipedia: https://en.wikipedia.org/wiki/Graph_coloring +""" + + +def valid_coloring( + neighbours: list[int], colored_vertices: list[int], color: int +) -> bool: + """ + Check if a given vertex can be assigned the specified color + without violating the graph coloring constraints (i.e., no two adjacent vertices + have the same color). + + Procedure: + For each neighbour check if the coloring constraint is satisfied + If any of the neighbours fail the constraint return False + If all neighbours validate the constraint return True + + Parameters: + neighbours: The list representing which vertices + are adjacent to the current vertex. + 1 indicates an edge between the current vertex + and the neighbour. + colored_vertices: List of current color assignments for all vertices + (-1 means uncolored). + color: The color we are trying to assign to the current vertex. + + Returns: + True if the vertex can be safely colored with the given color, + otherwise False. + + Examples: + >>> neighbours = [0, 1, 0, 1, 0] + >>> colored_vertices = [0, 2, 1, 2, 0] + >>> color = 1 + >>> valid_coloring(neighbours, colored_vertices, color) + True + + >>> color = 2 + >>> valid_coloring(neighbours, colored_vertices, color) + False + + >>> neighbors = [1, 0, 1, 0] + >>> colored_vertices = [-1, -1, -1, -1] + >>> color = 0 + >>> valid_coloring(neighbors, colored_vertices, color) + True + """ + # Check if any adjacent vertex has already been colored with the same color + return not any( + neighbour == 1 and colored_vertices[i] == color + for i, neighbour in enumerate(neighbours) + ) + + +def util_color( + graph: list[list[int]], max_colors: int, colored_vertices: list[int], index: int +) -> bool: + """ + Recursive function to try and color the graph using backtracking. + + Base Case: + 1. Check if coloring is complete + 1.1 If complete return True (meaning that we successfully colored the graph) + + Recursive Step: + 2. Iterates over each color: + Check if the current coloring is valid: + 2.1. Color given vertex + 2.2. Do recursive call, check if this coloring leads to a solution + 2.4. if current coloring leads to a solution return + 2.5. Uncolor given vertex + + Parameters: + graph: Adjacency matrix representing the graph. + graph[i][j] is 1 if there is an edge + between vertex i and j. + max_colors: Maximum number of colors allowed (m in the m-coloring problem). + colored_vertices: Current color assignments for each vertex. + -1 indicates that the vertex has not been colored + yet. + index: The current vertex index being processed. + + Returns: + True if the graph can be colored using at most max_colors, otherwise False. + + Examples: + >>> graph = [[0, 1, 0, 0, 0], + ... [1, 0, 1, 0, 1], + ... [0, 1, 0, 1, 0], + ... [0, 1, 1, 0, 0], + ... [0, 1, 0, 0, 0]] + >>> max_colors = 3 + >>> colored_vertices = [0, 1, 0, 0, 0] + >>> index = 3 + + >>> util_color(graph, max_colors, colored_vertices, index) + True + + >>> max_colors = 2 + >>> util_color(graph, max_colors, colored_vertices, index) + False + """ + # Base Case: If all vertices have been assigned a color, we have a valid solution + if index == len(graph): + return True + + # Try each color for the current vertex + for color in range(max_colors): + # Check if it's valid to color the current vertex with 'color' + if valid_coloring(graph[index], colored_vertices, color): + colored_vertices[index] = color # Assign color + # Recur to color the rest of the vertices + if util_color(graph, max_colors, colored_vertices, index + 1): + return True + # Backtrack if no solution found with the current assignment + colored_vertices[index] = -1 + + return False # Return False if no valid coloring is possible + + +def color(graph: list[list[int]], max_colors: int) -> list[int]: + """ + Attempt to color the graph with at most max_colors colors such that no two adjacent + vertices have the same color. + If it is possible, returns the list of color assignments; + otherwise, returns an empty list. + + Parameters: + graph: Adjacency matrix representing the graph. + max_colors: Maximum number of colors allowed. + + Returns: + List of color assignments if the graph can be colored using max_colors. + Each index in the list represents the color assigned + to the corresponding vertex. + If coloring is not possible, returns an empty list. + + Examples: + >>> graph = [[0, 1, 0, 0, 0], + ... [1, 0, 1, 0, 1], + ... [0, 1, 0, 1, 0], + ... [0, 1, 1, 0, 0], + ... [0, 1, 0, 0, 0]] + >>> max_colors = 3 + >>> color(graph, max_colors) + [0, 1, 0, 2, 0] + + >>> max_colors = 2 + >>> color(graph, max_colors) + [] + + >>> graph = [[0, 1], [1, 0]] # Simple 2-node graph + >>> max_colors = 2 + >>> color(graph, max_colors) + [0, 1] + + >>> graph = [[0, 1, 1], [1, 0, 1], [1, 1, 0]] # Complete graph of 3 vertices + >>> max_colors = 2 + >>> color(graph, max_colors) + [] + + >>> max_colors = 3 + >>> color(graph, max_colors) + [0, 1, 2] + >>> color([], 2) # empty graph + [] + >>> color([[0]], 1) # single node, 1 color + [0] + >>> color([[0, 1], [1, 0]], 1) # 2 nodes, 1 color (impossible) + [] + >>> color([[0, 1], [1, 0]], 2) # 2 nodes, 2 colors (possible) + [0, 1] + """ + # Initialize all vertices as uncolored (-1) + colored_vertices = [-1] * len(graph) + + # Use the utility function to try and color the graph starting from vertex 0 + if util_color(graph, max_colors, colored_vertices, 0): + return colored_vertices # The successful color assignment + + return [] # No valid coloring is possible diff --git a/backtracking/combination_sum.py b/backtracking/combination_sum.py new file mode 100644 index 000000000000..3d954f11d2c5 --- /dev/null +++ b/backtracking/combination_sum.py @@ -0,0 +1,76 @@ +""" +In the Combination Sum problem, we are given a list consisting of distinct integers. +We need to find all the combinations whose sum equals to target given. +We can use an element more than one. + +Time complexity(Average Case): O(n!) + +Constraints: +1 <= candidates.length <= 30 +2 <= candidates[i] <= 40 +All elements of candidates are distinct. +1 <= target <= 40 +""" + + +def backtrack( + candidates: list, path: list, answer: list, target: int, previous_index: int +) -> None: + """ + A recursive function that searches for possible combinations. Backtracks in case + of a bigger current combination value than the target value. + + Parameters + ---------- + previous_index: Last index from the previous search + target: The value we need to obtain by summing our integers in the path list. + answer: A list of possible combinations + path: Current combination + candidates: A list of integers we can use. + """ + if target == 0: + answer.append(path.copy()) + else: + for index in range(previous_index, len(candidates)): + if target >= candidates[index]: + path.append(candidates[index]) + backtrack(candidates, path, answer, target - candidates[index], index) + path.pop(len(path) - 1) + + +def combination_sum(candidates: list, target: int) -> list: + """ + >>> combination_sum([2, 3, 5], 8) + [[2, 2, 2, 2], [2, 3, 3], [3, 5]] + >>> combination_sum([2, 3, 6, 7], 7) + [[2, 2, 3], [7]] + >>> combination_sum([-8, 2.3, 0], 1) + Traceback (most recent call last): + ... + ValueError: All elements in candidates must be non-negative + >>> combination_sum([], 1) + Traceback (most recent call last): + ... + ValueError: Candidates list should not be empty + """ + if not candidates: + raise ValueError("Candidates list should not be empty") + + if any(x < 0 for x in candidates): + raise ValueError("All elements in candidates must be non-negative") + + path = [] # type: list[int] + answer = [] # type: list[int] + backtrack(candidates, path, answer, target, 0) + return answer + + +def main() -> None: + print(combination_sum([-8, 2.3, 0], 1)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/backtracking/crossword_puzzle_solver.py b/backtracking/crossword_puzzle_solver.py new file mode 100644 index 000000000000..e510909cfc41 --- /dev/null +++ b/backtracking/crossword_puzzle_solver.py @@ -0,0 +1,122 @@ +# https://www.geeksforgeeks.org/solve-crossword-puzzle/ + + +def is_valid( + puzzle: list[list[str]], word: str, row: int, col: int, vertical: bool +) -> bool: + """ + Check if a word can be placed at the given position. + A cell is valid if it is empty or already contains the correct letter + (enabling crossing/intersection between words). + + >>> puzzle = [['', '', '', ''], ['', '', '', ''], + ... ['', '', '', ''], ['', '', '', '']] + >>> is_valid(puzzle, 'word', 0, 0, True) + True + >>> is_valid(puzzle, 'word', 0, 0, False) + True + >>> puzzle2 = [['w', '', ''], ['o', '', ''], ['r', '', ''], ['d', '', '']] + >>> is_valid(puzzle2, 'word', 0, 0, True) + True + >>> is_valid(puzzle2, 'cat', 0, 0, True) + False + """ + rows, cols = len(puzzle), len(puzzle[0]) + for i, ch in enumerate(word): + r, c = (row + i, col) if vertical else (row, col + i) + if r >= rows or c >= cols: + return False + cell = puzzle[r][c] + if cell not in ("", ch): + return False + return True + + +def place_word( + puzzle: list[list[str]], word: str, row: int, col: int, vertical: bool +) -> None: + """ + Place a word at the given position in the puzzle. + + >>> puzzle = [['', '', '', ''], ['', '', '', ''], + ... ['', '', '', ''], ['', '', '', '']] + >>> place_word(puzzle, 'word', 0, 0, True) + >>> puzzle + [['w', '', '', ''], ['o', '', '', ''], ['r', '', '', ''], ['d', '', '', '']] + """ + for i, ch in enumerate(word): + if vertical: + puzzle[row + i][col] = ch + else: + puzzle[row][col + i] = ch + + +def remove_word( + puzzle: list[list[str]], + word: str, + row: int, + col: int, + vertical: bool, + snapshot: list[list[str]], +) -> None: + """ + Remove a word from the puzzle, restoring only cells that were empty + before placement. Cells shared with crossing words are preserved. + + >>> puzzle = [['w', 'o', 'r', 'd'], ['', '', '', ''], + ... ['', '', '', ''], ['', '', '', '']] + >>> snap = [['', 'o', 'r', 'd'], ['', '', '', ''], + ... ['', '', '', ''], ['', '', '', '']] + >>> remove_word(puzzle, 'word', 0, 0, False, snap) + >>> puzzle + [['', 'o', 'r', 'd'], ['', '', '', ''], ['', '', '', ''], ['', '', '', '']] + """ + for i in range(len(word)): + r, c = (row + i, col) if vertical else (row, col + i) + if snapshot[r][c] == "": + puzzle[r][c] = "" + + +def solve_crossword(puzzle: list[list[str]], words: list[str]) -> bool: + """ + Solve the crossword puzzle using backtracking. + Words are tried longest-first to prune the search space early. + Intersections between words (shared letters) are supported. + + >>> puzzle = [['', '', '', ''], ['', '', '', ''], + ... ['', '', '', ''], ['', '', '', '']] + >>> solve_crossword(puzzle, ['word', 'four', 'more', 'last']) + True + >>> puzzle2 = [['', '', '', ''], ['', '', '', ''], + ... ['', '', '', ''], ['', '', '', '']] + >>> solve_crossword(puzzle2, ['word', 'four', 'more', 'paragraphs']) + False + """ + if not words: + return True + + remaining = sorted(words, key=len, reverse=True) + word, rest = remaining[0], remaining[1:] + + for row in range(len(puzzle)): + for col in range(len(puzzle[0])): + for vertical in (True, False): + if is_valid(puzzle, word, row, col, vertical): + snapshot = [r[:] for r in puzzle] + place_word(puzzle, word, row, col, vertical) + if solve_crossword(puzzle, rest): + return True + remove_word(puzzle, word, row, col, vertical, snapshot) + + return False + + +if __name__ == "__main__": + PUZZLE = [[""] * 3 for _ in range(3)] + WORDS = ["cat", "dog", "car"] + if solve_crossword(PUZZLE, WORDS): + print("Solution found:") + for row in PUZZLE: + print(" ".join(cell or "." for cell in row)) + else: + print("No solution found.") diff --git a/backtracking/generate_parentheses.py b/backtracking/generate_parentheses.py new file mode 100644 index 000000000000..5094f4b08619 --- /dev/null +++ b/backtracking/generate_parentheses.py @@ -0,0 +1,81 @@ +""" +author: Aayush Soni +Given n pairs of parentheses, write a function to generate all +combinations of well-formed parentheses. +Input: n = 2 +Output: ["(())","()()"] +Leetcode link: https://leetcode.com/problems/generate-parentheses/description/ +""" + + +def backtrack( + partial: str, open_count: int, close_count: int, n: int, result: list[str] +) -> None: + """ + Generate valid combinations of balanced parentheses using recursion. + + :param partial: A string representing the current combination. + :param open_count: An integer representing the count of open parentheses. + :param close_count: An integer representing the count of close parentheses. + :param n: An integer representing the total number of pairs. + :param result: A list to store valid combinations. + :return: None + + This function uses recursion to explore all possible combinations, + ensuring that at each step, the parentheses remain balanced. + + Example: + >>> result = [] + >>> backtrack("", 0, 0, 2, result) + >>> result + ['(())', '()()'] + """ + if len(partial) == 2 * n: + # When the combination is complete, add it to the result. + result.append(partial) + return + + if open_count < n: + # If we can add an open parenthesis, do so, and recurse. + backtrack(partial + "(", open_count + 1, close_count, n, result) + + if close_count < open_count: + # If we can add a close parenthesis (it won't make the combination invalid), + # do so, and recurse. + backtrack(partial + ")", open_count, close_count + 1, n, result) + + +def generate_parenthesis(n: int) -> list[str]: + """ + Generate valid combinations of balanced parentheses for a given n. + + :param n: An integer representing the number of pairs of parentheses. + :return: A list of strings with valid combinations. + + This function uses a recursive approach to generate the combinations. + + Time Complexity: O(2^(2n)) - In the worst case, we have 2^(2n) combinations. + Space Complexity: O(n) - where 'n' is the number of pairs. + + Example 1: + >>> generate_parenthesis(3) + ['((()))', '(()())', '(())()', '()(())', '()()()'] + + Example 2: + >>> generate_parenthesis(1) + ['()'] + + Example 3: + >>> generate_parenthesis(0) + [''] + """ + + result: list[str] = [] + backtrack("", 0, 0, n, result) + return result + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/backtracking/generate_parentheses_iterative.py b/backtracking/generate_parentheses_iterative.py new file mode 100644 index 000000000000..84c032f52dc4 --- /dev/null +++ b/backtracking/generate_parentheses_iterative.py @@ -0,0 +1,67 @@ +def generate_parentheses_iterative(length: int) -> list[str]: + """ + Generate all valid combinations of parentheses (Iterative Approach). + + The algorithm works as follows: + 1. Initialize an empty list to store the combinations. + 2. Initialize a stack to keep track of partial combinations. + 3. Start with empty string and push it on stack along with + the counts of '(' and ')'. + 4. While the stack is not empty: + a. Pop a partial combination and its open and close counts from the stack. + b. If the combination length is equal to 2*length, add it to the result. + c. If open count < length, push new combination with added '(' on stack. + d. If close count < open count, push new combination with added ')' on stack. + 5. Return the result containing all valid combinations. + + Args: + length: The desired length of the parentheses combinations + + Returns: + A list of strings representing valid combinations of parentheses + + Time Complexity: + O(2^(2*length)) + + Space Complexity: + O(2^(2*length)) + + >>> generate_parentheses_iterative(3) + ['()()()', '()(())', '(())()', '(()())', '((()))'] + >>> generate_parentheses_iterative(2) + ['()()', '(())'] + >>> generate_parentheses_iterative(1) + ['()'] + >>> generate_parentheses_iterative(0) + [''] + """ + if length == 0: + return [""] + + result: list[str] = [] + stack: list[tuple[str, int, int]] = [] + + # Each element in stack is a tuple (current_combination, open_count, close_count) + stack.append(("", 0, 0)) + + while stack: + current_combination, open_count, close_count = stack.pop() + + if len(current_combination) == 2 * length: + result.append(current_combination) + continue + + if open_count < length: + stack.append((current_combination + "(", open_count + 1, close_count)) + + if close_count < open_count: + stack.append((current_combination + ")", open_count, close_count + 1)) + + return result + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + print(generate_parentheses_iterative(3)) diff --git a/backtracking/hamiltonian_cycle.py b/backtracking/hamiltonian_cycle.py new file mode 100644 index 000000000000..f6e4212e47f4 --- /dev/null +++ b/backtracking/hamiltonian_cycle.py @@ -0,0 +1,176 @@ +""" +A Hamiltonian cycle (Hamiltonian circuit) is a graph cycle +through a graph that visits each node exactly once. +Determining whether such paths and cycles exist in graphs +is the 'Hamiltonian path problem', which is NP-complete. + +Wikipedia: https://en.wikipedia.org/wiki/Hamiltonian_path +""" + + +def valid_connection( + graph: list[list[int]], next_ver: int, curr_ind: int, path: list[int] +) -> bool: + """ + Checks whether it is possible to add next into path by validating 2 statements + 1. There should be path between current and next vertex + 2. Next vertex should not be in path + If both validations succeed we return True, saying that it is possible to connect + this vertices, otherwise we return False + + Case 1:Use exact graph as in main function, with initialized values + >>> graph = [[0, 1, 0, 1, 0], + ... [1, 0, 1, 1, 1], + ... [0, 1, 0, 0, 1], + ... [1, 1, 0, 0, 1], + ... [0, 1, 1, 1, 0]] + >>> path = [0, -1, -1, -1, -1, 0] + >>> curr_ind = 1 + >>> next_ver = 1 + >>> valid_connection(graph, next_ver, curr_ind, path) + True + + Case 2: Same graph, but trying to connect to node that is already in path + >>> path = [0, 1, 2, 4, -1, 0] + >>> curr_ind = 4 + >>> next_ver = 1 + >>> valid_connection(graph, next_ver, curr_ind, path) + False + """ + + # 1. Validate that path exists between current and next vertices + if graph[path[curr_ind - 1]][next_ver] == 0: + return False + + # 2. Validate that next vertex is not already in path + return not any(vertex == next_ver for vertex in path) + + +def util_hamilton_cycle(graph: list[list[int]], path: list[int], curr_ind: int) -> bool: + """ + Pseudo-Code + Base Case: + 1. Check if we visited all of vertices + 1.1 If last visited vertex has path to starting vertex return True either + return False + Recursive Step: + 2. Iterate over each vertex + Check if next vertex is valid for transiting from current vertex + 2.1 Remember next vertex as next transition + 2.2 Do recursive call and check if going to this vertex solves problem + 2.3 If next vertex leads to solution return True + 2.4 Else backtrack, delete remembered vertex + + Case 1: Use exact graph as in main function, with initialized values + >>> graph = [[0, 1, 0, 1, 0], + ... [1, 0, 1, 1, 1], + ... [0, 1, 0, 0, 1], + ... [1, 1, 0, 0, 1], + ... [0, 1, 1, 1, 0]] + >>> path = [0, -1, -1, -1, -1, 0] + >>> curr_ind = 1 + >>> util_hamilton_cycle(graph, path, curr_ind) + True + >>> path + [0, 1, 2, 4, 3, 0] + + Case 2: Use exact graph as in previous case, but in the properties taken from + middle of calculation + >>> graph = [[0, 1, 0, 1, 0], + ... [1, 0, 1, 1, 1], + ... [0, 1, 0, 0, 1], + ... [1, 1, 0, 0, 1], + ... [0, 1, 1, 1, 0]] + >>> path = [0, 1, 2, -1, -1, 0] + >>> curr_ind = 3 + >>> util_hamilton_cycle(graph, path, curr_ind) + True + >>> path + [0, 1, 2, 4, 3, 0] + """ + + # Base Case + if curr_ind == len(graph): + # return whether path exists between current and starting vertices + return graph[path[curr_ind - 1]][path[0]] == 1 + + # Recursive Step + for next_ver in range(len(graph)): + if valid_connection(graph, next_ver, curr_ind, path): + # Insert current vertex into path as next transition + path[curr_ind] = next_ver + # Validate created path + if util_hamilton_cycle(graph, path, curr_ind + 1): + return True + # Backtrack + path[curr_ind] = -1 + return False + + +def hamilton_cycle(graph: list[list[int]], start_index: int = 0) -> list[int]: + r""" + Wrapper function to call subroutine called util_hamilton_cycle, + which will either return array of vertices indicating hamiltonian cycle + or an empty list indicating that hamiltonian cycle was not found. + Case 1: + Following graph consists of 5 edges. + If we look closely, we can see that there are multiple Hamiltonian cycles. + For example one result is when we iterate like: + (0)->(1)->(2)->(4)->(3)->(0) + + (0)---(1)---(2) + | / \ | + | / \ | + | / \ | + |/ \| + (3)---------(4) + >>> graph = [[0, 1, 0, 1, 0], + ... [1, 0, 1, 1, 1], + ... [0, 1, 0, 0, 1], + ... [1, 1, 0, 0, 1], + ... [0, 1, 1, 1, 0]] + >>> hamilton_cycle(graph) + [0, 1, 2, 4, 3, 0] + + Case 2: + Same Graph as it was in Case 1, changed starting index from default to 3 + + (0)---(1)---(2) + | / \ | + | / \ | + | / \ | + |/ \| + (3)---------(4) + >>> graph = [[0, 1, 0, 1, 0], + ... [1, 0, 1, 1, 1], + ... [0, 1, 0, 0, 1], + ... [1, 1, 0, 0, 1], + ... [0, 1, 1, 1, 0]] + >>> hamilton_cycle(graph, 3) + [3, 0, 1, 2, 4, 3] + + Case 3: + Following Graph is exactly what it was before, but edge 3-4 is removed. + Result is that there is no Hamiltonian Cycle anymore. + + (0)---(1)---(2) + | / \ | + | / \ | + | / \ | + |/ \| + (3) (4) + >>> graph = [[0, 1, 0, 1, 0], + ... [1, 0, 1, 1, 1], + ... [0, 1, 0, 0, 1], + ... [1, 1, 0, 0, 0], + ... [0, 1, 1, 0, 0]] + >>> hamilton_cycle(graph,4) + [] + """ + + # Initialize path with -1, indicating that we have not visited them yet + path = [-1] * (len(graph) + 1) + # initialize start and end of path with starting index + path[0] = path[-1] = start_index + # evaluate and if we find answer return path either return empty array + return path if util_hamilton_cycle(graph, path, 1) else [] diff --git a/backtracking/knight_tour.py b/backtracking/knight_tour.py new file mode 100644 index 000000000000..8906aaa1094c --- /dev/null +++ b/backtracking/knight_tour.py @@ -0,0 +1,101 @@ +# Knight Tour Intro: https://www.youtube.com/watch?v=ab_dY3dZFHM + +from __future__ import annotations + + +def get_valid_pos(position: tuple[int, int], n: int) -> list[tuple[int, int]]: + """ + Find all the valid positions a knight can move to from the current position. + + >>> get_valid_pos((1, 3), 4) + [(2, 1), (0, 1), (3, 2)] + """ + + y, x = position + positions = [ + (y + 1, x + 2), + (y - 1, x + 2), + (y + 1, x - 2), + (y - 1, x - 2), + (y + 2, x + 1), + (y + 2, x - 1), + (y - 2, x + 1), + (y - 2, x - 1), + ] + permissible_positions = [] + + for inner_position in positions: + y_test, x_test = inner_position + if 0 <= y_test < n and 0 <= x_test < n: + permissible_positions.append(inner_position) + + return permissible_positions + + +def is_complete(board: list[list[int]]) -> bool: + """ + Check if the board (matrix) has been completely filled with non-zero values. + + >>> is_complete([[1]]) + True + + >>> is_complete([[1, 2], [3, 0]]) + False + """ + + return not any(elem == 0 for row in board for elem in row) + + +def open_knight_tour_helper( + board: list[list[int]], pos: tuple[int, int], curr: int +) -> bool: + """ + Helper function to solve knight tour problem. + """ + + if is_complete(board): + return True + + for position in get_valid_pos(pos, len(board)): + y, x = position + + if board[y][x] == 0: + board[y][x] = curr + 1 + if open_knight_tour_helper(board, position, curr + 1): + return True + board[y][x] = 0 + + return False + + +def open_knight_tour(n: int) -> list[list[int]]: + """ + Find the solution for the knight tour problem for a board of size n. Raises + ValueError if the tour cannot be performed for the given size. + + >>> open_knight_tour(1) + [[1]] + + >>> open_knight_tour(2) + Traceback (most recent call last): + ... + ValueError: Open Knight Tour cannot be performed on a board of size 2 + """ + + board = [[0 for i in range(n)] for j in range(n)] + + for i in range(n): + for j in range(n): + board[i][j] = 1 + if open_knight_tour_helper(board, (i, j), 1): + return board + board[i][j] = 0 + + msg = f"Open Knight Tour cannot be performed on a board of size {n}" + raise ValueError(msg) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/backtracking/m_coloring_problem.py b/backtracking/m_coloring_problem.py new file mode 100644 index 000000000000..89de4c09e10b --- /dev/null +++ b/backtracking/m_coloring_problem.py @@ -0,0 +1,86 @@ +def is_safe( + node: int, + color: int, + graph: list[list[int]], + num_vertices: int, + col: list[int], +) -> bool: + """ + Check if it is safe to assign a color to a node. + + >>> is_safe(0, 1, [[0,1],[1,0]], 2, [0,1]) + False + >>> is_safe(0, 2, [[0,1],[1,0]], 2, [0,1]) + True + """ + return all( + not (graph[node][k] == 1 and col[k] == color) for k in range(num_vertices) + ) + + +def solve( + node: int, + col: list[int], + max_colors: int, + num_vertices: int, + graph: list[list[int]], +) -> bool: + """ + Recursively try to color the graph using at most max_colors. + + >>> solve(0, [0]*3, 3, 3, [[0,1,0],[1,0,1],[0,1,0]]) + True + >>> solve(0, [0]*3, 2, 3, [[0,1,0],[1,0,1],[0,1,0]]) + True + """ + if node == num_vertices: + return True + for c in range(1, max_colors + 1): + if is_safe(node, c, graph, num_vertices, col): + col[node] = c + if solve(node + 1, col, max_colors, num_vertices, graph): + return True + col[node] = 0 + return False + + +def graph_coloring(graph: list[list[int]], max_colors: int, num_vertices: int) -> bool: + """ + Determine if the graph can be colored with at most max_colors. + + >>> graph_coloring([[0,1,1],[1,0,1],[1,1,0]], 3, 3) + True + >>> graph_coloring([[0,1,1],[1,0,1],[1,1,0]], 2, 3) + False + """ + col = [0] * num_vertices + return solve(0, col, max_colors, num_vertices, graph) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + num_vertices = int(input("Enter vertices: ")) + num_edges = int(input("Enter edges: ")) + graph = [[0] * num_vertices for _ in range(num_vertices)] + + print("Enter edges (u v):") + for _ in range(num_edges): + try: + u, v = map(int, input().split()) + if 0 <= u < num_vertices and 0 <= v < num_vertices: + graph[u][v] = 1 + graph[v][u] = 1 + else: + print("Invalid edge.") + except ValueError: + print("Invalid input.") + + max_colors = int(input("Enter max colors: ")) + + if graph_coloring(graph, max_colors, num_vertices): + print("Coloring possible.") + else: + print("Coloring not possible.") diff --git a/backtracking/match_word_pattern.py b/backtracking/match_word_pattern.py new file mode 100644 index 000000000000..bfa9b1354d51 --- /dev/null +++ b/backtracking/match_word_pattern.py @@ -0,0 +1,61 @@ +def match_word_pattern(pattern: str, input_string: str) -> bool: + """ + Determine if a given pattern matches a string using backtracking. + + pattern: The pattern to match. + input_string: The string to match against the pattern. + return: True if the pattern matches the string, False otherwise. + + >>> match_word_pattern("aba", "GraphTreesGraph") + True + + >>> match_word_pattern("xyx", "PythonRubyPython") + True + + >>> match_word_pattern("GG", "PythonJavaPython") + False + """ + + def backtrack(pattern_index: int, str_index: int) -> bool: + """ + >>> backtrack(0, 0) + True + + >>> backtrack(0, 1) + True + + >>> backtrack(0, 4) + False + """ + if pattern_index == len(pattern) and str_index == len(input_string): + return True + if pattern_index == len(pattern) or str_index == len(input_string): + return False + char = pattern[pattern_index] + if char in pattern_map: + mapped_str = pattern_map[char] + if input_string.startswith(mapped_str, str_index): + return backtrack(pattern_index + 1, str_index + len(mapped_str)) + else: + return False + for end in range(str_index + 1, len(input_string) + 1): + substr = input_string[str_index:end] + if substr in str_map: + continue + pattern_map[char] = substr + str_map[substr] = char + if backtrack(pattern_index + 1, end): + return True + del pattern_map[char] + del str_map[substr] + return False + + pattern_map: dict[str, str] = {} + str_map: dict[str, str] = {} + return backtrack(0, 0) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/backtracking/minimax.py b/backtracking/minimax.py new file mode 100644 index 000000000000..96fd88cd5afa --- /dev/null +++ b/backtracking/minimax.py @@ -0,0 +1,101 @@ +""" +Minimax helps to achieve maximum score in a game by checking all possible moves +depth is current depth in game tree. + +nodeIndex is index of current node in scores[]. +if move is of maximizer return true else false +leaves of game tree is stored in scores[] +height is maximum height of Game tree +""" + +from __future__ import annotations + +import math + + +def minimax( + depth: int, node_index: int, is_max: bool, scores: list[int], height: float +) -> int: + """ + This function implements the minimax algorithm, which helps achieve the optimal + score for a player in a two-player game by checking all possible moves. + If the player is the maximizer, then the score is maximized. + If the player is the minimizer, then the score is minimized. + + Parameters: + - depth: Current depth in the game tree. + - node_index: Index of the current node in the scores list. + - is_max: A boolean indicating whether the current move + is for the maximizer (True) or minimizer (False). + - scores: A list containing the scores of the leaves of the game tree. + - height: The maximum height of the game tree. + + Returns: + - An integer representing the optimal score for the current player. + + >>> import math + >>> scores = [90, 23, 6, 33, 21, 65, 123, 34423] + >>> height = math.log(len(scores), 2) + >>> minimax(0, 0, True, scores, height) + 65 + >>> minimax(-1, 0, True, scores, height) + Traceback (most recent call last): + ... + ValueError: Depth cannot be less than 0 + >>> minimax(0, 0, True, [], 2) + Traceback (most recent call last): + ... + ValueError: Scores cannot be empty + >>> minimax(0, 0, True, [1, 2, 3], 2) + Traceback (most recent call last): + ... + ValueError: Number of scores must be a power of 2 + >>> scores = [3, 5, 2, 9, 12, 5, 23, 23] + >>> height = math.log(len(scores), 2) + >>> minimax(0, 0, True, scores, height) + 12 + """ + + if depth < 0: + raise ValueError("Depth cannot be less than 0") + if len(scores) == 0: + raise ValueError("Scores cannot be empty") + if len(scores) & (len(scores) - 1) != 0: + raise ValueError("Number of scores must be a power of 2") + + # Base case: If the current depth equals the height of the tree, + # return the score of the current node. + if depth == height: + return scores[node_index] + + # If it's the maximizer's turn, choose the maximum score + # between the two possible moves. + if is_max: + return max( + minimax(depth + 1, node_index * 2, False, scores, height), + minimax(depth + 1, node_index * 2 + 1, False, scores, height), + ) + + # If it's the minimizer's turn, choose the minimum score + # between the two possible moves. + return min( + minimax(depth + 1, node_index * 2, True, scores, height), + minimax(depth + 1, node_index * 2 + 1, True, scores, height), + ) + + +def main() -> None: + # Sample scores and height calculation + scores = [90, 23, 6, 33, 21, 65, 123, 34423] + height = math.log(len(scores), 2) + + # Calculate and print the optimal value using the minimax algorithm + print("Optimal value : ", end="") + print(minimax(0, 0, True, scores, height)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/backtracking/n_queens.py b/backtracking/n_queens.py new file mode 100644 index 000000000000..6fac93aa77d6 --- /dev/null +++ b/backtracking/n_queens.py @@ -0,0 +1,109 @@ +""" + +The nqueens problem is of placing N queens on a N * N +chess board such that no queen can attack any other queens placed +on that chess board. +This means that one queen cannot have any other queen on its horizontal, vertical and +diagonal lines. + +""" + +from __future__ import annotations + +solution = [] + + +def is_safe(board: list[list[int]], row: int, column: int) -> bool: + """ + This function returns a boolean value True if it is safe to place a queen there + considering the current state of the board. + + Parameters: + board (2D matrix): The chessboard + row, column: Coordinates of the cell on the board + + Returns: + Boolean Value + + >>> is_safe([[0, 0, 0], [0, 0, 0], [0, 0, 0]], 1, 1) + True + >>> is_safe([[0, 1, 0], [0, 0, 0], [0, 0, 0]], 1, 1) + False + >>> is_safe([[1, 0, 0], [0, 0, 0], [0, 0, 0]], 1, 1) + False + >>> is_safe([[0, 0, 1], [0, 0, 0], [0, 0, 0]], 1, 1) + False + >>> is_safe([[1, 0, 0], [0, 0, 0], [0, 0, 0]], 1, 2) + True + >>> is_safe([[1, 0, 0], [0, 0, 0], [0, 0, 0]], 2, 1) + True + >>> is_safe([[0, 0, 0], [1, 0, 0], [0, 0, 0]], 0, 2) + True + >>> is_safe([[0, 0, 0], [1, 0, 0], [0, 0, 0]], 2, 2) + True + """ + + n = len(board) # Size of the board + + # Check if there is any queen in the same upper column, + # left upper diagonal and right upper diagonal + return ( + all(board[i][j] != 1 for i, j in zip(range(row), [column] * row)) + and all( + board[i][j] != 1 + for i, j in zip(range(row - 1, -1, -1), range(column - 1, -1, -1)) + ) + and all( + board[i][j] != 1 + for i, j in zip(range(row - 1, -1, -1), range(column + 1, n)) + ) + ) + + +def solve(board: list[list[int]], row: int) -> bool: + """ + This function creates a state space tree and calls the safe function until it + receives a False Boolean and terminates that branch and backtracks to the next + possible solution branch. + """ + if row >= len(board): + """ + If the row number exceeds N, we have a board with a successful combination + and that combination is appended to the solution list and the board is printed. + """ + solution.append(board) + printboard(board) + print() + return True + for i in range(len(board)): + """ + For every row, it iterates through each column to check if it is feasible to + place a queen there. + If all the combinations for that particular branch are successful, the board is + reinitialized for the next possible combination. + """ + if is_safe(board, row, i): + board[row][i] = 1 + solve(board, row + 1) + board[row][i] = 0 + return False + + +def printboard(board: list[list[int]]) -> None: + """ + Prints the boards that have a successful combination. + """ + for i in range(len(board)): + for j in range(len(board)): + if board[i][j] == 1: + print("Q", end=" ") # Queen is present + else: + print(".", end=" ") # Empty cell + print() + + +# Number of queens (e.g., n=8 for an 8x8 board) +n = 8 +board = [[0 for i in range(n)] for j in range(n)] +solve(board, 0) +print("The total number of solutions are:", len(solution)) diff --git a/backtracking/n_queens_math.py b/backtracking/n_queens_math.py new file mode 100644 index 000000000000..287d1f090373 --- /dev/null +++ b/backtracking/n_queens_math.py @@ -0,0 +1,158 @@ +r""" +Problem: + +The n queens problem is: placing N queens on a N * N chess board such that no queen +can attack any other queens placed on that chess board. This means that one queen +cannot have any other queen on its horizontal, vertical and diagonal lines. + +Solution: + +To solve this problem we will use simple math. First we know the queen can move in all +the possible ways, we can simplify it in this: vertical, horizontal, diagonal left and + diagonal right. + +We can visualize it like this: + +left diagonal = \ +right diagonal = / + +On a chessboard vertical movement could be the rows and horizontal movement could be +the columns. + +In programming we can use an array, and in this array each index could be the rows and +each value in the array could be the column. For example: + + . Q . . We have this chessboard with one queen in each column and each queen + . . . Q can't attack to each other. + Q . . . The array for this example would look like this: [1, 3, 0, 2] + . . Q . + +So if we use an array and we verify that each value in the array is different to each +other we know that at least the queens can't attack each other in horizontal and +vertical. + +At this point we have it halfway completed and we will treat the chessboard as a +Cartesian plane. Hereinafter we are going to remember basic math, so in the school we +learned this formula: + + Slope of a line: + + y2 - y1 + m = ---------- + x2 - x1 + +This formula allow us to get the slope. For the angles 45º (right diagonal) and 135º +(left diagonal) this formula gives us m = 1, and m = -1 respectively. + +See:: +https://www.enotes.com/homework-help/write-equation-line-that-hits-origin-45-degree-1474860 + +Then we have this other formula: + +Slope intercept: + +y = mx + b + +b is where the line crosses the Y axis (to get more information see: +https://www.mathsisfun.com/y_intercept.html), if we change the formula to solve for b +we would have: + +y - mx = b + +And since we already have the m values for the angles 45º and 135º, this formula would +look like this: + +45º: y - (1)x = b +45º: y - x = b + +135º: y - (-1)x = b +135º: y + x = b + +y = row +x = column + +Applying these two formulas we can check if a queen in some position is being attacked +for another one or vice versa. + +""" + +from __future__ import annotations + + +def depth_first_search( + possible_board: list[int], + diagonal_right_collisions: list[int], + diagonal_left_collisions: list[int], + boards: list[list[str]], + n: int, +) -> None: + """ + >>> boards = [] + >>> depth_first_search([], [], [], boards, 4) + >>> for board in boards: + ... print(board) + ['. Q . . ', '. . . Q ', 'Q . . . ', '. . Q . '] + ['. . Q . ', 'Q . . . ', '. . . Q ', '. Q . . '] + """ + + # Get next row in the current board (possible_board) to fill it with a queen + row = len(possible_board) + + # If row is equal to the size of the board it means there are a queen in each row in + # the current board (possible_board) + if row == n: + # We convert the variable possible_board that looks like this: [1, 3, 0, 2] to + # this: ['. Q . . ', '. . . Q ', 'Q . . . ', '. . Q . '] + boards.append([". " * i + "Q " + ". " * (n - 1 - i) for i in possible_board]) + return + + # We iterate each column in the row to find all possible results in each row + for col in range(n): + # We apply that we learned previously. First we check that in the current board + # (possible_board) there are not other same value because if there is it means + # that there are a collision in vertical. Then we apply the two formulas we + # learned before: + # + # 45º: y - x = b or 45: row - col = b + # 135º: y + x = b or row + col = b. + # + # And we verify if the results of this two formulas not exist in their variables + # respectively. (diagonal_right_collisions, diagonal_left_collisions) + # + # If any or these are True it means there is a collision so we continue to the + # next value in the for loop. + if ( + col in possible_board + or row - col in diagonal_right_collisions + or row + col in diagonal_left_collisions + ): + continue + + # If it is False we call dfs function again and we update the inputs + depth_first_search( + [*possible_board, col], + [*diagonal_right_collisions, row - col], + [*diagonal_left_collisions, row + col], + boards, + n, + ) + + +def n_queens_solution(n: int) -> None: + boards: list[list[str]] = [] + depth_first_search([], [], [], boards, n) + + # Print all the boards + for board in boards: + for column in board: + print(column) + print("") + + print(len(boards), "solutions were found.") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + n_queens_solution(4) diff --git a/backtracking/power_sum.py b/backtracking/power_sum.py new file mode 100644 index 000000000000..ee2eac426ec7 --- /dev/null +++ b/backtracking/power_sum.py @@ -0,0 +1,91 @@ +""" +Problem source: https://www.hackerrank.com/challenges/the-power-sum/problem +Find the number of ways that a given integer X, can be expressed as the sum +of the Nth powers of unique, natural numbers. For example, if X=13 and N=2. +We have to find all combinations of unique squares adding up to 13. +The only solution is 2^2+3^2. Constraints: 1<=X<=1000, 2<=N<=10. +""" + + +def backtrack( + needed_sum: int, + power: int, + current_number: int, + current_sum: int, + solutions_count: int, +) -> tuple[int, int]: + """ + >>> backtrack(13, 2, 1, 0, 0) + (0, 1) + >>> backtrack(10, 2, 1, 0, 0) + (0, 1) + >>> backtrack(10, 3, 1, 0, 0) + (0, 0) + >>> backtrack(20, 2, 1, 0, 0) + (0, 1) + >>> backtrack(15, 10, 1, 0, 0) + (0, 0) + >>> backtrack(16, 2, 1, 0, 0) + (0, 1) + >>> backtrack(20, 1, 1, 0, 0) + (0, 64) + """ + if current_sum == needed_sum: + # If the sum of the powers is equal to needed_sum, then we have a solution. + solutions_count += 1 + return current_sum, solutions_count + + i_to_n = current_number**power + if current_sum + i_to_n <= needed_sum: + # If the sum of the powers is less than needed_sum, then continue adding powers. + current_sum += i_to_n + current_sum, solutions_count = backtrack( + needed_sum, power, current_number + 1, current_sum, solutions_count + ) + current_sum -= i_to_n + if i_to_n < needed_sum: + # If the power of i is less than needed_sum, then try with the next power. + current_sum, solutions_count = backtrack( + needed_sum, power, current_number + 1, current_sum, solutions_count + ) + return current_sum, solutions_count + + +def solve(needed_sum: int, power: int) -> int: + """ + >>> solve(13, 2) + 1 + >>> solve(10, 2) + 1 + >>> solve(10, 3) + 0 + >>> solve(20, 2) + 1 + >>> solve(15, 10) + 0 + >>> solve(16, 2) + 1 + >>> solve(20, 1) + Traceback (most recent call last): + ... + ValueError: Invalid input + needed_sum must be between 1 and 1000, power between 2 and 10. + >>> solve(-10, 5) + Traceback (most recent call last): + ... + ValueError: Invalid input + needed_sum must be between 1 and 1000, power between 2 and 10. + """ + if not (1 <= needed_sum <= 1000 and 2 <= power <= 10): + raise ValueError( + "Invalid input\n" + "needed_sum must be between 1 and 1000, power between 2 and 10." + ) + + return backtrack(needed_sum, power, 1, 0, 0)[1] # Return the solutions_count + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/backtracking/rat_in_maze.py b/backtracking/rat_in_maze.py new file mode 100644 index 000000000000..626c83cb4a15 --- /dev/null +++ b/backtracking/rat_in_maze.py @@ -0,0 +1,197 @@ +from __future__ import annotations + + +def solve_maze( + maze: list[list[int]], + source_row: int, + source_column: int, + destination_row: int, + destination_column: int, +) -> list[list[int]]: + """ + This method solves the "rat in maze" problem. + Parameters : + - maze: A two dimensional matrix of zeros and ones. + - source_row: The row index of the starting point. + - source_column: The column index of the starting point. + - destination_row: The row index of the destination point. + - destination_column: The column index of the destination point. + Returns: + - solution: A 2D matrix representing the solution path if it exists. + Raises: + - ValueError: If no solution exists or if the source or + destination coordinates are invalid. + Description: + This method navigates through a maze represented as an n by n matrix, + starting from a specified source cell and + aiming to reach a destination cell. + The maze consists of walls (1s) and open paths (0s). + By providing custom row and column values, the source and destination + cells can be adjusted. + >>> maze = [[0, 1, 0, 1, 1], + ... [0, 0, 0, 0, 0], + ... [1, 0, 1, 0, 1], + ... [0, 0, 1, 0, 0], + ... [1, 0, 0, 1, 0]] + >>> solve_maze(maze,0,0,len(maze)-1,len(maze)-1) # doctest: +NORMALIZE_WHITESPACE + [[0, 1, 1, 1, 1], + [0, 0, 0, 0, 1], + [1, 1, 1, 0, 1], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 0]] + + Note: + In the output maze, the zeros (0s) represent one of the possible + paths from the source to the destination. + + >>> maze = [[0, 1, 0, 1, 1], + ... [0, 0, 0, 0, 0], + ... [0, 0, 0, 0, 1], + ... [0, 0, 0, 0, 0], + ... [0, 0, 0, 0, 0]] + >>> solve_maze(maze,0,0,len(maze)-1,len(maze)-1) # doctest: +NORMALIZE_WHITESPACE + [[0, 1, 1, 1, 1], + [0, 1, 1, 1, 1], + [0, 1, 1, 1, 1], + [0, 1, 1, 1, 1], + [0, 0, 0, 0, 0]] + + >>> maze = [[0, 0, 0], + ... [0, 1, 0], + ... [1, 0, 0]] + >>> solve_maze(maze,0,0,len(maze)-1,len(maze)-1) # doctest: +NORMALIZE_WHITESPACE + [[0, 0, 0], + [1, 1, 0], + [1, 1, 0]] + + >>> maze = [[1, 0, 0], + ... [0, 1, 0], + ... [1, 0, 0]] + >>> solve_maze(maze,0,1,len(maze)-1,len(maze)-1) # doctest: +NORMALIZE_WHITESPACE + [[1, 0, 0], + [1, 1, 0], + [1, 1, 0]] + + >>> maze = [[1, 1, 0, 0, 1, 0, 0, 1], + ... [1, 0, 1, 0, 0, 1, 1, 1], + ... [0, 1, 0, 1, 0, 0, 1, 0], + ... [1, 1, 1, 0, 0, 1, 0, 1], + ... [0, 1, 0, 0, 1, 0, 1, 1], + ... [0, 0, 0, 1, 1, 1, 0, 1], + ... [0, 1, 0, 1, 0, 1, 1, 1], + ... [1, 1, 0, 0, 0, 0, 0, 1]] + >>> solve_maze(maze,0,2,len(maze)-1,2) # doctest: +NORMALIZE_WHITESPACE + [[1, 1, 0, 0, 1, 1, 1, 1], + [1, 1, 1, 0, 0, 1, 1, 1], + [1, 1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 0, 0, 1, 1, 1], + [1, 1, 0, 0, 1, 1, 1, 1], + [1, 1, 0, 1, 1, 1, 1, 1], + [1, 1, 0, 1, 1, 1, 1, 1], + [1, 1, 0, 1, 1, 1, 1, 1]] + >>> maze = [[1, 0, 0], + ... [0, 1, 1], + ... [1, 0, 1]] + >>> solve_maze(maze,0,1,len(maze)-1,len(maze)-1) + Traceback (most recent call last): + ... + ValueError: No solution exists! + + >>> maze = [[0, 0], + ... [1, 1]] + >>> solve_maze(maze,0,0,len(maze)-1,len(maze)-1) + Traceback (most recent call last): + ... + ValueError: No solution exists! + + >>> maze = [[0, 1], + ... [1, 0]] + >>> solve_maze(maze,2,0,len(maze)-1,len(maze)-1) + Traceback (most recent call last): + ... + ValueError: Invalid source or destination coordinates + + >>> maze = [[1, 0, 0], + ... [0, 1, 0], + ... [1, 0, 0]] + >>> solve_maze(maze,0,1,len(maze),len(maze)-1) + Traceback (most recent call last): + ... + ValueError: Invalid source or destination coordinates + """ + size = len(maze) + # Check if source and destination coordinates are Invalid. + if not (0 <= source_row <= size - 1 and 0 <= source_column <= size - 1) or ( + not (0 <= destination_row <= size - 1 and 0 <= destination_column <= size - 1) + ): + raise ValueError("Invalid source or destination coordinates") + # We need to create solution object to save path. + solutions = [[1 for _ in range(size)] for _ in range(size)] + solved = run_maze( + maze, source_row, source_column, destination_row, destination_column, solutions + ) + if solved: + return solutions + else: + raise ValueError("No solution exists!") + + +def run_maze( + maze: list[list[int]], + i: int, + j: int, + destination_row: int, + destination_column: int, + solutions: list[list[int]], +) -> bool: + """ + This method is recursive starting from (i, j) and going in one of four directions: + up, down, left, right. + If a path is found to destination it returns True otherwise it returns False. + Parameters + maze: A two dimensional matrix of zeros and ones. + i, j : coordinates of matrix + solutions: A two dimensional matrix of solutions. + Returns: + Boolean if path is found True, Otherwise False. + """ + size = len(maze) + # Final check point. + if i == destination_row and j == destination_column and maze[i][j] == 0: + solutions[i][j] = 0 + return True + + lower_flag = (not i < 0) and (not j < 0) # Check lower bounds + upper_flag = (i < size) and (j < size) # Check upper bounds + + if lower_flag and upper_flag: + # check for already visited and block points. + block_flag = (solutions[i][j]) and (not maze[i][j]) + if block_flag: + # check visited + solutions[i][j] = 0 + + # check for directions + if ( + run_maze(maze, i + 1, j, destination_row, destination_column, solutions) + or run_maze( + maze, i, j + 1, destination_row, destination_column, solutions + ) + or run_maze( + maze, i - 1, j, destination_row, destination_column, solutions + ) + or run_maze( + maze, i, j - 1, destination_row, destination_column, solutions + ) + ): + return True + + solutions[i][j] = 1 + return False + return False + + +if __name__ == "__main__": + import doctest + + doctest.testmod(optionflags=doctest.NORMALIZE_WHITESPACE) diff --git a/backtracking/sudoku.py b/backtracking/sudoku.py new file mode 100644 index 000000000000..cabeebb90433 --- /dev/null +++ b/backtracking/sudoku.py @@ -0,0 +1,133 @@ +""" +Given a partially filled 9x9 2D array, the objective is to fill a 9x9 +square grid with digits numbered 1 to 9, so that every row, column, and +and each of the nine 3x3 sub-grids contains all of the digits. + +This can be solved using Backtracking and is similar to n-queens. +We check to see if a cell is safe or not and recursively call the +function on the next column to see if it returns True. if yes, we +have solved the puzzle. else, we backtrack and place another number +in that cell and repeat this process. +""" + +from __future__ import annotations + +Matrix = list[list[int]] + +# assigning initial values to the grid +initial_grid: Matrix = [ + [3, 0, 6, 5, 0, 8, 4, 0, 0], + [5, 2, 0, 0, 0, 0, 0, 0, 0], + [0, 8, 7, 0, 0, 0, 0, 3, 1], + [0, 0, 3, 0, 1, 0, 0, 8, 0], + [9, 0, 0, 8, 6, 3, 0, 0, 5], + [0, 5, 0, 0, 9, 0, 6, 0, 0], + [1, 3, 0, 0, 0, 0, 2, 5, 0], + [0, 0, 0, 0, 0, 0, 0, 7, 4], + [0, 0, 5, 2, 0, 6, 3, 0, 0], +] + +# a grid with no solution +no_solution: Matrix = [ + [5, 0, 6, 5, 0, 8, 4, 0, 3], + [5, 2, 0, 0, 0, 0, 0, 0, 2], + [1, 8, 7, 0, 0, 0, 0, 3, 1], + [0, 0, 3, 0, 1, 0, 0, 8, 0], + [9, 0, 0, 8, 6, 3, 0, 0, 5], + [0, 5, 0, 0, 9, 0, 6, 0, 0], + [1, 3, 0, 0, 0, 0, 2, 5, 0], + [0, 0, 0, 0, 0, 0, 0, 7, 4], + [0, 0, 5, 2, 0, 6, 3, 0, 0], +] + + +def is_safe(grid: Matrix, row: int, column: int, n: int) -> bool: + """ + This function checks the grid to see if each row, + column, and the 3x3 subgrids contain the digit 'n'. + It returns False if it is not 'safe' (a duplicate digit + is found) else returns True if it is 'safe' + """ + for i in range(9): + if n in {grid[row][i], grid[i][column]}: + return False + + for i in range(3): + for j in range(3): + if grid[(row - row % 3) + i][(column - column % 3) + j] == n: + return False + + return True + + +def find_empty_location(grid: Matrix) -> tuple[int, int] | None: + """ + This function finds an empty location so that we can assign a number + for that particular row and column. + """ + for i in range(9): + for j in range(9): + if grid[i][j] == 0: + return i, j + return None + + +def sudoku(grid: Matrix) -> Matrix | None: + """ + Takes a partially filled-in grid and attempts to assign values to + all unassigned locations in such a way to meet the requirements + for Sudoku solution (non-duplication across rows, columns, and boxes) + + >>> sudoku(initial_grid) # doctest: +NORMALIZE_WHITESPACE + [[3, 1, 6, 5, 7, 8, 4, 9, 2], + [5, 2, 9, 1, 3, 4, 7, 6, 8], + [4, 8, 7, 6, 2, 9, 5, 3, 1], + [2, 6, 3, 4, 1, 5, 9, 8, 7], + [9, 7, 4, 8, 6, 3, 1, 2, 5], + [8, 5, 1, 7, 9, 2, 6, 4, 3], + [1, 3, 8, 9, 4, 7, 2, 5, 6], + [6, 9, 2, 3, 5, 1, 8, 7, 4], + [7, 4, 5, 2, 8, 6, 3, 1, 9]] + >>> sudoku(no_solution) is None + True + """ + if location := find_empty_location(grid): + row, column = location + else: + # If the location is ``None``, then the grid is solved. + return grid + + for digit in range(1, 10): + if is_safe(grid, row, column, digit): + grid[row][column] = digit + + if sudoku(grid) is not None: + return grid + + grid[row][column] = 0 + + return None + + +def print_solution(grid: Matrix) -> None: + """ + A function to print the solution in the form + of a 9x9 grid + """ + for row in grid: + for cell in row: + print(cell, end=" ") + print() + + +if __name__ == "__main__": + # make a copy of grid so that you can compare with the unmodified grid + for example_grid in (initial_grid, no_solution): + print("\nExample grid:\n" + "=" * 20) + print_solution(example_grid) + print("\nExample grid solution:") + solution = sudoku(example_grid) + if solution is not None: + print_solution(solution) + else: + print("Cannot find a solution.") diff --git a/backtracking/sum_of_subsets.py b/backtracking/sum_of_subsets.py new file mode 100644 index 000000000000..f26f179f8725 --- /dev/null +++ b/backtracking/sum_of_subsets.py @@ -0,0 +1,81 @@ +""" +The sum-of-subsets problem states that a set of non-negative integers, and a +value M, determine all possible subsets of the given set whose summation sum +equal to given M. + +Summation of the chosen numbers must be equal to given number M and one number +can be used only once. +""" + + +def generate_sum_of_subsets_solutions(nums: list[int], max_sum: int) -> list[list[int]]: + """ + The main function. For list of numbers 'nums' find the subsets with sum + equal to 'max_sum' + + >>> generate_sum_of_subsets_solutions(nums=[3, 34, 4, 12, 5, 2], max_sum=9) + [[3, 4, 2], [4, 5]] + >>> generate_sum_of_subsets_solutions(nums=[3, 34, 4, 12, 5, 2], max_sum=3) + [[3]] + >>> generate_sum_of_subsets_solutions(nums=[3, 34, 4, 12, 5, 2], max_sum=1) + [] + """ + + result: list[list[int]] = [] + path: list[int] = [] + num_index = 0 + remaining_nums_sum = sum(nums) + create_state_space_tree(nums, max_sum, num_index, path, result, remaining_nums_sum) + return result + + +def create_state_space_tree( + nums: list[int], + max_sum: int, + num_index: int, + path: list[int], + result: list[list[int]], + remaining_nums_sum: int, +) -> None: + """ + Creates a state space tree to iterate through each branch using DFS. + It terminates the branching of a node when any of the two conditions + given below satisfy. + This algorithm follows depth-fist-search and backtracks when the node is not + branchable. + + >>> path = [] + >>> result = [] + >>> create_state_space_tree( + ... nums=[1], + ... max_sum=1, + ... num_index=0, + ... path=path, + ... result=result, + ... remaining_nums_sum=1) + >>> path + [] + >>> result + [[1]] + """ + + if sum(path) > max_sum or (remaining_nums_sum + sum(path)) < max_sum: + return + if sum(path) == max_sum: + result.append(path) + return + for index in range(num_index, len(nums)): + create_state_space_tree( + nums, + max_sum, + index + 1, + [*path, nums[index]], + result, + remaining_nums_sum - nums[index], + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/backtracking/word_break.py b/backtracking/word_break.py new file mode 100644 index 000000000000..2e874a02b61c --- /dev/null +++ b/backtracking/word_break.py @@ -0,0 +1,74 @@ +""" +Word Break Problem is a well-known problem in computer science. +Given a string and a dictionary of words, the task is to determine if +the string can be segmented into a sequence of one or more dictionary words. + +Wikipedia: https://en.wikipedia.org/wiki/Word_break_problem +""" + + +def backtrack(input_string: str, word_dict: set[str], start: int) -> bool: + """ + Helper function that uses backtracking to determine if a valid + word segmentation is possible starting from index 'start'. + + Parameters: + input_string (str): The input string to be segmented. + word_dict (set[str]): A set of valid dictionary words. + start (int): The starting index of the substring to be checked. + + Returns: + bool: True if a valid segmentation is possible, otherwise False. + + Example: + >>> backtrack("leetcode", {"leet", "code"}, 0) + True + + >>> backtrack("applepenapple", {"apple", "pen"}, 0) + True + + >>> backtrack("catsandog", {"cats", "dog", "sand", "and", "cat"}, 0) + False + """ + + # Base case: if the starting index has reached the end of the string + if start == len(input_string): + return True + + # Try every possible substring from 'start' to 'end' + for end in range(start + 1, len(input_string) + 1): + if input_string[start:end] in word_dict and backtrack( + input_string, word_dict, end + ): + return True + + return False + + +def word_break(input_string: str, word_dict: set[str]) -> bool: + """ + Determines if the input string can be segmented into a sequence of + valid dictionary words using backtracking. + + Parameters: + input_string (str): The input string to segment. + word_dict (set[str]): The set of valid words. + + Returns: + bool: True if the string can be segmented into valid words, otherwise False. + + Example: + >>> word_break("leetcode", {"leet", "code"}) + True + + >>> word_break("applepenapple", {"apple", "pen"}) + True + + >>> word_break("catsandog", {"cats", "dog", "sand", "and", "cat"}) + False + + >>> word_break("applepenapple", {}) + False + """ + + return backtrack(input_string, word_dict, 0) diff --git a/backtracking/word_ladder.py b/backtracking/word_ladder.py new file mode 100644 index 000000000000..7d9fd00f6669 --- /dev/null +++ b/backtracking/word_ladder.py @@ -0,0 +1,100 @@ +""" +Word Ladder is a classic problem in computer science. +The problem is to transform a start word into an end word +by changing one letter at a time. +Each intermediate word must be a valid word from a given list of words. +The goal is to find a transformation sequence +from the start word to the end word. + +Wikipedia: https://en.wikipedia.org/wiki/Word_ladder +""" + +import string + + +def backtrack( + current_word: str, path: list[str], end_word: str, word_set: set[str] +) -> list[str]: + """ + Helper function to perform backtracking to find the transformation + from the current_word to the end_word. + + Parameters: + current_word (str): The current word in the transformation sequence. + path (list[str]): The list of transformations from begin_word to current_word. + end_word (str): The target word for transformation. + word_set (set[str]): The set of valid words for transformation. + + Returns: + list[str]: The list of transformations from begin_word to end_word. + Returns an empty list if there is no valid + transformation from current_word to end_word. + + Example: + >>> backtrack("hit", ["hit"], "cog", {"hot", "dot", "dog", "lot", "log", "cog"}) + ['hit', 'hot', 'dot', 'lot', 'log', 'cog'] + + >>> backtrack("hit", ["hit"], "cog", {"hot", "dot", "dog", "lot", "log"}) + [] + + >>> backtrack("lead", ["lead"], "gold", {"load", "goad", "gold", "lead", "lord"}) + ['lead', 'lead', 'load', 'goad', 'gold'] + + >>> backtrack("game", ["game"], "code", {"came", "cage", "code", "cade", "gave"}) + ['game', 'came', 'cade', 'code'] + """ + + # Base case: If the current word is the end word, return the path + if current_word == end_word: + return path + + # Try all possible single-letter transformations + for i in range(len(current_word)): + for c in string.ascii_lowercase: # Try changing each letter + transformed_word = current_word[:i] + c + current_word[i + 1 :] + if transformed_word in word_set: + word_set.remove(transformed_word) + # Recur with the new word added to the path + result = backtrack( + transformed_word, [*path, transformed_word], end_word, word_set + ) + if result: # valid transformation found + return result + word_set.add(transformed_word) # backtrack + + return [] # No valid transformation found + + +def word_ladder(begin_word: str, end_word: str, word_set: set[str]) -> list[str]: + """ + Solve the Word Ladder problem using Backtracking and return + the list of transformations from begin_word to end_word. + + Parameters: + begin_word (str): The word from which the transformation starts. + end_word (str): The target word for transformation. + word_list (list[str]): The list of valid words for transformation. + + Returns: + list[str]: The list of transformations from begin_word to end_word. + Returns an empty list if there is no valid transformation. + + Example: + >>> word_ladder("hit", "cog", ["hot", "dot", "dog", "lot", "log", "cog"]) + ['hit', 'hot', 'dot', 'lot', 'log', 'cog'] + + >>> word_ladder("hit", "cog", ["hot", "dot", "dog", "lot", "log"]) + [] + + >>> word_ladder("lead", "gold", ["load", "goad", "gold", "lead", "lord"]) + ['lead', 'lead', 'load', 'goad', 'gold'] + + >>> word_ladder("game", "code", ["came", "cage", "code", "cade", "gave"]) + ['game', 'came', 'cade', 'code'] + """ + + if end_word not in word_set: # no valid transformation possible + return [] + + # Perform backtracking starting from the begin_word + return backtrack(begin_word, [begin_word], end_word, word_set) diff --git a/backtracking/word_search.py b/backtracking/word_search.py new file mode 100644 index 000000000000..b6ee1211eb07 --- /dev/null +++ b/backtracking/word_search.py @@ -0,0 +1,249 @@ +""" +Author : Alexander Pantyukhin +Date : November 24, 2022 + +Task: +Given an m x n grid of characters board and a string word, +return true if word exists in the grid. + +The word can be constructed from letters of sequentially adjacent cells, +where adjacent cells are horizontally or vertically neighboring. +The same letter cell may not be used more than once. + +Example: + +Matrix: +--------- +|A|B|C|E| +|S|F|C|S| +|A|D|E|E| +--------- + +Word: +"ABCCED" + +Result: +True + +Implementation notes: Use a backtracking approach. +At each point, check all neighbors to try to find the next letter of the word. + +leetcode: https://leetcode.com/problems/word-search/ + +""" + + +def get_point_key(len_board: int, len_board_column: int, row: int, column: int) -> int: + """ + Returns the hash key of matrix indexes. + + >>> get_point_key(10, 20, 1, 0) + 200 + """ + + return len_board * len_board_column * row + column + + +def exits_word( + board: list[list[str]], + word: str, + row: int, + column: int, + word_index: int, + visited_points_set: set[int], +) -> bool: + """ + Return True if it's possible to search for the word suffix + starting from the word_index. + + >>> exits_word([["A"]], "B", 0, 0, 0, set()) + False + """ + + if board[row][column] != word[word_index]: + return False + + if word_index == len(word) - 1: + return True + + traverts_directions = [(0, 1), (0, -1), (-1, 0), (1, 0)] + len_board = len(board) + len_board_column = len(board[0]) + for direction in traverts_directions: + next_i = row + direction[0] + next_j = column + direction[1] + if not (0 <= next_i < len_board and 0 <= next_j < len_board_column): + continue + + key = get_point_key(len_board, len_board_column, next_i, next_j) + if key in visited_points_set: + continue + + visited_points_set.add(key) + if exits_word(board, word, next_i, next_j, word_index + 1, visited_points_set): + return True + + visited_points_set.remove(key) + + return False + + +def validate_board_and_word(board: list[list[str]], word: str) -> None: + """ + >>> board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]] + >>> validate_board_and_word(board, "ABCCED") + >>> validate_board_and_word(board, "SEE") + >>> validate_board_and_word(board, "ABCB") + >>> validate_board_and_word([["A"]], "A") + >>> validate_board_and_word( + ... [["B", "A", "A"], ["A", "A", "A"], ["A", "B", "A"]], "ABB" + ... ) + >>> validate_board_and_word([["A"]], 123) + Traceback (most recent call last): + ... + ValueError: The word parameter should be a string of length greater than 0. + >>> validate_board_and_word([["A"]], "") + Traceback (most recent call last): + ... + ValueError: The word parameter should be a string of length greater than 0. + >>> validate_board_and_word([[]], "AB") + Traceback (most recent call last): + ... + ValueError: The board should be a non-empty matrix of single-character strings. + >>> validate_board_and_word([], "AB") + Traceback (most recent call last): + ... + ValueError: The board should be a non-empty matrix of single-character strings. + >>> validate_board_and_word([["A"], [21]], "AB") + Traceback (most recent call last): + ... + ValueError: The board should be a non-empty matrix of single-character strings. + """ + + # Validate board + msg = "The board should be a non-empty matrix of single-character strings." + if not board or not isinstance(board, list): + raise ValueError(msg) + + for row in board: + if not row or not isinstance(row, list): + raise ValueError(msg) + + for item in row: + if not item or not isinstance(item, str): + raise ValueError(msg) + + # Validate word + if not isinstance(word, str) or len(word) == 0: + msg = "The word parameter should be a string of length greater than 0." + raise ValueError(msg) + + +def get_word_path(board: list[list[str]], word: str) -> list[tuple[int, int]] | None: + """ + Return the path of the word in the board if it exists; otherwise, return None. + + >>> board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]] + >>> get_word_path(board, "ABCCED") + [(0, 0), (0, 1), (0, 2), (1, 2), (2, 2), (2, 1)] + >>> get_word_path(board, "SEE") + [(1, 3), (2, 3), (2, 2)] + >>> get_word_path(board, "ABCB") is None + True + >>> get_word_path([["A"]], 123) + Traceback (most recent call last): + ... + ValueError: The word parameter should be a string of length greater than 0. + """ + validate_board_and_word(board, word) + rows, cols = len(board), len(board[0]) + + def backtrack( + r: int, + c: int, + index: int, + path: list[tuple[int, int]], + visited: set[tuple[int, int]], + ) -> list[tuple[int, int]] | None: + if board[r][c] != word[index]: + return None + + path.append((r, c)) + visited.add((r, c)) + + if index == len(word) - 1: + return path.copy() + + directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] + + for dr, dc in directions: + nr, nc = r + dr, c + dc + if 0 <= nr < rows and 0 <= nc < cols and (nr, nc) not in visited: + result = backtrack(nr, nc, index + 1, path, visited) + if result: + return result + + path.pop() + visited.remove((r, c)) + return None + + for i in range(rows): + for j in range(cols): + result = backtrack(i, j, 0, [], set()) + if result: + return result + + return None + + +def word_exists(board: list[list[str]], word: str) -> bool: + """ + >>> board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]] + >>> word_exists(board, "ABCCED") + True + >>> word_exists(board, "SEE") + True + >>> word_exists(board, "ABCB") + False + >>> word_exists([["A"]], "A") + True + >>> word_exists([["B", "A", "A"], ["A", "A", "A"], ["A", "B", "A"]], "ABB") + False + >>> word_exists([["A"]], 123) + Traceback (most recent call last): + ... + ValueError: The word parameter should be a string of length greater than 0. + >>> word_exists([["A"]], "") + Traceback (most recent call last): + ... + ValueError: The word parameter should be a string of length greater than 0. + >>> word_exists([[]], "AB") + Traceback (most recent call last): + ... + ValueError: The board should be a non-empty matrix of single-character strings. + >>> word_exists([], "AB") + Traceback (most recent call last): + ... + ValueError: The board should be a non-empty matrix of single-character strings. + >>> word_exists([["A"], [21]], "AB") + Traceback (most recent call last): + ... + ValueError: The board should be a non-empty matrix of single-character strings. + """ + validate_board_and_word(board, word) + len_board = len(board) + len_board_column = len(board[0]) + for i in range(len_board): + for j in range(len_board_column): + if exits_word( + board, word, i, j, 0, {get_point_key(len_board, len_board_column, i, j)} + ): + return True + + return False + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/README.md b/bit_manipulation/README.md new file mode 100644 index 000000000000..3f5e028beb8e --- /dev/null +++ b/bit_manipulation/README.md @@ -0,0 +1,11 @@ +# Bit manipulation + +Bit manipulation is the act of manipulating bits to detect errors (hamming code), encrypts and decrypts messages (more on that in the 'ciphers' folder) or just do anything at the lowest level of your computer. + +* +* +* +* +* +* +* diff --git a/bit_manipulation/__init__.py b/bit_manipulation/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/bit_manipulation/binary_and_operator.py b/bit_manipulation/binary_and_operator.py new file mode 100644 index 000000000000..18761424c9b4 --- /dev/null +++ b/bit_manipulation/binary_and_operator.py @@ -0,0 +1,90 @@ +"""Bitwise AND helper. + +Return a zero-padded binary string representing ``a & b`` where the width is the +maximum bit length of the inputs. Only non-negative integers are accepted. + +>>> binary_and(25, 32) +'0b000000' +>>> binary_and(37, 50) +'0b100000' +>>> binary_and(21, 30) +'0b10100' +>>> binary_and(58, 73) +'0b0001000' +>>> binary_and(0, 255) +'0b00000000' +>>> binary_and(256, 256) +'0b100000000' + +Invalid inputs raise clear exceptions: + +>>> binary_and(0, -1) +Traceback (most recent call last): + ... +ValueError: inputs must be non-negative integers +>>> binary_and(0, 1.1) +Traceback (most recent call last): + ... +TypeError: inputs must be integers +>>> binary_and("0", "1") +Traceback (most recent call last): + ... +TypeError: inputs must be integers +""" + +# https://www.tutorialspoint.com/python3/bitwise_operators_example.htm + + +def binary_and(a: int, b: int) -> str: + """ + Take in 2 integers, convert them to binary, + return a binary number that is the + result of a binary and operation on the integers provided. + + >>> binary_and(25, 32) + '0b000000' + >>> binary_and(37, 50) + '0b100000' + >>> binary_and(21, 30) + '0b10100' + >>> binary_and(58, 73) + '0b0001000' + >>> binary_and(0, 255) + '0b00000000' + >>> binary_and(256, 256) + '0b100000000' + >>> binary_and(0, -1) + Traceback (most recent call last): + ... + ValueError: inputs must be non-negative integers + >>> binary_and(0, 1.1) + Traceback (most recent call last): + ... + TypeError: inputs must be integers + >>> binary_and("0", "1") + Traceback (most recent call last): + ... + TypeError: inputs must be integers + >>> binary_and(10, "1") + Traceback (most recent call last): + ... + TypeError: inputs must be integers + """ + if not isinstance(a, int) or not isinstance(b, int): + raise TypeError("inputs must be integers") + if a < 0 or b < 0: + raise ValueError("inputs must be non-negative integers") + + a_binary = format(a, "b") + b_binary = format(b, "b") + + max_len = max(len(a_binary), len(b_binary)) + + max_len = max(a.bit_length(), b.bit_length()) + return f"0b{(a & b):0{max_len}b}" + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/binary_coded_decimal.py b/bit_manipulation/binary_coded_decimal.py new file mode 100644 index 000000000000..676fd6d54fc5 --- /dev/null +++ b/bit_manipulation/binary_coded_decimal.py @@ -0,0 +1,29 @@ +def binary_coded_decimal(number: int) -> str: + """ + Find binary coded decimal (bcd) of integer base 10. + Each digit of the number is represented by a 4-bit binary. + Example: + >>> binary_coded_decimal(-2) + '0b0000' + >>> binary_coded_decimal(-1) + '0b0000' + >>> binary_coded_decimal(0) + '0b0000' + >>> binary_coded_decimal(3) + '0b0011' + >>> binary_coded_decimal(2) + '0b0010' + >>> binary_coded_decimal(12) + '0b00010010' + >>> binary_coded_decimal(987) + '0b100110000111' + """ + return "0b" + "".join( + str(bin(int(digit)))[2:].zfill(4) for digit in str(max(0, number)) + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/binary_count_setbits.py b/bit_manipulation/binary_count_setbits.py new file mode 100644 index 000000000000..0244853e740b --- /dev/null +++ b/bit_manipulation/binary_count_setbits.py @@ -0,0 +1,41 @@ +def binary_count_setbits(a: int) -> int: + """ + Take in 1 integer, return a number that is + the number of 1's in binary representation of that number. + + >>> binary_count_setbits(25) + 3 + >>> binary_count_setbits(36) + 2 + >>> binary_count_setbits(16) + 1 + >>> binary_count_setbits(58) + 4 + >>> binary_count_setbits(4294967295) + 32 + >>> binary_count_setbits(0) + 0 + >>> binary_count_setbits(-10) + Traceback (most recent call last): + ... + ValueError: Input value must be a positive integer + >>> binary_count_setbits(0.8) + Traceback (most recent call last): + ... + TypeError: Input value must be a 'int' type + >>> binary_count_setbits("0") + Traceback (most recent call last): + ... + TypeError: '<' not supported between instances of 'str' and 'int' + """ + if a < 0: + raise ValueError("Input value must be a positive integer") + if isinstance(a, float): + raise TypeError("Input value must be a 'int' type") + return bin(a).count("1") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/binary_count_trailing_zeros.py b/bit_manipulation/binary_count_trailing_zeros.py new file mode 100644 index 000000000000..e989d5086c8d --- /dev/null +++ b/bit_manipulation/binary_count_trailing_zeros.py @@ -0,0 +1,55 @@ +from math import log2 + + +def binary_count_trailing_zeros(a: int) -> int: + """ + Take in 1 integer, return the number of trailing zeros in binary representation. + + >>> binary_count_trailing_zeros(25) + 0 + >>> binary_count_trailing_zeros(36) + 2 + >>> binary_count_trailing_zeros(16) + 4 + >>> binary_count_trailing_zeros(58) + 1 + >>> binary_count_trailing_zeros(4294967296) + 32 + >>> binary_count_trailing_zeros(0) + Traceback (most recent call last): + ... + ValueError: Trailing zeros for 0 are undefined + >>> binary_count_trailing_zeros(-10) + Traceback (most recent call last): + ... + ValueError: Input value must be a positive integer + >>> binary_count_trailing_zeros(0.8) + Traceback (most recent call last): + ... + TypeError: Input value must be an integer + >>> binary_count_trailing_zeros("0") + Traceback (most recent call last): + ... + TypeError: Input value must be an integer + """ + + # Type check + if not isinstance(a, int): + raise TypeError("Input value must be an integer") + + # Edge case: zero + if a == 0: + raise ValueError("Trailing zeros for 0 are undefined") + + # Negative numbers not allowed + if a < 0: + raise ValueError("Input value must be a positive integer") + + # Core logic + return int(log2(a & -a)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/binary_or_operator.py b/bit_manipulation/binary_or_operator.py new file mode 100644 index 000000000000..ced9e4d2d4a4 --- /dev/null +++ b/bit_manipulation/binary_or_operator.py @@ -0,0 +1,50 @@ +"""Bitwise OR helper. + +Return a zero-padded binary string representing ``a | b`` where the width is the +maximum bit length of the inputs. Only non-negative integers are accepted. + +>>> binary_or(25, 32) +'0b111001' +>>> binary_or(37, 50) +'0b110111' +>>> binary_or(21, 30) +'0b11111' +>>> binary_or(58, 73) +'0b1111011' +>>> binary_or(0, 255) +'0b11111111' +>>> binary_or(0, 256) +'0b100000000' + +Invalid inputs raise clear exceptions: + +>>> binary_or(0, -1) +Traceback (most recent call last): + ... +ValueError: inputs must be non-negative integers +>>> binary_or(0, 1.1) +Traceback (most recent call last): + ... +TypeError: inputs must be integers +>>> binary_or("0", "1") +Traceback (most recent call last): + ... +TypeError: inputs must be integers +""" + +# https://www.tutorialspoint.com/python3/bitwise_operators_example.htm + + +def binary_or(a: int, b: int) -> str: + if not isinstance(a, int) or not isinstance(b, int): + raise TypeError("inputs must be integers") + if a < 0 or b < 0: + raise ValueError("inputs must be non-negative integers") + max_len = max(a.bit_length(), b.bit_length()) + return f"0b{(a | b):0{max_len}b}" + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/binary_shifts.py b/bit_manipulation/binary_shifts.py new file mode 100644 index 000000000000..50dc27ffeef6 --- /dev/null +++ b/bit_manipulation/binary_shifts.py @@ -0,0 +1,109 @@ +# Information on binary shifts: +# https://docs.python.org/3/library/stdtypes.html#bitwise-operations-on-integer-types +# https://www.interviewcake.com/concept/java/bit-shift + + +def logical_left_shift(number: int, shift_amount: int) -> str: + """ + Take in 2 positive integers. + 'number' is the integer to be logically left shifted 'shift_amount' times. + i.e. (number << shift_amount) + Return the shifted binary representation. + + >>> logical_left_shift(0, 1) + '0b00' + >>> logical_left_shift(1, 1) + '0b10' + >>> logical_left_shift(1, 5) + '0b100000' + >>> logical_left_shift(17, 2) + '0b1000100' + >>> logical_left_shift(1983, 4) + '0b111101111110000' + >>> logical_left_shift(1, -1) + Traceback (most recent call last): + ... + ValueError: both inputs must be positive integers + """ + if number < 0 or shift_amount < 0: + raise ValueError("both inputs must be positive integers") + + binary_number = str(bin(number)) + binary_number += "0" * shift_amount + return binary_number + + +def logical_right_shift(number: int, shift_amount: int) -> str: + """ + Take in positive 2 integers. + 'number' is the integer to be logically right shifted 'shift_amount' times. + i.e. (number >>> shift_amount) + Return the shifted binary representation. + + >>> logical_right_shift(0, 1) + '0b0' + >>> logical_right_shift(1, 1) + '0b0' + >>> logical_right_shift(1, 5) + '0b0' + >>> logical_right_shift(17, 2) + '0b100' + >>> logical_right_shift(1983, 4) + '0b1111011' + >>> logical_right_shift(1, -1) + Traceback (most recent call last): + ... + ValueError: both inputs must be positive integers + """ + if number < 0 or shift_amount < 0: + raise ValueError("both inputs must be positive integers") + + binary_number = str(bin(number))[2:] + if shift_amount >= len(binary_number): + return "0b0" + shifted_binary_number = binary_number[: len(binary_number) - shift_amount] + return "0b" + shifted_binary_number + + +def arithmetic_right_shift(number: int, shift_amount: int) -> str: + """ + Take in 2 integers. + 'number' is the integer to be arithmetically right shifted 'shift_amount' times. + i.e. (number >> shift_amount) + Return the shifted binary representation. + + >>> arithmetic_right_shift(0, 1) + '0b00' + >>> arithmetic_right_shift(1, 1) + '0b00' + >>> arithmetic_right_shift(-1, 1) + '0b11' + >>> arithmetic_right_shift(17, 2) + '0b000100' + >>> arithmetic_right_shift(-17, 2) + '0b111011' + >>> arithmetic_right_shift(-1983, 4) + '0b111110000100' + """ + if number >= 0: # Get binary representation of positive number + binary_number = "0" + str(bin(number)).strip("-")[2:] + else: # Get binary (2's complement) representation of negative number + binary_number_length = len(bin(number)[3:]) # Find 2's complement of number + binary_number = bin(abs(number) - (1 << binary_number_length))[3:] + binary_number = ( + "1" + "0" * (binary_number_length - len(binary_number)) + binary_number + ) + + if shift_amount >= len(binary_number): + return "0b" + binary_number[0] * len(binary_number) + return ( + "0b" + + binary_number[0] * shift_amount + + binary_number[: len(binary_number) - shift_amount] + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/binary_twos_complement.py b/bit_manipulation/binary_twos_complement.py new file mode 100644 index 000000000000..2c064ec142d7 --- /dev/null +++ b/bit_manipulation/binary_twos_complement.py @@ -0,0 +1,43 @@ +# Information on 2's complement: https://en.wikipedia.org/wiki/Two%27s_complement + + +def twos_complement(number: int) -> str: + """ + Take in a negative integer 'number'. + Return the two's complement representation of 'number'. + + >>> twos_complement(0) + '0b0' + >>> twos_complement(-1) + '0b11' + >>> twos_complement(-5) + '0b1011' + >>> twos_complement(-17) + '0b101111' + >>> twos_complement(-207) + '0b100110001' + >>> twos_complement(1) + Traceback (most recent call last): + ... + ValueError: input must be a negative integer + """ + if number > 0: + raise ValueError("input must be a negative integer") + binary_number_length = len(bin(number)[3:]) + twos_complement_number = bin(abs(number) - (1 << binary_number_length))[3:] + twos_complement_number = ( + ( + "1" + + "0" * (binary_number_length - len(twos_complement_number)) + + twos_complement_number + ) + if number < 0 + else "0" + ) + return "0b" + twos_complement_number + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/binary_xor_operator.py b/bit_manipulation/binary_xor_operator.py new file mode 100644 index 000000000000..3c69fdb135c0 --- /dev/null +++ b/bit_manipulation/binary_xor_operator.py @@ -0,0 +1,51 @@ +"""Bitwise XOR helper. + +Return a zero-padded binary string representing ``a ^ b`` where the width is the +maximum bit length of the inputs. Only non-negative integers are accepted. + +>>> binary_xor(25, 32) +'0b111001' +>>> binary_xor(37, 50) +'0b010111' +>>> binary_xor(21, 30) +'0b01011' +>>> binary_xor(58, 73) +'0b1110011' +>>> binary_xor(0, 255) +'0b11111111' +>>> binary_xor(256, 256) +'0b000000000' + +Invalid inputs raise clear exceptions: + +>>> binary_xor(0, -1) +Traceback (most recent call last): + ... +ValueError: inputs must be non-negative integers +>>> binary_xor(0, 1.1) +Traceback (most recent call last): + ... +TypeError: inputs must be integers +>>> binary_xor("0", "1") +Traceback (most recent call last): + ... +TypeError: inputs must be integers +""" + +# https://www.tutorialspoint.com/python3/bitwise_operators_example.htm + + +def binary_xor(a: int, b: int) -> str: + if not isinstance(a, int) or not isinstance(b, int): + raise TypeError("inputs must be integers") + if a < 0 or b < 0: + raise ValueError("inputs must be non-negative integers") + + max_len = max(a.bit_length(), b.bit_length()) + return f"0b{(a ^ b):0{max_len}b}" + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/bitwise_addition_recursive.py b/bit_manipulation/bitwise_addition_recursive.py new file mode 100644 index 000000000000..70eaf6887b64 --- /dev/null +++ b/bit_manipulation/bitwise_addition_recursive.py @@ -0,0 +1,55 @@ +""" +Calculates the sum of two non-negative integers using bitwise operators +Wikipedia explanation: https://en.wikipedia.org/wiki/Binary_number +""" + + +def bitwise_addition_recursive(number: int, other_number: int) -> int: + """ + >>> bitwise_addition_recursive(4, 5) + 9 + >>> bitwise_addition_recursive(8, 9) + 17 + >>> bitwise_addition_recursive(0, 4) + 4 + >>> bitwise_addition_recursive(4.5, 9) + Traceback (most recent call last): + ... + TypeError: Both arguments MUST be integers! + >>> bitwise_addition_recursive('4', 9) + Traceback (most recent call last): + ... + TypeError: Both arguments MUST be integers! + >>> bitwise_addition_recursive('4.5', 9) + Traceback (most recent call last): + ... + TypeError: Both arguments MUST be integers! + >>> bitwise_addition_recursive(-1, 9) + Traceback (most recent call last): + ... + ValueError: Both arguments MUST be non-negative! + >>> bitwise_addition_recursive(1, -9) + Traceback (most recent call last): + ... + ValueError: Both arguments MUST be non-negative! + """ + + if not isinstance(number, int) or not isinstance(other_number, int): + raise TypeError("Both arguments MUST be integers!") + + if number < 0 or other_number < 0: + raise ValueError("Both arguments MUST be non-negative!") + + bitwise_sum = number ^ other_number + carry = number & other_number + + if carry == 0: + return bitwise_sum + + return bitwise_addition_recursive(bitwise_sum, carry << 1) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/count_1s_brian_kernighan_method.py b/bit_manipulation/count_1s_brian_kernighan_method.py new file mode 100644 index 000000000000..2ed81b09d675 --- /dev/null +++ b/bit_manipulation/count_1s_brian_kernighan_method.py @@ -0,0 +1,46 @@ +def get_1s_count(number: int) -> int: + """ + Count the number of set bits in a 32 bit integer using Brian Kernighan's way. + Ref - https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan + >>> get_1s_count(25) + 3 + >>> get_1s_count(37) + 3 + >>> get_1s_count(21) + 3 + >>> get_1s_count(58) + 4 + >>> get_1s_count(0) + 0 + >>> get_1s_count(256) + 1 + >>> get_1s_count(-1) + Traceback (most recent call last): + ... + ValueError: Input must be a non-negative integer + >>> get_1s_count(0.8) + Traceback (most recent call last): + ... + ValueError: Input must be a non-negative integer + >>> get_1s_count("25") + Traceback (most recent call last): + ... + ValueError: Input must be a non-negative integer + """ + if not isinstance(number, int) or number < 0: + raise ValueError("Input must be a non-negative integer") + + count = 0 + while number: + # This way we arrive at next set bit (next 1) instead of looping + # through each bit and checking for 1s hence the + # loop won't run 32 times it will only run the number of `1` times + number &= number - 1 + count += 1 + return count + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/count_number_of_one_bits.py b/bit_manipulation/count_number_of_one_bits.py new file mode 100644 index 000000000000..e9064c2745c7 --- /dev/null +++ b/bit_manipulation/count_number_of_one_bits.py @@ -0,0 +1,146 @@ +from timeit import timeit + + +def get_set_bits_count_using_brian_kernighans_algorithm(number: int) -> int: + """ + Count the number of set bits in a 32 bit integer + >>> get_set_bits_count_using_brian_kernighans_algorithm(25) + 3 + >>> get_set_bits_count_using_brian_kernighans_algorithm(37) + 3 + >>> get_set_bits_count_using_brian_kernighans_algorithm(21) + 3 + >>> get_set_bits_count_using_brian_kernighans_algorithm(58) + 4 + >>> get_set_bits_count_using_brian_kernighans_algorithm(0) + 0 + >>> get_set_bits_count_using_brian_kernighans_algorithm(256) + 1 + >>> get_set_bits_count_using_brian_kernighans_algorithm(-1) + Traceback (most recent call last): + ... + ValueError: the value of input must not be negative + >>> get_set_bits_count_using_brian_kernighans_algorithm(1023) + 10 + """ + if number < 0: + raise ValueError("the value of input must not be negative") + result = 0 + while number: + number &= number - 1 + result += 1 + return result + + +def get_set_bits_count_using_modulo_operator(number: int) -> int: + """ + Count the number of set bits in a 32 bit integer + >>> get_set_bits_count_using_modulo_operator(25) + 3 + >>> get_set_bits_count_using_modulo_operator(37) + 3 + >>> get_set_bits_count_using_modulo_operator(21) + 3 + >>> get_set_bits_count_using_modulo_operator(58) + 4 + >>> get_set_bits_count_using_modulo_operator(0) + 0 + >>> get_set_bits_count_using_modulo_operator(256) + 1 + >>> get_set_bits_count_using_modulo_operator(-1) + Traceback (most recent call last): + ... + ValueError: the value of input must not be negative + >>> get_set_bits_count_using_modulo_operator(1024) + 1 + """ + if number < 0: + raise ValueError("the value of input must not be negative") + result = 0 + while number: + if number % 2 == 1: + result += 1 + number >>= 1 + return result + + +def get_set_bits_count_using_lookup_table(number: int) -> int: + """ + Count the number of set bits in a 32-bit integer using a precomputed lookup table. + + I see similar approach in GeeksforGeeks, but the implementation is different. + Link to Code: + https://www.geeksforgeeks.org/dsa/count-set-bits-integer-using-lookup-table/ + + >>> get_set_bits_count_using_lookup_table(25) + 3 + >>> get_set_bits_count_using_lookup_table(37) + 3 + >>> get_set_bits_count_using_lookup_table(21) + 3 + >>> get_set_bits_count_using_lookup_table(58) + 4 + >>> get_set_bits_count_using_lookup_table(0) + 0 + >>> get_set_bits_count_using_lookup_table(256) + 1 + >>> get_set_bits_count_using_lookup_table(-1) + Traceback (most recent call last): + ... + ValueError: the value of input must not be negative + """ + _lookup_table = [bin(i).count("1") for i in range(256)] + + if number < 0: + raise ValueError("the value of input must not be negative") + + # Split 32-bit number into four 8-bit chunks and use lookup table + return ( + _lookup_table[number & 0xFF] + + _lookup_table[(number >> 8) & 0xFF] + + _lookup_table[(number >> 16) & 0xFF] + + _lookup_table[(number >> 24) & 0xFF] + ) + + +def benchmark() -> None: + """ + Benchmark code for comparing 3 functions, with different length int values. + Brian Kernighan's algorithm is consistently faster than using modulo_operator, + and the lookup table method is often the fastest for repeated calls. + """ + + def do_benchmark(number: int) -> None: + setup = "import __main__ as z" + print(f"Benchmark when {number = }:") + + print(f"{get_set_bits_count_using_modulo_operator(number) = }") + timing = timeit( + f"z.get_set_bits_count_using_modulo_operator({number})", setup=setup + ) + print(f"timeit() runs in {timing} seconds") + + print(f"{get_set_bits_count_using_brian_kernighans_algorithm(number) = }") + timing = timeit( + f"z.get_set_bits_count_using_brian_kernighans_algorithm({number})", + setup=setup, + ) + print(f"timeit() runs in {timing} seconds") + + print(f"{get_set_bits_count_using_lookup_table(number) = }") + timing = timeit( + f"z.get_set_bits_count_using_lookup_table({number})", + setup=setup, + ) + print(f"timeit() runs in {timing} seconds") + + for number in (25, 37, 58, 0): + do_benchmark(number) + print() + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + benchmark() diff --git a/bit_manipulation/excess_3_code.py b/bit_manipulation/excess_3_code.py new file mode 100644 index 000000000000..7beaabd90e8a --- /dev/null +++ b/bit_manipulation/excess_3_code.py @@ -0,0 +1,27 @@ +def excess_3_code(number: int) -> str: + """ + Find excess-3 code of integer base 10. + Add 3 to all digits in a decimal number then convert to a binary-coded decimal. + https://en.wikipedia.org/wiki/Excess-3 + + >>> excess_3_code(0) + '0b0011' + >>> excess_3_code(3) + '0b0110' + >>> excess_3_code(2) + '0b0101' + >>> excess_3_code(20) + '0b01010011' + >>> excess_3_code(120) + '0b010001010011' + """ + num = "" + for digit in str(max(0, number)): + num += str(bin(int(digit) + 3))[2:].zfill(4) + return "0b" + num + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/fast_walsh_hadamard_transform.py b/bit_manipulation/fast_walsh_hadamard_transform.py new file mode 100644 index 000000000000..1c7d447e3c0a --- /dev/null +++ b/bit_manipulation/fast_walsh_hadamard_transform.py @@ -0,0 +1,209 @@ +""" +Fast Walsh-Hadamard Transform (FWHT) for Bitwise Convolutions. + +Reference: https://en.wikipedia.org/wiki/Fast_Walsh%E2%80%93Hadamard_transform +Reference: https://cp-algorithms.com/algebra/walsh-hadamard-transform.html + +Computes bitwise XOR, AND, and OR convolutions of two numeric sequences in O(N log N) +time, where N is a positive power of 2. +""" + + +def fwht_xor(sequence: list[int], inverse: bool = False) -> list[int]: + """ + Perform Fast Walsh-Hadamard Transform (or inverse) for XOR operation. + + Time Complexity: O(N log N) + + >>> fwht_xor([1, 2, 3, 4]) + [10, -2, -4, 0] + >>> fwht_xor([10, -2, -4, 0], inverse=True) + [1, 2, 3, 4] + >>> fwht_xor([1, 2, 3]) + Traceback (most recent call last): + ... + ValueError: Length of sequence must be a positive power of 2. + >>> fwht_xor([]) + Traceback (most recent call last): + ... + ValueError: Length of sequence must be a positive power of 2. + >>> fwht_xor([1, 2, 3, 5], inverse=True) + Traceback (most recent call last): + ... + ValueError: Inverse XOR transform requires elements divisible by sequence length. + """ + sequence_length = len(sequence) + if sequence_length == 0 or (sequence_length & (sequence_length - 1)) != 0: + raise ValueError("Length of sequence must be a positive power of 2.") + + result = list(sequence) + half_block = 1 + while half_block < sequence_length: + block_size = half_block * 2 + for block_start in range(0, sequence_length, block_size): + for offset in range(half_block): + left_index = block_start + offset + right_index = left_index + half_block + left_val = result[left_index] + right_val = result[right_index] + result[left_index] = left_val + right_val + result[right_index] = left_val - right_val + half_block *= 2 + + if inverse: + if any(element % sequence_length != 0 for element in result): + raise ValueError( + "Inverse XOR transform requires elements divisible by sequence length." + ) + result = [element // sequence_length for element in result] + return result + + +def xor_convolution(sequence_a: list[int], sequence_b: list[int]) -> list[int]: + """ + Compute bitwise XOR convolution C[k] = sum_{i ^ j = k} (A[i] * B[j]). + + Time Complexity: O(N log N) + + >>> xor_convolution([1, 2], [3, 4]) + [11, 10] + >>> xor_convolution([1, 2], [3]) + Traceback (most recent call last): + ... + ValueError: Input sequences must have equal length. + """ + if len(sequence_a) != len(sequence_b): + raise ValueError("Input sequences must have equal length.") + + transformed_a = fwht_xor(sequence_a) + transformed_b = fwht_xor(sequence_b) + pointwise_product = [ + transformed_a[index] * transformed_b[index] for index in range(len(sequence_a)) + ] + return fwht_xor(pointwise_product, inverse=True) + + +def fwht_or(sequence: list[int], inverse: bool = False) -> list[int]: + """ + Perform Fast Walsh-Hadamard Transform for OR operation. + + Time Complexity: O(N log N) + + >>> fwht_or([1, 2]) + [1, 3] + >>> fwht_or([1, 3], inverse=True) + [1, 2] + >>> fwht_or([1, 2, 3]) + Traceback (most recent call last): + ... + ValueError: Length of sequence must be a positive power of 2. + """ + sequence_length = len(sequence) + if sequence_length == 0 or (sequence_length & (sequence_length - 1)) != 0: + raise ValueError("Length of sequence must be a positive power of 2.") + + result = list(sequence) + half_block = 1 + while half_block < sequence_length: + block_size = half_block * 2 + for block_start in range(0, sequence_length, block_size): + for offset in range(half_block): + left_index = block_start + offset + right_index = left_index + half_block + if not inverse: + result[right_index] += result[left_index] + else: + result[right_index] -= result[left_index] + half_block *= 2 + + return result + + +def or_convolution(sequence_a: list[int], sequence_b: list[int]) -> list[int]: + """ + Compute bitwise OR convolution C[k] = sum_{i | j = k} (A[i] * B[j]). + + Time Complexity: O(N log N) + + >>> or_convolution([1, 2], [3, 4]) + [3, 18] + >>> or_convolution([1, 2], [3]) + Traceback (most recent call last): + ... + ValueError: Input sequences must have equal length. + """ + if len(sequence_a) != len(sequence_b): + raise ValueError("Input sequences must have equal length.") + + transformed_a = fwht_or(sequence_a) + transformed_b = fwht_or(sequence_b) + pointwise_product = [ + transformed_a[index] * transformed_b[index] for index in range(len(sequence_a)) + ] + return fwht_or(pointwise_product, inverse=True) + + +def fwht_and(sequence: list[int], inverse: bool = False) -> list[int]: + """ + Perform Fast Walsh-Hadamard Transform for AND operation. + + Time Complexity: O(N log N) + + >>> fwht_and([1, 2]) + [3, 2] + >>> fwht_and([3, 2], inverse=True) + [1, 2] + >>> fwht_and([1, 2, 3]) + Traceback (most recent call last): + ... + ValueError: Length of sequence must be a positive power of 2. + """ + sequence_length = len(sequence) + if sequence_length == 0 or (sequence_length & (sequence_length - 1)) != 0: + raise ValueError("Length of sequence must be a positive power of 2.") + + result = list(sequence) + half_block = 1 + while half_block < sequence_length: + block_size = half_block * 2 + for block_start in range(0, sequence_length, block_size): + for offset in range(half_block): + left_index = block_start + offset + right_index = left_index + half_block + if not inverse: + result[left_index] += result[right_index] + else: + result[left_index] -= result[right_index] + half_block *= 2 + + return result + + +def and_convolution(sequence_a: list[int], sequence_b: list[int]) -> list[int]: + """ + Compute bitwise AND convolution C[k] = sum_{i & j = k} (A[i] * B[j]). + + Time Complexity: O(N log N) + + >>> and_convolution([1, 2], [3, 4]) + [13, 8] + >>> and_convolution([1, 2], [3]) + Traceback (most recent call last): + ... + ValueError: Input sequences must have equal length. + """ + if len(sequence_a) != len(sequence_b): + raise ValueError("Input sequences must have equal length.") + + transformed_a = fwht_and(sequence_a) + transformed_b = fwht_and(sequence_b) + pointwise_product = [ + transformed_a[index] * transformed_b[index] for index in range(len(sequence_a)) + ] + return fwht_and(pointwise_product, inverse=True) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/find_previous_power_of_two.py b/bit_manipulation/find_previous_power_of_two.py new file mode 100644 index 000000000000..8ac74ac98478 --- /dev/null +++ b/bit_manipulation/find_previous_power_of_two.py @@ -0,0 +1,30 @@ +def find_previous_power_of_two(number: int) -> int: + """ + Find the largest power of two that is less than or equal to a given integer. + https://stackoverflow.com/questions/1322510 + + >>> [find_previous_power_of_two(i) for i in range(18)] + [0, 1, 2, 2, 4, 4, 4, 4, 8, 8, 8, 8, 8, 8, 8, 8, 16, 16] + >>> find_previous_power_of_two(-5) + Traceback (most recent call last): + ... + ValueError: Input must be a non-negative integer + >>> find_previous_power_of_two(10.5) + Traceback (most recent call last): + ... + ValueError: Input must be a non-negative integer + """ + if not isinstance(number, int) or number < 0: + raise ValueError("Input must be a non-negative integer") + if number == 0: + return 0 + power = 1 + while power <= number: + power <<= 1 # Equivalent to multiplying by 2 + return power >> 1 if number > 1 else 1 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/find_unique_number.py b/bit_manipulation/find_unique_number.py new file mode 100644 index 000000000000..77970b4865d1 --- /dev/null +++ b/bit_manipulation/find_unique_number.py @@ -0,0 +1,37 @@ +def find_unique_number(arr: list[int]) -> int: + """ + Given a list of integers where every element appears twice except for one, + this function returns the element that appears only once using bitwise XOR. + + >>> find_unique_number([1, 1, 2, 2, 3]) + 3 + >>> find_unique_number([4, 5, 4, 6, 6]) + 5 + >>> find_unique_number([7]) + 7 + >>> find_unique_number([10, 20, 10]) + 20 + >>> find_unique_number([]) + Traceback (most recent call last): + ... + ValueError: input list must not be empty + >>> find_unique_number([1, 'a', 1]) + Traceback (most recent call last): + ... + TypeError: all elements must be integers + """ + if not arr: + raise ValueError("input list must not be empty") + if not all(isinstance(x, int) for x in arr): + raise TypeError("all elements must be integers") + + result = 0 + for num in arr: + result ^= num + return result + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/gray_code_sequence.py b/bit_manipulation/gray_code_sequence.py new file mode 100644 index 000000000000..ab66eb7b1e7c --- /dev/null +++ b/bit_manipulation/gray_code_sequence.py @@ -0,0 +1,95 @@ +def gray_code(bit_count: int) -> list[int]: + """ + Takes in an integer n and returns a n-bit + gray code sequence + An n-bit gray code sequence is a sequence of 2^n + integers where: + + a) Every integer is between [0,2^n -1] inclusive + b) The sequence begins with 0 + c) An integer appears at most one time in the sequence + d) The binary representation of every pair of integers differ + by exactly one bit + e) The binary representation of first and last bit also + differ by exactly one bit + + >>> gray_code(0) + [0] + + >>> gray_code(2) + [0, 1, 3, 2] + + >>> gray_code(1) + [0, 1] + + >>> gray_code(3) + [0, 1, 3, 2, 6, 7, 5, 4] + + >>> gray_code(-1) + Traceback (most recent call last): + ... + ValueError: The given input must be positive + + >>> gray_code(10.6) + Traceback (most recent call last): + ... + TypeError: unsupported operand type(s) for <<: 'int' and 'float' + """ + + # bit count represents no. of bits in the gray code + if bit_count < 0: + raise ValueError("The given input must be positive") + + # get the generated string sequence and convert them to integers + sequence = gray_code_sequence_string(bit_count) + return [int(code, 2) for code in sequence] + + +def gray_code_sequence_string(bit_count: int) -> list[str]: + """ + Will output the n-bit Gray code sequence as a + string of bits + + >>> gray_code_sequence_string(0) + ['0'] + + >>> gray_code_sequence_string(2) + ['00', '01', '11', '10'] + + >>> gray_code_sequence_string(1) + ['0', '1'] + """ + + # The approach is a recursive one + # Base case achieved when either n = 0 or n=1 + if bit_count == 0: + return ["0"] + + if bit_count == 1: + return ["0", "1"] + + seq_len = 1 << bit_count # defines the length of the sequence + # 1<< n is equivalent to 2^n + + # recursive answer will generate answer for n-1 bits + smaller_sequence = gray_code_sequence_string(bit_count - 1) + + sequence: list[str] = [] + + # append 0 to first half of the smaller sequence generated + for i in range(seq_len // 2): + generated_no = "0" + smaller_sequence[i] + sequence.append(generated_no) + + # append 1 to second half ... start from the end of the list + for i in reversed(range(seq_len // 2)): + generated_no = "1" + smaller_sequence[i] + sequence.append(generated_no) + + return sequence + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/highest_set_bit.py b/bit_manipulation/highest_set_bit.py new file mode 100644 index 000000000000..21d92dcb9492 --- /dev/null +++ b/bit_manipulation/highest_set_bit.py @@ -0,0 +1,34 @@ +def get_highest_set_bit_position(number: int) -> int: + """ + Returns position of the highest set bit of a number. + Ref - https://graphics.stanford.edu/~seander/bithacks.html#IntegerLogObvious + >>> get_highest_set_bit_position(25) + 5 + >>> get_highest_set_bit_position(37) + 6 + >>> get_highest_set_bit_position(1) + 1 + >>> get_highest_set_bit_position(4) + 3 + >>> get_highest_set_bit_position(0) + 0 + >>> get_highest_set_bit_position(0.8) + Traceback (most recent call last): + ... + TypeError: Input value must be an 'int' type + """ + if not isinstance(number, int): + raise TypeError("Input value must be an 'int' type") + + position = 0 + while number: + position += 1 + number >>= 1 + + return position + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/index_of_rightmost_set_bit.py b/bit_manipulation/index_of_rightmost_set_bit.py new file mode 100644 index 000000000000..c9c911660b08 --- /dev/null +++ b/bit_manipulation/index_of_rightmost_set_bit.py @@ -0,0 +1,51 @@ +# Reference: https://www.geeksforgeeks.org/position-of-rightmost-set-bit/ + + +def get_index_of_rightmost_set_bit(number: int) -> int: + """ + Take in a positive integer 'number'. + Returns the zero-based index of first set bit in that 'number' from right. + Returns -1, If no set bit found. + + >>> get_index_of_rightmost_set_bit(0) + -1 + >>> get_index_of_rightmost_set_bit(5) + 0 + >>> get_index_of_rightmost_set_bit(36) + 2 + >>> get_index_of_rightmost_set_bit(8) + 3 + >>> get_index_of_rightmost_set_bit(-18) + Traceback (most recent call last): + ... + ValueError: Input must be a non-negative integer + >>> get_index_of_rightmost_set_bit('test') + Traceback (most recent call last): + ... + ValueError: Input must be a non-negative integer + >>> get_index_of_rightmost_set_bit(1.25) + Traceback (most recent call last): + ... + ValueError: Input must be a non-negative integer + """ + + if not isinstance(number, int) or number < 0: + raise ValueError("Input must be a non-negative integer") + + intermediate = number & ~(number - 1) + index = 0 + while intermediate: + intermediate >>= 1 + index += 1 + return index - 1 + + +if __name__ == "__main__": + """ + Finding the index of rightmost set bit has some very peculiar use-cases, + especially in finding missing or/and repeating numbers in a list of + positive integers. + """ + import doctest + + doctest.testmod(verbose=True) diff --git a/bit_manipulation/is_even.py b/bit_manipulation/is_even.py new file mode 100644 index 000000000000..a87988a2c3f8 --- /dev/null +++ b/bit_manipulation/is_even.py @@ -0,0 +1,75 @@ +def is_even(number: int) -> bool: + """Return True if the input integer is even using a bitwise check. + + Explanation: + In binary, even numbers always have the least significant bit cleared (0), + while odd numbers have it set (1). Therefore, ``n & 1 == 0`` implies even. + + >>> is_even(1) + False + >>> is_even(4) + True + >>> is_even(9) + False + >>> is_even(15) + False + >>> is_even(40) + True + >>> is_even(100) + True + >>> is_even(101) + False + >>> is_even(True) + Traceback (most recent call last): + ... + TypeError: input must be an integer + >>> is_even(3.14) + Traceback (most recent call last): + ... + TypeError: input must be an integer + """ + if not isinstance(number, int) or isinstance(number, bool): + # bool is a subclass of int; explicitly disallow it as a number here. + raise TypeError("input must be an integer") + return (number & 1) == 0 + + +def is_even_using_shift_operator(number: int) -> bool: + """ + Returns True if the input integer is even. + + Explanation: + In binary, even numbers end with 0, odd numbers end with 1. + Examples: + 2 -> 10 + 3 -> 11 + 4 -> 100 + 5 -> 101 + + For odd numbers, the last bit is always 1. + Using shift: + (n >> 1) << 1 removes the last bit. + If result equals n, n is even. + + >>> is_even_using_shift_operator(1) + False + >>> is_even_using_shift_operator(4) + True + >>> is_even_using_shift_operator(9) + False + >>> is_even_using_shift_operator(15) + False + >>> is_even_using_shift_operator(40) + True + >>> is_even_using_shift_operator(100) + True + >>> is_even_using_shift_operator(101) + False + """ + return (number >> 1) << 1 == number + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/is_power_of_two.py b/bit_manipulation/is_power_of_two.py new file mode 100644 index 000000000000..3dd8df3540bc --- /dev/null +++ b/bit_manipulation/is_power_of_two.py @@ -0,0 +1,61 @@ +""" +Author : Alexander Pantyukhin +Date : November 1, 2022 + +Task: +Given a positive int number. Return True if this number is power of 2 +or False otherwise. + +Implementation notes: Use bit manipulation. +For example if the number is the power of two it's bits representation: +n = 0..100..00 +n - 1 = 0..011..11 + +n & (n - 1) - no intersections = 0 +""" + + +def is_power_of_two(number: int) -> bool: + """ + Return True if this number is power of 2 or False otherwise. + + >>> is_power_of_two(0) + True + >>> is_power_of_two(1) + True + >>> is_power_of_two(2) + True + >>> is_power_of_two(4) + True + >>> is_power_of_two(6) + False + >>> is_power_of_two(8) + True + >>> is_power_of_two(17) + False + >>> is_power_of_two(1024) + True + >>> is_power_of_two(1023) + False + >>> is_power_of_two(-1) + Traceback (most recent call last): + ... + ValueError: number must not be negative + >>> is_power_of_two(1.2) + Traceback (most recent call last): + ... + TypeError: unsupported operand type(s) for &: 'float' and 'float' + + # Test all powers of 2 from 0 to 10,000 + >>> all(is_power_of_two(int(2 ** i)) for i in range(10000)) + True + """ + if number < 0: + raise ValueError("number must not be negative") + return number & (number - 1) == 0 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/largest_pow_of_two_le_num.py b/bit_manipulation/largest_pow_of_two_le_num.py new file mode 100644 index 000000000000..6ef827312199 --- /dev/null +++ b/bit_manipulation/largest_pow_of_two_le_num.py @@ -0,0 +1,60 @@ +""" +Author : Naman Sharma +Date : October 2, 2023 + +Task: +To Find the largest power of 2 less than or equal to a given number. + +Implementation notes: Use bit manipulation. +We start from 1 & left shift the set bit to check if (res<<1)<=number. +Each left bit shift represents a pow of 2. + +For example: +number: 15 +res: 1 0b1 + 2 0b10 + 4 0b100 + 8 0b1000 + 16 0b10000 (Exit) +""" + + +def largest_pow_of_two_le_num(number: int) -> int: + """ + Return the largest power of two less than or equal to a number. + + >>> largest_pow_of_two_le_num(0) + 0 + >>> largest_pow_of_two_le_num(1) + 1 + >>> largest_pow_of_two_le_num(-1) + 0 + >>> largest_pow_of_two_le_num(3) + 2 + >>> largest_pow_of_two_le_num(15) + 8 + >>> largest_pow_of_two_le_num(99) + 64 + >>> largest_pow_of_two_le_num(178) + 128 + >>> largest_pow_of_two_le_num(999999) + 524288 + >>> largest_pow_of_two_le_num(99.9) + Traceback (most recent call last): + ... + TypeError: Input value must be a 'int' type + """ + if isinstance(number, float): + raise TypeError("Input value must be a 'int' type") + if number <= 0: + return 0 + res = 1 + while (res << 1) <= number: + res <<= 1 + return res + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/missing_number.py b/bit_manipulation/missing_number.py new file mode 100644 index 000000000000..554887b17562 --- /dev/null +++ b/bit_manipulation/missing_number.py @@ -0,0 +1,40 @@ +def find_missing_number(nums: list[int]) -> int: + """ + Finds the missing number in a list of consecutive integers. + + Args: + nums: A list of integers. + + Returns: + The missing number. + + Example: + >>> find_missing_number([0, 1, 3, 4]) + 2 + >>> find_missing_number([4, 3, 1, 0]) + 2 + >>> find_missing_number([-4, -3, -1, 0]) + -2 + >>> find_missing_number([-2, 2, 1, 3, 0]) + -1 + >>> find_missing_number([1, 3, 4, 5, 6]) + 2 + >>> find_missing_number([6, 5, 4, 2, 1]) + 3 + >>> find_missing_number([6, 1, 5, 3, 4]) + 2 + """ + low = min(nums) + high = max(nums) + missing_number = high + + for i in range(low, high): + missing_number ^= i ^ nums[i - low] + + return missing_number + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/next_power_of_two.py b/bit_manipulation/next_power_of_two.py new file mode 100644 index 000000000000..4b523c17b130 --- /dev/null +++ b/bit_manipulation/next_power_of_two.py @@ -0,0 +1,55 @@ +""" +Author : Basuki Nath +Date : 2025-10-04 + +Utility to compute the next power of two greater than or equal to n. +""" + + +def next_power_of_two(n: int) -> int: + """ + Return the smallest power of two >= n for positive integers. + + >>> next_power_of_two(1) + 1 + >>> next_power_of_two(2) + 2 + >>> next_power_of_two(3) + 4 + >>> next_power_of_two(5) + 8 + >>> next_power_of_two(1025) + 2048 + >>> next_power_of_two(0) + Traceback (most recent call last): + ... + ValueError: n must be positive + >>> next_power_of_two(-3) + Traceback (most recent call last): + ... + ValueError: n must be positive + """ + if n <= 0: + raise ValueError("n must be positive") + # If already a power of two, return it + if n & (n - 1) == 0: + return n + # Otherwise, fill bits to the right + v = n - 1 + v |= v >> 1 + v |= v >> 2 + v |= v >> 4 + v |= v >> 8 + v |= v >> 16 + # for very large ints, continue shifting using Python's arbitrary precision + shift = 32 + while (1 << shift) <= v: + v |= v >> shift + shift <<= 1 + return v + 1 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/numbers_different_signs.py b/bit_manipulation/numbers_different_signs.py new file mode 100644 index 000000000000..cf8b6d86f1eb --- /dev/null +++ b/bit_manipulation/numbers_different_signs.py @@ -0,0 +1,39 @@ +""" +Author : Alexander Pantyukhin +Date : November 30, 2022 + +Task: +Given two int numbers. Return True these numbers have opposite signs +or False otherwise. + +Implementation notes: Use bit manipulation. +Use XOR for two numbers. +""" + + +def different_signs(num1: int, num2: int) -> bool: + """ + Return True if numbers have opposite signs False otherwise. + + >>> different_signs(1, -1) + True + >>> different_signs(1, 1) + False + >>> different_signs(1000000000000000000000000000, -1000000000000000000000000000) + True + >>> different_signs(-1000000000000000000000000000, 1000000000000000000000000000) + True + >>> different_signs(50, 278) + False + >>> different_signs(0, 2) + False + >>> different_signs(2, 0) + False + """ + return num1 ^ num2 < 0 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/parity.py b/bit_manipulation/parity.py new file mode 100644 index 000000000000..019c88deea30 --- /dev/null +++ b/bit_manipulation/parity.py @@ -0,0 +1,43 @@ +""" +Author : Basuki Nath +Date : 2025-10-04 + +Simple parity utility for integers. + +The parity is 1 when the number of set bits is odd, otherwise 0. +""" + + +def parity(number: int) -> int: + """ + Return 1 if `number` has an odd number of set bits, otherwise 0. + + >>> parity(0) + 0 + >>> parity(1) + 1 + >>> parity(2) # 10b -> one set bit + 1 + >>> parity(3) # 11b -> two set bits + 0 + >>> parity(1023) # 10 ones -> even + 0 + >>> parity(-1) + Traceback (most recent call last): + ... + ValueError: number must not be negative + """ + if number < 0: + raise ValueError("number must not be negative") + # Kernighan's algorithm toggling parity + p = 0 + while number: + p ^= 1 + number &= number - 1 + return p + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/power_of_4.py b/bit_manipulation/power_of_4.py new file mode 100644 index 000000000000..09e6e28621df --- /dev/null +++ b/bit_manipulation/power_of_4.py @@ -0,0 +1,67 @@ +""" + +Task: +Given a positive int number. Return True if this number is power of 4 +or False otherwise. + +Implementation notes: Use bit manipulation. +For example if the number is the power of 2 it's bits representation: +n = 0..100..00 +n - 1 = 0..011..11 + +n & (n - 1) - no intersections = 0 +If the number is a power of 4 then it should be a power of 2 +and the set bit should be at an odd position. +""" + + +def power_of_4(number: int) -> bool: + """ + Return True if this number is power of 4 or False otherwise. + + >>> power_of_4(0) + Traceback (most recent call last): + ... + ValueError: number must be positive + >>> power_of_4(1) + True + >>> power_of_4(2) + False + >>> power_of_4(4) + True + >>> power_of_4(6) + False + >>> power_of_4(8) + False + >>> power_of_4(17) + False + >>> power_of_4(64) + True + >>> power_of_4(-1) + Traceback (most recent call last): + ... + ValueError: number must be positive + >>> power_of_4(1.2) + Traceback (most recent call last): + ... + TypeError: number must be an integer + + """ + if not isinstance(number, int): + raise TypeError("number must be an integer") + if number <= 0: + raise ValueError("number must be positive") + if number & (number - 1) == 0: + c = 0 + while number: + c += 1 + number >>= 1 + return c % 2 == 1 + else: + return False + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/reverse_bits.py b/bit_manipulation/reverse_bits.py new file mode 100644 index 000000000000..4a0b2ff7047a --- /dev/null +++ b/bit_manipulation/reverse_bits.py @@ -0,0 +1,86 @@ +def get_reverse_bit_string(number: int) -> str: + """ + Return the reverse bit string of a 32 bit integer + + >>> get_reverse_bit_string(9) + '10010000000000000000000000000000' + >>> get_reverse_bit_string(43) + '11010100000000000000000000000000' + >>> get_reverse_bit_string(2873) + '10011100110100000000000000000000' + >>> get_reverse_bit_string(2550136832) + '00000000000000000000000000011001' + >>> get_reverse_bit_string("this is not a number") + Traceback (most recent call last): + ... + TypeError: operation can not be conducted on an object of type str + """ + if not isinstance(number, int): + msg = ( + "operation can not be conducted on an object of type " + f"{type(number).__name__}" + ) + raise TypeError(msg) + bit_string = "" + for _ in range(32): + bit_string += str(number % 2) + number >>= 1 + return bit_string + + +def reverse_bit(number: int) -> int: + """ + Take in a 32 bit integer, reverse its bits, return a 32 bit integer result + + >>> reverse_bit(25) + 2550136832 + >>> reverse_bit(37) + 2751463424 + >>> reverse_bit(21) + 2818572288 + >>> reverse_bit(58) + 1543503872 + >>> reverse_bit(0) + 0 + >>> reverse_bit(256) + 8388608 + >>> reverse_bit(2550136832) + 25 + >>> reverse_bit(-1) + Traceback (most recent call last): + ... + ValueError: The value of input must be non-negative + + >>> reverse_bit(1.1) + Traceback (most recent call last): + ... + TypeError: Input value must be an 'int' type + + >>> reverse_bit("0") + Traceback (most recent call last): + ... + TypeError: Input value must be an 'int' type + """ + if not isinstance(number, int): + raise TypeError("Input value must be an 'int' type") + if number < 0: + raise ValueError("The value of input must be non-negative") + + result = 0 + # iterator over [0 to 31], since we are dealing with a 32 bit integer + for _ in range(32): + # left shift the bits by unity + result <<= 1 + # get the end bit + end_bit = number & 1 + # right shift the bits by unity + number >>= 1 + # add that bit to our answer + result |= end_bit + return result + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/rotate_bits.py b/bit_manipulation/rotate_bits.py new file mode 100644 index 000000000000..521ab8de203f --- /dev/null +++ b/bit_manipulation/rotate_bits.py @@ -0,0 +1,70 @@ +""" +Author : Basuki Nath +Date : 2025-10-04 + +Bit rotation helpers for 32-bit unsigned integers. +""" + + +def rotate_left32(x: int, k: int) -> int: + """ + Rotate the lower 32 bits of x left by k and return result in 0..2**32-1. + + >>> rotate_left32(1, 1) + 2 + >>> rotate_left32(1, 31) + 2147483648 + >>> rotate_left32(0x80000000, 1) + 1 + >>> rotate_left32(0x12345678, 4) + 591751041 + >>> rotate_left32(-1, 3) + Traceback (most recent call last): + ... + ValueError: x must be a non-negative integer + >>> rotate_left32(1, -1) + Traceback (most recent call last): + ... + ValueError: k must be non-negative + """ + if not isinstance(x, int) or x < 0: + raise ValueError("x must be a non-negative integer") + if not isinstance(k, int) or k < 0: + raise ValueError("k must be non-negative") + mask = (1 << 32) - 1 + k &= 31 + return ((x << k) & mask) | ((x & mask) >> (32 - k)) + + +def rotate_right32(x: int, k: int) -> int: + """ + Rotate the lower 32 bits of x right by k and return result in 0..2**32-1. + + >>> rotate_right32(2, 1) + 1 + >>> rotate_right32(1, 1) + 2147483648 + >>> rotate_right32(0x12345678, 4) + 2166572391 + >>> rotate_right32(-1, 1) + Traceback (most recent call last): + ... + ValueError: x must be a non-negative integer + >>> rotate_right32(1, -3) + Traceback (most recent call last): + ... + ValueError: k must be non-negative + """ + if not isinstance(x, int) or x < 0: + raise ValueError("x must be a non-negative integer") + if not isinstance(k, int) or k < 0: + raise ValueError("k must be non-negative") + mask = (1 << 32) - 1 + k &= 31 + return ((x & mask) >> k) | ((x << (32 - k)) & mask) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/single_bit_manipulation_operations.py b/bit_manipulation/single_bit_manipulation_operations.py new file mode 100644 index 000000000000..95be4be976d8 --- /dev/null +++ b/bit_manipulation/single_bit_manipulation_operations.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 + +"""Provide the functionality to manipulate a single bit.""" + + +def set_bit(number: int, position: int) -> int: + """ + Set the bit at position to 1. + + Details: perform bitwise or for given number and X. + Where X is a number with all the bits - zeroes and bit on given + position - one. + + >>> set_bit(0b1101, 1) # 0b1111 + 15 + >>> set_bit(0b0, 5) # 0b100000 + 32 + >>> set_bit(0b1111, 1) # 0b1111 + 15 + """ + return number | (1 << position) + + +def clear_bit(number: int, position: int) -> int: + """ + Set the bit at position to 0. + + Details: perform bitwise and for given number and X. + Where X is a number with all the bits - ones and bit on given + position - zero. + + >>> clear_bit(0b10010, 1) # 0b10000 + 16 + >>> clear_bit(0b0, 5) # 0b0 + 0 + """ + return number & ~(1 << position) + + +def flip_bit(number: int, position: int) -> int: + """ + Flip the bit at position. + + Details: perform bitwise xor for given number and X. + Where X is a number with all the bits - zeroes and bit on given + position - one. + + >>> flip_bit(0b101, 1) # 0b111 + 7 + >>> flip_bit(0b101, 0) # 0b100 + 4 + """ + return number ^ (1 << position) + + +def is_bit_set(number: int, position: int) -> bool: + """ + Is the bit at position set? + + Details: Shift the bit at position to be the first (smallest) bit. + Then check if the first bit is set by anding the shifted number with 1. + + >>> is_bit_set(0b1010, 0) + False + >>> is_bit_set(0b1010, 1) + True + >>> is_bit_set(0b1010, 2) + False + >>> is_bit_set(0b1010, 3) + True + >>> is_bit_set(0b0, 17) + False + """ + return ((number >> position) & 1) == 1 + + +def get_bit(number: int, position: int) -> int: + """ + Get the bit at the given position + + Details: perform bitwise and for the given number and X, + Where X is a number with all the bits - zeroes and bit on given position - one. + If the result is not equal to 0, then the bit on the given position is 1, else 0. + + >>> get_bit(0b1010, 0) + 0 + >>> get_bit(0b1010, 1) + 1 + >>> get_bit(0b1010, 2) + 0 + >>> get_bit(0b1010, 3) + 1 + """ + return int((number & (1 << position)) != 0) + + +def clear_least_significant_set_bit(number: int) -> int: + """ + Clear the least significant set bit (rightmost 1 bit). + + Subtracting 1 changes the rightmost 1 to 0 and the 0 bits to its right to 1. + ANDing the result with the original number therefore clears that set bit. + For negative integers, Python's infinite sign extension is used. + https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan + + >>> clear_least_significant_set_bit(0b101100) # 0b101000 + 40 + >>> clear_least_significant_set_bit(0b1000) # 0b0 + 0 + >>> clear_least_significant_set_bit(0) + 0 + >>> clear_least_significant_set_bit(0b1111) # 0b1110 + 14 + >>> clear_least_significant_set_bit(-5) + -6 + """ + return number & (number - 1) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/bit_manipulation/swap_all_odd_and_even_bits.py b/bit_manipulation/swap_all_odd_and_even_bits.py new file mode 100644 index 000000000000..5ec84417bea6 --- /dev/null +++ b/bit_manipulation/swap_all_odd_and_even_bits.py @@ -0,0 +1,58 @@ +def show_bits(before: int, after: int) -> str: + """ + >>> print(show_bits(0, 0xFFFF)) + 0: 00000000 + 65535: 1111111111111111 + """ + return f"{before:>5}: {before:08b}\n{after:>5}: {after:08b}" + + +def swap_odd_even_bits(num: int) -> int: + """ + 1. We use bitwise AND operations to separate the even bits (0, 2, 4, 6, etc.) and + odd bits (1, 3, 5, 7, etc.) in the input number. + 2. We then right-shift the even bits by 1 position and left-shift the odd bits by + 1 position to swap them. + 3. Finally, we combine the swapped even and odd bits using a bitwise OR operation + to obtain the final result. + >>> print(show_bits(0, swap_odd_even_bits(0))) + 0: 00000000 + 0: 00000000 + >>> print(show_bits(1, swap_odd_even_bits(1))) + 1: 00000001 + 2: 00000010 + >>> print(show_bits(2, swap_odd_even_bits(2))) + 2: 00000010 + 1: 00000001 + >>> print(show_bits(3, swap_odd_even_bits(3))) + 3: 00000011 + 3: 00000011 + >>> print(show_bits(4, swap_odd_even_bits(4))) + 4: 00000100 + 8: 00001000 + >>> print(show_bits(5, swap_odd_even_bits(5))) + 5: 00000101 + 10: 00001010 + >>> print(show_bits(6, swap_odd_even_bits(6))) + 6: 00000110 + 9: 00001001 + >>> print(show_bits(23, swap_odd_even_bits(23))) + 23: 00010111 + 43: 00101011 + """ + # Get all even bits - 0xAAAAAAAA is a 32-bit number with all even bits set to 1 + even_bits = num & 0xAAAAAAAA + + # Get all odd bits - 0x55555555 is a 32-bit number with all odd bits set to 1 + odd_bits = num & 0x55555555 + + # Right shift even bits and left shift odd bits and swap them + return even_bits >> 1 | odd_bits << 1 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + for i in (-1, 0, 1, 2, 3, 4, 23, 24): + print(show_bits(i, swap_odd_even_bits(i)), "\n") diff --git a/bit_manipulation/xor_swap_two_integers.py b/bit_manipulation/xor_swap_two_integers.py new file mode 100644 index 000000000000..3c558eb9a0f9 --- /dev/null +++ b/bit_manipulation/xor_swap_two_integers.py @@ -0,0 +1,40 @@ +# Information on XOR swap: https://en.wikipedia.org/wiki/Bitwise_operation#XOR + +# Algorithm: +# 1. Take two integers a and b. +# 2. Apply XOR between a and b and store the result in a: +# a = a ^ b +# 3. XOR the new value of a with b to get the original value of a and store it in b: +# b = a ^ b +# 4. XOR the new value of a with the new value of b. +# This gives the original value of b, which we store in a: +# a = a ^ b +# 5. Return the swapped values (a, b). +# This method swaps two numbers without using a temporary variable. + + +def xor_swap(a: int, b: int) -> tuple[int, int]: + """ + Swap two integers using bitwise XOR operation and return the swapped values. + + >>> xor_swap(5, 10) + (10, 5) + >>> xor_swap(0, 0) + (0, 0) + >>> xor_swap(-1, 1) + (1, -1) + >>> xor_swap(123, 456) + (456, 123) + >>> xor_swap(12345, 54321) + (54321, 12345) + """ + a = a ^ b + b = a ^ b + a = a ^ b + return a, b + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/blockchain/README.md b/blockchain/README.md new file mode 100644 index 000000000000..47d2a6e62b1f --- /dev/null +++ b/blockchain/README.md @@ -0,0 +1,160 @@ +# Blockchain Technology + +

+ +

+ +A Blockchain is a type of **distributed ledger** technology (DLT) that consists of a growing list of records, called **blocks**, that are securely linked together using **cryptography**. + +Let's break down the terminologies in the above definition. We find below terminologies, + +- Digital Ledger Technology (DLT) +- Blocks +- Cryptography + +## Distributed Ledger Technology (DLT) + +First of all, a **ledger** is a book or collection of accounts that keeps track of account transactions. Usually, ledgers are **centralised**, meaning that they're controlled by a sole influence. +These ledgers are physical records maintained by banks, governments, and other establishments to track financial transactions, ownership records, and any other essential information. +However, this approach only serves as a hotbed for inefficiency and over-reliance on a sole entity. + +This is where Distributed Ledger Technology comes in. DLT represents a massive shift from centralised data management. + +Instead of one single entity managing data, the ledger, which in this case is **digital**, is shared among multiple **nodes**(computers) in a network. Each node is responsible for maintaining an **identical** copy of the main ledger, and all these nodes must consent before any new information is added to the main ledger. + +*Key Characteristics of DLT:* + +- Decentralisation: No single point of control, management or failure +- Transparency: All network participants can see transactions, although personal details may be encrypted +- Immutability: Once data is recorded and confirmed, it becomes extremely hard to change +- Consensus Mechanisms: Network participants must agree on the credibility of new transactions + +*To further conceptualise Distributed Ledger Technology, imagine the scenario below:* + +- Four roommates live in an apartment together. +- They all contribute towards groceries, utilities, and rent. +- In the spirit of accountability, they use a **shared Google Sheet** where every expense is tracked. +- Everyone can **see**, **add**, and **validate** expenses, but **no one can delete past expense entries** — they're only allowed to append new rows with reasons for any corrections. + +This is exactly how a Distributed Ledger works. + +| Google Sheet Feature | DLT Concept Equivalent | +|---------------------------------------------------|---------------------------------------------------| +| Everyone has access to the same document | **Distributed ledger** — everyone has a synchronised copy | +| Changes are visible to all in real time | **Transparency and consensus** | +| No one can erase old entries; only new ones can be added with explanations | **Immutability** of records | +| Each entry includes a timestamp and who added it | **Timestamped transactions** and **identity** (like digital signatures) | +| Everyone can verify what’s been entered | **Decentralised verification** | +| Any disputes are resolved by checking the shared record | **Consensus mechanism** (though manual here) | + +## Centralised vs. Distributed Management + +**Centralised approach:** Here, an organisation with 4 branches stores all its data in one central database. This database is managed by the Chief Administrator at the headquarters. Each branch needs approval from the Chief Administrator to view and access information. +If a bad actor gains access to the central database, they can unduly alter all the data, and if the central database fails, all the branches lose their access to vital data. + +**Distributed approach:** In this case, each organisational branch maintains a complete **carbon copy** of the original ledger. For one branch to add information, all branches must reach a consensus. +When one branch adds information, it's broadcast to the entire network and systematically synchronised across all other ledgers through a peer-to-peer network after verification. + +In summary, a distributed approach offers: + +- Fault tolerance: If information is altered in one branch, other branches can detect the change through consensus algorithms +- Security: If one branch is compromised, other branches remain secure and can identify the bad actor +- Availability: No single point of failure — if one computer goes down, the network is still highly functional +- Verification: Any member of the network can independently verify the integrity of the complete transaction history + +## Blocks + +A **block** is a cluster of transactions grouped together with metadata in a structured manner. +Imagine a block as a page in a digital ledger. This page consists of multiple transaction entries, including important information about when and how that page was created. +Cryptography is the practice and study of secure communication techniques amid adversarial behavior. More broadly, cryptography is the creation and analysis of protocols that prevent third parties or the general public from accessing private messages. + +*Structure of a Block:* + +1. Block Header: + +- Previous Block Hash: A unique fingerprint of the previous block, creating the *chain* +- Merkle Root: A cryptographic summary of all transactions in the block +- Timestamp: The time the block was created +- Nonce: A number used in the Proof-of-Work (PoW) mining process +- Difficulty Target: The complexity level required to mine this block + +2. Block Body: + +- Transaction Data: The actual transaction records (transfers, smart contract executions, etc.) +- Transaction Count: Number of transactions confirmed in the block + +## The Chain Formation + +Blocks are connected cryptographically using hash functions, in order to create an **immutable** chain: + +1. Genesis Block: The first block in the chain, which is hardcoded into the blockchain protocol +2. Subsequent Blocks: Each new block contains the hash function of the preceding block +3. Chain Integrity: If someone tries to modify a previous block, its hash function changes, breaking the chain and notifying the entire network + +With cryptographic linking, once a block is added to the chain and verified by the network, modifying any preceding block is computationally impractical. The deeper a block is in the chain (the more blocks built on top of it), the more secure it becomes. + +## Cryptography + +Cryptography is the practice and study of secure communication strategies that safeguard information from unwanted access. In the context of blockchain, cryptography is the backbone of security, privacy, and trust, in a decentralised network where participants don't know, or even trust each other. + +*Key Cryptographic Concepts in Blockchain:* + +1. Hash Functions: + +- Generate fixed-size output (hash) from variable-size input +- The SHA-256 function is typically used in Bitcoin +- Properties: Deterministic (giving input to a hash function will always produce the same output), avalanche effect, irreversible +- Used for: Block linking, transaction verification, mining puzzles + +2. Digital Signatures: + +- Prove ownership and authorise transactions without revealing private keys +- Based on public-key cryptography (asymmetric encryption) +- Each user has a pair: private key (secret) and public key (shareable) +- Process: Sign with private key and verify with public key + +3. Merkle Trees: + +- Binary tree structure that effectively outlines all transactions in a block +- Enables quick verification of transaction inclusion without having to download the entire block +- Provides evidence for altered transaction data + +*Cryptographic Security in Practice:* + +1. Transaction Security: +When person A wants to send **cryptocurrency** to person B: + +- Person A creates a transaction using person B's **public address** +- Person A signs the transaction with their **private key** +- Network nodes verify the signature using person A's **public key** +- Once verified, the transaction is added to a block + +2. Block Integrity: + +- Each block contains the hash of the previous block +- Any change to a previous block changes its hash +- This breaks the chain and immediately notifies the entire network +- Makes altering historical data unfeasible + +3. Privacy vs. Transparency: + +- **Transparency** means amounts and addresses are public. +- **Privacy** ensures that real-world identities behind addresses are undisclosed. +- This balance allows for accountability and anonymity in transactions. + +> In summary, **Blockchain technology** represents a pioneering shift in how we store, verify, and transfer value in digital systems. It's a distributed, immutable, non-physical ledger that records transactions and tracks assets across a decentralised network, without requiring **trust** in a sole entity. +Blockchain networks can track and trade virtually any **asset** of value, from tangible assets like real estate, commodities, and vehicles, to intangible assets such as cryptocurrencies, intellectual property, digital art (NFTs), and carbon credits. +A blockchain network can track and sell almost anything of value, lowering risk and costs for everyone involved. + +This is just a brief introduction to blockchain technology. +To find out more about this technology, check out the links below: + +- - Comprehensive overview of blockchain technology +- - The original document by Satoshi Nakamoto +- - Introduction to programmable blockchain +- - Understanding the math behind blockchain security +- - Data structure for efficient verification +- - JavaScript library for blockchain interaction +- +- +- diff --git a/blockchain/__init__.py b/blockchain/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/blockchain/diophantine_equation.py b/blockchain/diophantine_equation.py new file mode 100644 index 000000000000..5fc3777c431e --- /dev/null +++ b/blockchain/diophantine_equation.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +from maths.greatest_common_divisor import greatest_common_divisor + + +def diophantine(a: int, b: int, c: int) -> tuple[float, float]: + """ + Diophantine Equation : Given integers a,b,c ( at least one of a and b != 0), the + diophantine equation a*x + b*y = c has a solution (where x and y are integers) + iff greatest_common_divisor(a,b) divides c. + + GCD ( Greatest Common Divisor ) or HCF ( Highest Common Factor ) + + >>> diophantine(10,6,14) + (-7.0, 14.0) + + >>> diophantine(391,299,-69) + (9.0, -12.0) + + But above equation has one more solution i.e., x = -4, y = 5. + That's why we need diophantine all solution function. + + """ + + assert ( + c % greatest_common_divisor(a, b) == 0 + ) # greatest_common_divisor(a,b) is in maths directory + (d, x, y) = extended_gcd(a, b) # extended_gcd(a,b) function implemented below + r = c / d + return (r * x, r * y) + + +def diophantine_all_soln(a: int, b: int, c: int, n: int = 2) -> None: + """ + Lemma : if n|ab and gcd(a,n) = 1, then n|b. + + Finding All solutions of Diophantine Equations: + + Theorem : Let gcd(a,b) = d, a = d*p, b = d*q. If (x0,y0) is a solution of + Diophantine Equation a*x + b*y = c. a*x0 + b*y0 = c, then all the + solutions have the form a(x0 + t*q) + b(y0 - t*p) = c, + where t is an arbitrary integer. + + n is the number of solution you want, n = 2 by default + + >>> diophantine_all_soln(10, 6, 14) + -7.0 14.0 + -4.0 9.0 + + >>> diophantine_all_soln(10, 6, 14, 4) + -7.0 14.0 + -4.0 9.0 + -1.0 4.0 + 2.0 -1.0 + + >>> diophantine_all_soln(391, 299, -69, n = 4) + 9.0 -12.0 + 22.0 -29.0 + 35.0 -46.0 + 48.0 -63.0 + + """ + (x0, y0) = diophantine(a, b, c) # Initial value + d = greatest_common_divisor(a, b) + p = a // d + q = b // d + + for i in range(n): + x = x0 + i * q + y = y0 - i * p + print(x, y) + + +def extended_gcd(a: int, b: int) -> tuple[int, int, int]: + """ + Extended Euclid's Algorithm : If d divides a and b and d = a*x + b*y for integers + x and y, then d = gcd(a,b) + + >>> extended_gcd(10, 6) + (2, -1, 2) + + >>> extended_gcd(7, 5) + (1, -2, 3) + + """ + assert a >= 0 + assert b >= 0 + + if b == 0: + d, x, y = a, 1, 0 + else: + (d, p, q) = extended_gcd(b, a % b) + x = q + y = p - q * (a // b) + + assert a % d == 0 + assert b % d == 0 + assert d == a * x + b * y + + return (d, x, y) + + +def all_diophantine_solutions( + a: int, + b: int, + c: int, + n: int = 2, +) -> list[tuple[int, int]]: + """ + Return up to `n` integer solutions (x, y) to the linear Diophantine equation + a*x + b*y = c using the extended Euclidean algorithm. + + Raises + ------ + ValueError + If no integer solutions exist. + + Time complexity + --------------- + O(log(max(|a|, |b|))) to compute a base solution using extended_gcd; + plus O(n) to enumerate `n` solutions. + + Space complexity + ---------------- + O(1) beyond the returned list. + + Examples + -------- + >>> all_diophantine_solutions(10, 6, 14, n=2) + [(-7, 14), (-4, 9)] + >>> all_diophantine_solutions(10, 6, 14, n=4) + [(-7, 14), (-4, 9), (-1, 4), (2, -1)] + >>> all_diophantine_solutions(3, 6, 10, n=1) + Traceback (most recent call last): + ... + ValueError: No integer solutions exist for a=3, b=6, c=10 + """ + if a == 0 and b == 0: + if c == 0: + # Infinite solutions; return one canonical solution. + return [(0, 0)][: min(1, n)] + raise ValueError("No integer solutions exist for a=0, b=0, c!=0") + + g, xg, yg = extended_gcd(abs(a), abs(b)) + if c % g != 0: + msg = f"No integer solutions exist for a={a}, b={b}, c={c}" + raise ValueError(msg) + + # Scale a particular solution to ax + by = c + x0, y0 = xg * (c // g), yg * (c // g) + if a < 0: + x0 = -x0 + if b < 0: + y0 = -y0 + + # General solution: x = x0 + t*(b/g), y = y0 - t*(a/g) + dx, dy = b // g, a // g + return [(x0 + t * dx, y0 - t * dy) for t in range(n)] + + +if __name__ == "__main__": + from doctest import testmod + + testmod(name="diophantine", verbose=True) + testmod(name="diophantine_all_soln", verbose=True) + testmod(name="extended_gcd", verbose=True) + testmod(name="greatest_common_divisor", verbose=True) diff --git a/blockchain/merkle_tree.py b/blockchain/merkle_tree.py new file mode 100644 index 000000000000..627937fdd54a --- /dev/null +++ b/blockchain/merkle_tree.py @@ -0,0 +1,107 @@ +""" +Merkle Tree Construction and Verification + +This module implements the construction of a Merkle Tree and +verification of inclusion proofs for blockchain data integrity. + +Each leaf is a SHA-256 hash of a transaction, and internal nodes are +computed by hashing the concatenation of their child nodes. + +References: +https://en.wikipedia.org/wiki/Merkle_tree +""" + +import hashlib + + +def sha256(data: str) -> str: + """ + Compute the SHA-256 hash of the given string. + + Args: + data (str): Input string. + + Returns: + str: Hexadecimal SHA-256 hash of the input. + + Example: + >>> sha256("abc") + 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' + """ + return hashlib.sha256(data.encode()).hexdigest() + + +def build_merkle_tree(leaves: list[str]) -> list[list[str]]: + """ + Build a Merkle Tree from the given leaf nodes. + + Args: + leaves: List of data strings (transactions). + + Returns: + A list of lists representing tree levels, + with the last level containing the Merkle root. + + >>> len(build_merkle_tree(["a", "b", "c", "d"])[-1][0]) + 64 + """ + if not leaves: + raise ValueError("Leaf list cannot be empty.") + + current_level = [sha256(x) for x in leaves] + tree = [current_level] + + while len(current_level) > 1: + next_level = [] + for i in range(0, len(current_level), 2): + left = current_level[i] + right = current_level[i + 1] if i + 1 < len(current_level) else left + next_level.append(sha256(left + right)) + current_level = next_level + tree.append(current_level) + + return tree + + +def merkle_root(leaves: list[str]) -> str: + """ + Return the Merkle root hash for a given list of data. + + >>> r = merkle_root(["tx1", "tx2", "tx3"]) + >>> isinstance(r, str) + True + """ + return build_merkle_tree(leaves)[-1][0] + + +def verify_proof(leaf: str, proof: list[str], root: str) -> bool: + """ + Verify inclusion of a leaf using a Merkle proof. + + Args: + leaf: Original data string. + proof: List of sibling hashes up the path. + root: Expected Merkle root hash. + + Returns: + True if proof is valid, else False. + + >>> data = ["a", "b", "c", "d"] + >>> tree = build_merkle_tree(data) + >>> root = tree[-1][0] + >>> leaf = "a" + >>> proof = [sha256("b"), sha256(sha256("c") + sha256("d"))] + >>> verify_proof(leaf, proof, root) + True + """ + computed_hash = sha256(leaf) + for sibling in proof: + combined = sha256(computed_hash + sibling) + computed_hash = combined + return computed_hash == root + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/blockchain/proof_of_stake.py b/blockchain/proof_of_stake.py new file mode 100644 index 000000000000..7eea9ded21e5 --- /dev/null +++ b/blockchain/proof_of_stake.py @@ -0,0 +1,40 @@ +import random + + +class Validator: + def __init__(self, name: str, stake: int) -> None: + """ + Initializes a new validator with a given name and stake. + + Args: + name (str): The name of the validator. + stake (int): The amount of stake the validator has. + """ + self.name = name + self.stake = stake + + +def choose_validator(validators: list[Validator]) -> Validator: + """ + Selects a validator to create the next block based on the weight of their stake. + + The higher the stake, the greater the chance to be selected. + + Args: + validators (list[Validator]): A list of Validator objects. + + Returns: + Validator: The selected validator based on weighted random selection. + + Example: + >>> validators = [Validator("Alice", 50), Validator("Bob", 30)] + >>> chosen = choose_validator(validators) + >>> isinstance(chosen, Validator) + True + """ + total_stake = sum(v.stake for v in validators) + weighted_validators = [(v, v.stake / total_stake) for v in validators] + selected = random.choices( + [v[0] for v in weighted_validators], weights=[v[1] for v in weighted_validators] + ) + return selected[0] diff --git a/blockchain/proof_of_work.py b/blockchain/proof_of_work.py new file mode 100644 index 000000000000..8ad2b4b88c42 --- /dev/null +++ b/blockchain/proof_of_work.py @@ -0,0 +1,189 @@ +# Title: Proof of Work Algorithm for Blockchain + +## Algorithm Statement: +# The algorithm implements the Proof of Work (PoW) consensus mechanism used in +# blockchain to validate blocks. PoW ensures participants (miners) perform a +# computational task to create a valid block and add it to the blockchain. The +# difficulty is defined by the number of leading zeros required in the block hash. + +import hashlib +import time + + +class Block: + def __init__( + self, + index: int, + previous_hash: str, + transactions: str, + timestamp: float, + difficulty: int, + ) -> None: + """ + Initializes a Block object with the specified parameters. + + Parameters: + - index (int): The index of the block in the blockchain. + - previous_hash (str): The hash of the previous block. + - transactions (str): The list of transactions in the block. + - timestamp (float): The time when the block was created + (in Unix timestamp format). + - difficulty (int): The difficulty level for mining this block. + """ + self.index = index + self.previous_hash = previous_hash + self.transactions = transactions + self.timestamp = timestamp + self.nonce = 0 # Start with nonce 0 + self.difficulty = difficulty + self.hash = self.compute_hash() + + def compute_hash(self) -> str: + """ + Generates the hash of the block content. + Combines index, previous hash, transactions, timestamp, and nonce into + a string, which is then hashed using SHA-256. + + Returns: + - str: The hash of the block. + """ + block_string = ( + f"{self.index}{self.previous_hash}{self.transactions}{self.timestamp}" + f"{self.nonce}" + ) + return hashlib.sha256(block_string.encode()).hexdigest() + + def mine_block(self) -> None: + """ + Performs Proof of Work by adjusting the nonce until a valid hash is found. + A valid hash has the required number of leading zeros based on the + difficulty level. + + Returns: + - None + """ + target = ( + "0" * self.difficulty + ) # Target hash should start with 'difficulty' zeros + while self.hash[: self.difficulty] != target: + self.nonce += 1 + self.hash = self.compute_hash() + + print(f"Block mined with nonce {self.nonce}, hash: {self.hash}") + + +class Blockchain: + def __init__(self, difficulty: int) -> None: + """ + Initializes the blockchain with a given difficulty level. + + Parameters: + - difficulty (int): The difficulty level for mining blocks in this blockchain. + + Returns: + - None + """ + self.chain: list[Block] = [] # Adding type hint for the list of blocks + self.difficulty = difficulty + self.create_genesis_block() + + def create_genesis_block(self) -> None: + """ + Creates the first block in the blockchain (the Genesis block). + + Returns: + - None + """ + genesis_block = Block(0, "0", "Genesis Block", time.time(), self.difficulty) + genesis_block.mine_block() + self.chain.append(genesis_block) + + def add_block(self, transactions: str) -> None: + """ + Adds a new block to the blockchain after performing Proof of Work. + + Parameters: + - transactions (str): The list of transactions to be added in the new block. + + Returns: + - None + """ + previous_block = self.chain[-1] + new_block = Block( + len(self.chain), + previous_block.hash, + transactions, + time.time(), + self.difficulty, + ) + new_block.mine_block() + self.chain.append(new_block) + + def is_chain_valid(self) -> bool: + """ + Verifies the integrity of the blockchain by ensuring each block's previous + hash matches and that all blocks meet the Proof of Work requirement. + + Returns: + - bool: True if the blockchain is valid, False otherwise. + """ + for i in range(1, len(self.chain)): + current_block = self.chain[i] + previous_block = self.chain[i - 1] + + if current_block.hash != current_block.compute_hash(): + print(f"Invalid block at index {i}. Hash mismatch.") + return False + + if current_block.previous_hash != previous_block.hash: + print(f"Invalid chain at index {i}. Previous hash mismatch.") + return False + + return True + + +# Test cases + + +## Test Case 1: Blockchain Initialization and Genesis Block +# This test verifies if the blockchain is correctly initialized with a Genesis block +# and if the block is successfully mined. +def test_blockchain() -> None: + """ + Test cases for the Blockchain proof of work algorithm. + + Returns: + - None + """ + # Create blockchain with difficulty level of 4 (hash should start with 4 zeros) + blockchain = Blockchain(difficulty=4) + + ## Test Case 2: Add a block and verify the block is mined + # This test adds a new block with transactions and ensures it's mined according + # to the proof of work mechanism. + blockchain.add_block("Transaction 1: Alice pays Bob 5 BTC") + blockchain.add_block("Transaction 2: Bob pays Charlie 3 BTC") + + ## Test Case 3: Verify blockchain integrity + # This test checks that the blockchain remains valid after adding new blocks + assert blockchain.is_chain_valid(), "Blockchain should be valid" + + ## Test Case 4: Tampering with the blockchain + # This test simulates tampering with the blockchain and checks that the validation + # correctly detects the tampering. + blockchain.chain[ + 1 + ].transactions = "Transaction 1: Alice pays Bob 50 BTC" # Tampering + assert not blockchain.is_chain_valid(), ( + "Blockchain should be invalid due to tampering" + ) + + ## Test Case 5: Correct blockchain validation + # This test checks if the blockchain becomes invalid after tampering and verifies + # if the PoW still holds after tampering is done. + + print("All test cases passed.") + + +if __name__ == "__main__": + test_blockchain() diff --git a/blockchain/simple_blockchain.py b/blockchain/simple_blockchain.py new file mode 100644 index 000000000000..6adaeb9883d4 --- /dev/null +++ b/blockchain/simple_blockchain.py @@ -0,0 +1,168 @@ +""" +A simple blockchain implementation with Proof-of-Work (PoW). + +This educational example demonstrates: +- Block structure with index, timestamp, data, previous hash, nonce, and hash +- Mining via Proof-of-Work +- Chain integrity verification + +Author: Letitia Gilbert +""" + +import hashlib +from time import time + + +class Block: + """ + Represents a single block in a blockchain. + + Attributes: + index (int): Position of the block in the chain. + timestamp (float): Creation time of the block. + data (str): Data stored in the block. + previous_hash (str): Hash of the previous block. + nonce (int): Number used for mining. + hash (str): SHA256 hash of the block's content. + """ + + def __init__( + self, index: int, data: str, previous_hash: str, difficulty: int = 2 + ) -> None: + self.index = index + self.timestamp = time() + self.data = data + self.previous_hash = previous_hash + self.nonce, self.hash = self.mine_block(difficulty) + + def compute_hash(self, nonce: int) -> str: + """ + Compute SHA256 hash of the block with given nonce. + + Args: + nonce (int): Nonce to include in the hash. + + Returns: + str: Hexadecimal hash string. + + >>> block = Block(0, "Genesis", "0", difficulty=2) + >>> len(block.compute_hash(0)) == 64 + True + >>> isinstance(block.compute_hash(0), str) + True + """ + block_string = ( + f"{self.index}{self.timestamp}{self.data}{self.previous_hash}{nonce}" + ) + return hashlib.sha256(block_string.encode()).hexdigest() + + def mine_block(self, difficulty: int) -> tuple[int, str]: + """ + Simple Proof-of-Work mining algorithm. + + Args: + difficulty (int): Number of leading zeros required in the hash. + + Returns: + Tuple[int, str]: Valid nonce and resulting hash that satisfies difficulty. + + >>> block = Block(0, "Genesis", "0", difficulty=2) + >>> block.hash.startswith('00') + True + """ + if difficulty < 1: + raise ValueError("Difficulty must be at least 1") + nonce = 0 + target = "0" * difficulty + while True: + hash_result = self.compute_hash(nonce) + if hash_result.startswith(target): + return nonce, hash_result + nonce += 1 + + +class Blockchain: + """ + Simple blockchain class maintaining a list of blocks. + + Attributes: + chain (List[Block]): List of blocks forming the chain. + """ + + def __init__(self, difficulty: int = 2) -> None: + self.difficulty = difficulty + self.chain: list[Block] = [self.create_genesis_block()] + + def create_genesis_block(self) -> Block: + """ + Create the first block in the blockchain. + + Returns: + Block: Genesis block. + + >>> bc = Blockchain() + >>> bc.chain[0].index + 0 + >>> bc.chain[0].hash.startswith('00') + True + """ + return Block(0, "Genesis Block", "0", self.difficulty) + + def add_block(self, data: str) -> Block: + """ + Add a new block to the blockchain with given data. + + Args: + data (str): Data to store in the block. + + Returns: + Block: Newly added block. + + >>> bc = Blockchain() + >>> new_block = bc.add_block("Test Data") + >>> new_block.index + 1 + >>> new_block.previous_hash == bc.chain[0].hash + True + >>> new_block.hash.startswith('00') + True + >>> bc.is_valid() + True + """ + prev_hash = self.chain[-1].hash + new_block = Block(len(self.chain), data, prev_hash, self.difficulty) + self.chain.append(new_block) + return new_block + + def is_valid(self) -> bool: + """ + Verify the integrity of the blockchain. + + Returns: + bool: True if chain is valid, False otherwise. + + >>> bc = Blockchain() + >>> new_block = bc.add_block("Test") + >>> new_block.index + 1 + >>> new_block.previous_hash == bc.chain[0].hash + True + >>> new_block.hash.startswith('00') + True + >>> bc.is_valid() + True + >>> bc.chain[1].previous_hash = "tampered" + >>> bc.is_valid() + False + + """ + for i in range(1, len(self.chain)): + current = self.chain[i] + prev = self.chain[i - 1] + if current.previous_hash != prev.hash: + return False + if not current.hash.startswith("0" * self.difficulty): + return False + if current.hash != current.compute_hash(current.nonce): + return False + return True diff --git a/blockchain/simple_proof_of_work.py b/blockchain/simple_proof_of_work.py new file mode 100644 index 000000000000..abcd591b8a10 --- /dev/null +++ b/blockchain/simple_proof_of_work.py @@ -0,0 +1,62 @@ +""" +Proof of Work (PoW) implementation. +This algorithm is used in blockchain technology +to reach consensus and secure the network +""" + +import hashlib + + +def proof_of_work( + block_number: int, transactions: str, previous_hash: str, difficulty: int +) -> tuple[int, str]: + """ + Finds a nonce such that the SHA-256 hash of the block starts with + a specific number of zeros (difficulty). + + >>> proof_of_work(1, "test", "abc", 1)[1].startswith("0") + True + >>> # Consistency check: same input must produce same output + >>> res1 = proof_of_work(1, "data", "hash", 2) + >>> res2 = proof_of_work(1, "data", "hash", 2) + >>> res1 == res2 + True + >>> # Difficulty 0 should return nonce 0 immediately + >>> proof_of_work(1, "data", "hash", 0)[0] + 0 + """ + if difficulty < 0: + raise ValueError("difficulty must be a non-negative integer") + + prefix = "0" * difficulty + nonce = 0 + + while True: + # Create a single string representing all block data + text = f"{block_number}{transactions}{previous_hash}{nonce}" + + # Calculate the SHA-256 hash + current_hash = hashlib.sha256(text.encode()).hexdigest() + + # Check if the hash meets the difficulty requirement + if current_hash.startswith(prefix): + return nonce, current_hash + + nonce += 1 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + # Example usage: + example_tx = "Alice sends 1 BTC to Bob" + prev_h = "00000abcdef1234567890" + diff = 4 + + print(f"Mining block... (Difficulty: {diff})") + nonce, hash_found = proof_of_work(1, example_tx, prev_h, diff) + + print(f"Success! Nonce: {nonce}") + print(f"Hash: {hash_found}") diff --git a/boolean_algebra/README.md b/boolean_algebra/README.md new file mode 100644 index 000000000000..45969c855f9c --- /dev/null +++ b/boolean_algebra/README.md @@ -0,0 +1,7 @@ +# Boolean Algebra + +Boolean algebra is used to do arithmetic with bits of values True (1) or False (0). +There are three basic operations: 'and', 'or' and 'not'. + +* +* diff --git a/boolean_algebra/__init__.py b/boolean_algebra/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/boolean_algebra/and_gate.py b/boolean_algebra/and_gate.py new file mode 100644 index 000000000000..2f012f779b08 --- /dev/null +++ b/boolean_algebra/and_gate.py @@ -0,0 +1,95 @@ +""" +An AND Gate is a logic gate in boolean algebra which results to 1 (True) if all the +inputs are 1 (True), and 0 (False) otherwise. + +Following is the truth table of a Two Input AND Gate: + ------------------------------ + | Input 1 | Input 2 | Output | + ------------------------------ + | 0 | 0 | 0 | + | 0 | 1 | 0 | + | 1 | 0 | 0 | + | 1 | 1 | 1 | + ------------------------------ + +Refer - https://www.geeksforgeeks.org/logic-gates/ +""" + + +def and_gate(input_1: int, input_2: int) -> int: + """ + Calculate AND of two binary input values. + + >>> and_gate(0, 0) + 0 + >>> and_gate(0, 1) + 0 + >>> and_gate(1, 0) + 0 + >>> and_gate(1, 1) + 1 + >>> and_gate(2, 1) + Traceback (most recent call last): + ... + ValueError: Both inputs must be 0 or 1 + >>> and_gate(0, "1") + Traceback (most recent call last): + ... + TypeError: Both inputs must be integers + """ + # Type validation + if not isinstance(input_1, int) or not isinstance(input_2, int): + raise TypeError("Both inputs must be integers") + + # Value validation + if input_1 not in (0, 1) or input_2 not in (0, 1): + raise ValueError("Both inputs must be 0 or 1") + + return input_1 & input_2 + + +def n_input_and_gate(inputs: list[int]) -> int: + """ + Calculate AND of a list of binary input values. + + >>> n_input_and_gate([1, 0, 1, 1, 0]) + 0 + >>> n_input_and_gate([1, 1, 1, 1, 1]) + 1 + >>> n_input_and_gate([1, 0, 1, 1, 0]) + 0 + >>> n_input_and_gate([]) + Traceback (most recent call last): + ... + ValueError: Input list cannot be empty + >>> n_input_and_gate([1, 2, 1]) + Traceback (most recent call last): + ... + ValueError: All inputs in the list must be 0 or 1 + >>> n_input_and_gate([1, "1"]) + Traceback (most recent call last): + ... + TypeError: All inputs in the list must be integers + """ + # Type validation for the list itself + if not isinstance(inputs, list): + raise TypeError("Input must be a list") + + # Edge case validation for an empty list + if not inputs: + raise ValueError("Input list cannot be empty") + + # Type and value validation for items within the list + for item in inputs: + if not isinstance(item, int): + raise TypeError("All inputs in the list must be integers") + if item not in (0, 1): + raise ValueError("All inputs in the list must be 0 or 1") + + return int(all(inputs)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/boolean_algebra/imply_gate.py b/boolean_algebra/imply_gate.py new file mode 100644 index 000000000000..3d71ff12f8d9 --- /dev/null +++ b/boolean_algebra/imply_gate.py @@ -0,0 +1,91 @@ +""" +An IMPLY Gate is a logic gate in boolean algebra which results to 1 if +either input 1 is 0, or if input 1 is 1, then the output is 1 only if input 2 is 1. +It is true if input 1 implies input 2. + +Following is the truth table of an IMPLY Gate: + ------------------------------ + | Input 1 | Input 2 | Output | + ------------------------------ + | 0 | 0 | 1 | + | 0 | 1 | 1 | + | 1 | 0 | 0 | + | 1 | 1 | 1 | + ------------------------------ + +Refer - https://en.wikipedia.org/wiki/IMPLY_gate +""" + + +def imply_gate(input_1: int, input_2: int) -> int: + """ + Calculate IMPLY of the input values + + >>> imply_gate(0, 0) + 1 + >>> imply_gate(0, 1) + 1 + >>> imply_gate(1, 0) + 0 + >>> imply_gate(1, 1) + 1 + """ + return int(input_1 == 0 or input_2 == 1) + + +def recursive_imply_list(input_list: list[int]) -> int: + """ + Recursively calculates the implication of a list. + Strictly the implication is applied consecutively left to right: + ( (a -> b) -> c ) -> d ... + + >>> recursive_imply_list([]) + Traceback (most recent call last): + ... + ValueError: Input list must contain at least two elements + >>> recursive_imply_list([0]) + Traceback (most recent call last): + ... + ValueError: Input list must contain at least two elements + >>> recursive_imply_list([1]) + Traceback (most recent call last): + ... + ValueError: Input list must contain at least two elements + >>> recursive_imply_list([0, 0]) + 1 + >>> recursive_imply_list([0, 1]) + 1 + >>> recursive_imply_list([1, 0]) + 0 + >>> recursive_imply_list([1, 1]) + 1 + >>> recursive_imply_list([0, 0, 0]) + 0 + >>> recursive_imply_list([0, 0, 1]) + 1 + >>> recursive_imply_list([0, 1, 0]) + 0 + >>> recursive_imply_list([0, 1, 1]) + 1 + >>> recursive_imply_list([1, 0, 0]) + 1 + >>> recursive_imply_list([1, 0, 1]) + 1 + >>> recursive_imply_list([1, 1, 0]) + 0 + >>> recursive_imply_list([1, 1, 1]) + 1 + """ + if len(input_list) < 2: + raise ValueError("Input list must contain at least two elements") + first_implication = imply_gate(input_list[0], input_list[1]) + if len(input_list) == 2: + return first_implication + new_list = [first_implication, *input_list[2:]] + return recursive_imply_list(new_list) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/boolean_algebra/karnaugh_map_simplification.py b/boolean_algebra/karnaugh_map_simplification.py new file mode 100644 index 000000000000..c7f2d4c6b897 --- /dev/null +++ b/boolean_algebra/karnaugh_map_simplification.py @@ -0,0 +1,55 @@ +""" +https://en.wikipedia.org/wiki/Karnaugh_map +https://www.allaboutcircuits.com/technical-articles/karnaugh-map-boolean-algebraic-simplification-technique +""" + + +def simplify_kmap(kmap: list[list[int]]) -> str: + """ + Simplify the Karnaugh map. + >>> simplify_kmap(kmap=[[0, 1], [1, 1]]) + "A'B + AB' + AB" + >>> simplify_kmap(kmap=[[0, 0], [0, 0]]) + '' + >>> simplify_kmap(kmap=[[0, 1], [1, -1]]) + "A'B + AB' + AB" + >>> simplify_kmap(kmap=[[0, 1], [1, 2]]) + "A'B + AB' + AB" + >>> simplify_kmap(kmap=[[0, 1], [1, 1.1]]) + "A'B + AB' + AB" + >>> simplify_kmap(kmap=[[0, 1], [1, 'a']]) + "A'B + AB' + AB" + """ + simplified_f = [] + for a, row in enumerate(kmap): + for b, item in enumerate(row): + if item: + term = ("A" if a else "A'") + ("B" if b else "B'") + simplified_f.append(term) + return " + ".join(simplified_f) + + +def main() -> None: + """ + Main function to create and simplify a K-Map. + + >>> main() + [0, 1] + [1, 1] + Simplified Expression: + A'B + AB' + AB + """ + kmap = [[0, 1], [1, 1]] + + # Manually generate the product of [0, 1] and [0, 1] + + for row in kmap: + print(row) + + print("Simplified Expression:") + print(simplify_kmap(kmap)) + + +if __name__ == "__main__": + main() + print(f"{simplify_kmap(kmap=[[0, 1], [1, 1]]) = }") diff --git a/boolean_algebra/multiplexer.py b/boolean_algebra/multiplexer.py new file mode 100644 index 000000000000..7e65c785c829 --- /dev/null +++ b/boolean_algebra/multiplexer.py @@ -0,0 +1,42 @@ +def mux(input0: int, input1: int, select: int) -> int: + """ + Implement a 2-to-1 Multiplexer. + + :param input0: The first input value (0 or 1). + :param input1: The second input value (0 or 1). + :param select: The select signal (0 or 1) to choose between input0 and input1. + :return: The output based on the select signal. input1 if select else input0. + + https://www.electrically4u.com/solved-problems-on-multiplexer + https://en.wikipedia.org/wiki/Multiplexer + + >>> mux(0, 1, 0) + 0 + >>> mux(0, 1, 1) + 1 + >>> mux(1, 0, 0) + 1 + >>> mux(1, 0, 1) + 0 + >>> mux(2, 1, 0) + Traceback (most recent call last): + ... + ValueError: Inputs and select signal must be 0 or 1 + >>> mux(0, -1, 0) + Traceback (most recent call last): + ... + ValueError: Inputs and select signal must be 0 or 1 + >>> mux(0, 1, 1.1) + Traceback (most recent call last): + ... + ValueError: Inputs and select signal must be 0 or 1 + """ + if all(i in (0, 1) for i in (input0, input1, select)): + return input1 if select else input0 + raise ValueError("Inputs and select signal must be 0 or 1") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/boolean_algebra/nand_gate.py b/boolean_algebra/nand_gate.py new file mode 100644 index 000000000000..ea7a6815dcc9 --- /dev/null +++ b/boolean_algebra/nand_gate.py @@ -0,0 +1,36 @@ +""" +A NAND Gate is a logic gate in boolean algebra which results to 0 (False) if both +the inputs are 1, and 1 (True) otherwise. It's similar to adding +a NOT gate along with an AND gate. +Following is the truth table of a NAND Gate: + ------------------------------ + | Input 1 | Input 2 | Output | + ------------------------------ + | 0 | 0 | 1 | + | 0 | 1 | 1 | + | 1 | 0 | 1 | + | 1 | 1 | 0 | + ------------------------------ +Refer - https://www.geeksforgeeks.org/logic-gates-in-python/ +""" + + +def nand_gate(input_1: int, input_2: int) -> int: + """ + Calculate NAND of the input values + >>> nand_gate(0, 0) + 1 + >>> nand_gate(0, 1) + 1 + >>> nand_gate(1, 0) + 1 + >>> nand_gate(1, 1) + 0 + """ + return int(not (input_1 and input_2)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/boolean_algebra/nimply_gate.py b/boolean_algebra/nimply_gate.py new file mode 100644 index 000000000000..68e82c8db8d9 --- /dev/null +++ b/boolean_algebra/nimply_gate.py @@ -0,0 +1,39 @@ +""" +An NIMPLY Gate is a logic gate in boolean algebra which results to 0 if +either input 1 is 0, or if input 1 is 1, then it is 0 only if input 2 is 1. +It is false if input 1 implies input 2. It is the negated form of imply + +Following is the truth table of an NIMPLY Gate: + ------------------------------ + | Input 1 | Input 2 | Output | + ------------------------------ + | 0 | 0 | 0 | + | 0 | 1 | 0 | + | 1 | 0 | 1 | + | 1 | 1 | 0 | + ------------------------------ + +Refer - https://en.wikipedia.org/wiki/NIMPLY_gate +""" + + +def nimply_gate(input_1: int, input_2: int) -> int: + """ + Calculate NIMPLY of the input values + + >>> nimply_gate(0, 0) + 0 + >>> nimply_gate(0, 1) + 0 + >>> nimply_gate(1, 0) + 1 + >>> nimply_gate(1, 1) + 0 + """ + return int(input_1 == 1 and input_2 == 0) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/boolean_algebra/nor_gate.py b/boolean_algebra/nor_gate.py new file mode 100644 index 000000000000..d4d6f0da23ea --- /dev/null +++ b/boolean_algebra/nor_gate.py @@ -0,0 +1,68 @@ +""" +A NOR Gate is a logic gate in boolean algebra which results in false(0) if any of the +inputs is 1, and True(1) if all inputs are 0. +Following is the truth table of a NOR Gate: + Truth Table of NOR Gate: + | Input 1 | Input 2 | Output | + | 0 | 0 | 1 | + | 0 | 1 | 0 | + | 1 | 0 | 0 | + | 1 | 1 | 0 | + + Code provided by Akshaj Vishwanathan +https://www.geeksforgeeks.org/logic-gates-in-python +""" + +from collections.abc import Callable + + +def nor_gate(input_1: int, input_2: int) -> int: + """ + >>> nor_gate(0, 0) + 1 + >>> nor_gate(0, 1) + 0 + >>> nor_gate(1, 0) + 0 + >>> nor_gate(1, 1) + 0 + >>> nor_gate(0.0, 0.0) + 1 + >>> nor_gate(0, -7) + 0 + """ + return int(input_1 == input_2 == 0) + + +def truth_table(func: Callable) -> str: + """ + >>> print(truth_table(nor_gate)) + Truth Table of NOR Gate: + | Input 1 | Input 2 | Output | + | 0 | 0 | 1 | + | 0 | 1 | 0 | + | 1 | 0 | 0 | + | 1 | 1 | 0 | + """ + + def make_table_row(items: list | tuple) -> str: + """ + >>> make_table_row(("One", "Two", "Three")) + '| One | Two | Three |' + """ + return f"| {' | '.join(f'{item:^8}' for item in items)} |" + + return "\n".join( + ( + "Truth Table of NOR Gate:", + make_table_row(("Input 1", "Input 2", "Output")), + *[make_table_row((i, j, func(i, j))) for i in (0, 1) for j in (0, 1)], + ) + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + print(truth_table(nor_gate)) diff --git a/boolean_algebra/not_gate.py b/boolean_algebra/not_gate.py new file mode 100644 index 000000000000..cfa74cf42204 --- /dev/null +++ b/boolean_algebra/not_gate.py @@ -0,0 +1,30 @@ +""" +A NOT Gate is a logic gate in boolean algebra which results to 0 (False) if the +input is high, and 1 (True) if the input is low. +Following is the truth table of a XOR Gate: + ------------------------------ + | Input | Output | + ------------------------------ + | 0 | 1 | + | 1 | 0 | + ------------------------------ +Refer - https://www.geeksforgeeks.org/logic-gates-in-python/ +""" + + +def not_gate(input_1: int) -> int: + """ + Calculate NOT of the input values + >>> not_gate(0) + 1 + >>> not_gate(1) + 0 + """ + + return 1 if input_1 == 0 else 0 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/boolean_algebra/or_gate.py b/boolean_algebra/or_gate.py new file mode 100644 index 000000000000..0fd4e5a5dc18 --- /dev/null +++ b/boolean_algebra/or_gate.py @@ -0,0 +1,35 @@ +""" +An OR Gate is a logic gate in boolean algebra which results to 0 (False) if both the +inputs are 0, and 1 (True) otherwise. +Following is the truth table of an AND Gate: + ------------------------------ + | Input 1 | Input 2 | Output | + ------------------------------ + | 0 | 0 | 0 | + | 0 | 1 | 1 | + | 1 | 0 | 1 | + | 1 | 1 | 1 | + ------------------------------ +Refer - https://www.geeksforgeeks.org/logic-gates-in-python/ +""" + + +def or_gate(input_1: int, input_2: int) -> int: + """ + Calculate OR of the input values + >>> or_gate(0, 0) + 0 + >>> or_gate(0, 1) + 1 + >>> or_gate(1, 0) + 1 + >>> or_gate(1, 1) + 1 + """ + return int((input_1, input_2).count(1) != 0) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/boolean_algebra/quine_mc_cluskey.py b/boolean_algebra/quine_mc_cluskey.py new file mode 100644 index 000000000000..8e22e66726d4 --- /dev/null +++ b/boolean_algebra/quine_mc_cluskey.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +from collections.abc import Sequence +from typing import Literal + + +def compare_string(string1: str, string2: str) -> str | Literal[False]: + """ + >>> compare_string('0010','0110') + '0_10' + + >>> compare_string('0110','1101') + False + """ + list1 = list(string1) + list2 = list(string2) + count = 0 + for i in range(len(list1)): + if list1[i] != list2[i]: + count += 1 + list1[i] = "_" + if count > 1: + return False + else: + return "".join(list1) + + +def check(binary: list[str]) -> list[str]: + """ + >>> check(['0.00.01.5']) + ['0.00.01.5'] + """ + pi = [] + while True: + check1 = ["$"] * len(binary) + temp = [] + for i in range(len(binary)): + for j in range(i + 1, len(binary)): + k = compare_string(binary[i], binary[j]) + if k is False: + check1[i] = "*" + check1[j] = "*" + temp.append("X") + for i in range(len(binary)): + if check1[i] == "$": + pi.append(binary[i]) + if len(temp) == 0: + return pi + binary = list(set(temp)) + + +def decimal_to_binary(no_of_variable: int, minterms: Sequence[float]) -> list[str]: + """ + >>> decimal_to_binary(3,[1.5]) + ['0.00.01.5'] + """ + temp = [] + for minterm in minterms: + string = "" + for _ in range(no_of_variable): + string = str(minterm % 2) + string + minterm //= 2 + temp.append(string) + return temp + + +def is_for_table(string1: str, string2: str, count: int) -> bool: + """ + >>> is_for_table('__1','011',2) + True + + >>> is_for_table('01_','001',1) + False + """ + list1 = list(string1) + list2 = list(string2) + count_n = sum(item1 != item2 for item1, item2 in zip(list1, list2)) + return count_n == count + + +def selection(chart: list[list[int]], prime_implicants: list[str]) -> list[str]: + """ + >>> selection([[1]],['0.00.01.5']) + ['0.00.01.5'] + + >>> selection([[1]],['0.00.01.5']) + ['0.00.01.5'] + """ + temp = [] + select = [0] * len(chart) + for i in range(len(chart[0])): + count = sum(row[i] == 1 for row in chart) + if count == 1: + rem = max(j for j, row in enumerate(chart) if row[i] == 1) + select[rem] = 1 + for i, item in enumerate(select): + if item != 1: + continue + for j in range(len(chart[0])): + if chart[i][j] != 1: + continue + for row in chart: + row[j] = 0 + temp.append(prime_implicants[i]) + while True: + counts = [chart[i].count(1) for i in range(len(chart))] + max_n = max(counts) + rem = counts.index(max_n) + + if max_n == 0: + return temp + + temp.append(prime_implicants[rem]) + + for j in range(len(chart[0])): + if chart[rem][j] != 1: + continue + for i in range(len(chart)): + chart[i][j] = 0 + + +def prime_implicant_chart( + prime_implicants: list[str], binary: list[str] +) -> list[list[int]]: + """ + >>> prime_implicant_chart(['0.00.01.5'],['0.00.01.5']) + [[1]] + """ + chart = [[0 for x in range(len(binary))] for x in range(len(prime_implicants))] + for i in range(len(prime_implicants)): + count = prime_implicants[i].count("_") + for j in range(len(binary)): + if is_for_table(prime_implicants[i], binary[j], count): + chart[i][j] = 1 + + return chart + + +def main() -> None: + no_of_variable = int(input("Enter the no. of variables\n")) + minterms = [ + float(x) + for x in input( + "Enter the decimal representation of Minterms 'Spaces Separated'\n" + ).split() + ] + binary = decimal_to_binary(no_of_variable, minterms) + + prime_implicants = check(binary) + print("Prime Implicants are:") + print(prime_implicants) + chart = prime_implicant_chart(prime_implicants, binary) + + essential_prime_implicants = selection(chart, prime_implicants) + print("Essential Prime Implicants are:") + print(essential_prime_implicants) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/boolean_algebra/xnor_gate.py b/boolean_algebra/xnor_gate.py new file mode 100644 index 000000000000..05b756da2960 --- /dev/null +++ b/boolean_algebra/xnor_gate.py @@ -0,0 +1,37 @@ +""" +A XNOR Gate is a logic gate in boolean algebra which results to 0 (False) if both the +inputs are different, and 1 (True), if the inputs are same. +It's similar to adding a NOT gate to an XOR gate + +Following is the truth table of a XNOR Gate: + ------------------------------ + | Input 1 | Input 2 | Output | + ------------------------------ + | 0 | 0 | 1 | + | 0 | 1 | 0 | + | 1 | 0 | 0 | + | 1 | 1 | 1 | + ------------------------------ +Refer - https://www.geeksforgeeks.org/logic-gates-in-python/ +""" + + +def xnor_gate(input_1: int, input_2: int) -> int: + """ + Calculate XOR of the input values + >>> xnor_gate(0, 0) + 1 + >>> xnor_gate(0, 1) + 0 + >>> xnor_gate(1, 0) + 0 + >>> xnor_gate(1, 1) + 1 + """ + return 1 if input_1 == input_2 else 0 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/boolean_algebra/xor_gate.py b/boolean_algebra/xor_gate.py new file mode 100644 index 000000000000..f3922e426e3d --- /dev/null +++ b/boolean_algebra/xor_gate.py @@ -0,0 +1,37 @@ +""" +A XOR Gate is a logic gate in boolean algebra which results to 1 (True) if only one of +the two inputs is 1, and 0 (False) if an even number of inputs are 1. +Following is the truth table of a XOR Gate: + ------------------------------ + | Input 1 | Input 2 | Output | + ------------------------------ + | 0 | 0 | 0 | + | 0 | 1 | 1 | + | 1 | 0 | 1 | + | 1 | 1 | 0 | + ------------------------------ + +Refer - https://www.geeksforgeeks.org/logic-gates-in-python/ +""" + + +def xor_gate(input_1: int, input_2: int) -> int: + """ + calculate xor of the input values + + >>> xor_gate(0, 0) + 0 + >>> xor_gate(0, 1) + 1 + >>> xor_gate(1, 0) + 1 + >>> xor_gate(1, 1) + 0 + """ + return (input_1, input_2).count(0) % 2 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/cellular_automata/README.md b/cellular_automata/README.md new file mode 100644 index 000000000000..c5681b33906c --- /dev/null +++ b/cellular_automata/README.md @@ -0,0 +1,8 @@ +# Cellular Automata + +Cellular automata are a way to simulate the behavior of "life", no matter if it is a robot or cell. +They usually follow simple rules but can lead to the creation of complex forms. +The most popular cellular automaton is Conway's [Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life). + +* +* diff --git a/cellular_automata/__init__.py b/cellular_automata/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/cellular_automata/conways_game_of_life.py b/cellular_automata/conways_game_of_life.py new file mode 100644 index 000000000000..4009d0e1b7ae --- /dev/null +++ b/cellular_automata/conways_game_of_life.py @@ -0,0 +1,97 @@ +""" +Conway's Game of Life implemented in Python. +https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life +""" + +from __future__ import annotations + +from PIL import Image + +# Define glider example +GLIDER = [ + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], +] + +# Define blinker example +BLINKER = [[0, 1, 0], [0, 1, 0], [0, 1, 0]] + + +def new_generation(cells: list[list[int]]) -> list[list[int]]: + """ + Generates the next generation for a given state of Conway's Game of Life. + >>> new_generation(BLINKER) + [[0, 0, 0], [1, 1, 1], [0, 0, 0]] + """ + next_generation = [] + for i in range(len(cells)): + next_generation_row = [] + for j in range(len(cells[i])): + # Get the number of live neighbours + neighbour_count = 0 + if i > 0 and j > 0: + neighbour_count += cells[i - 1][j - 1] + if i > 0: + neighbour_count += cells[i - 1][j] + if i > 0 and j < len(cells[i]) - 1: + neighbour_count += cells[i - 1][j + 1] + if j > 0: + neighbour_count += cells[i][j - 1] + if j < len(cells[i]) - 1: + neighbour_count += cells[i][j + 1] + if i < len(cells) - 1 and j > 0: + neighbour_count += cells[i + 1][j - 1] + if i < len(cells) - 1: + neighbour_count += cells[i + 1][j] + if i < len(cells) - 1 and j < len(cells[i]) - 1: + neighbour_count += cells[i + 1][j + 1] + + # Rules of the game of life (excerpt from Wikipedia): + # 1. Any live cell with two or three live neighbours survives. + # 2. Any dead cell with three live neighbours becomes a live cell. + # 3. All other live cells die in the next generation. + # Similarly, all other dead cells stay dead. + alive = cells[i][j] == 1 + if (alive and 2 <= neighbour_count <= 3) or ( + not alive and neighbour_count == 3 + ): + next_generation_row.append(1) + else: + next_generation_row.append(0) + + next_generation.append(next_generation_row) + return next_generation + + +def generate_images(cells: list[list[int]], frames: int) -> list[Image.Image]: + """ + Generates a list of images of subsequent Game of Life states. + """ + images = [] + for _ in range(frames): + # Create output image + img = Image.new("RGB", (len(cells[0]), len(cells))) + pixels = img.load() + assert pixels is not None + + # Save cells to image + for x in range(len(cells)): + for y in range(len(cells[0])): + colour = 255 - cells[y][x] * 255 + pixels[x, y] = (colour, colour, colour) + + # Save image + images.append(img) + cells = new_generation(cells) + return images + + +if __name__ == "__main__": + images = generate_images(GLIDER, 16) + images[0].save("out.gif", save_all=True, append_images=images[1:]) diff --git a/cellular_automata/elementary_cellular_automaton.py b/cellular_automata/elementary_cellular_automaton.py new file mode 100644 index 000000000000..582ef1736438 --- /dev/null +++ b/cellular_automata/elementary_cellular_automaton.py @@ -0,0 +1,74 @@ +""" +Elementary Cellular Automaton - Rule 30 +--------------------------------------- + +A one-dimensional cellular automaton introduced by Stephen Wolfram. +Each cell's next state depends on its current state and its two immediate neighbors. + +Reference: + https://en.wikipedia.org/wiki/Rule_30 +""" + + +def rule_30_step(current: list[int]) -> list[int]: + """ + Compute the next generation of a one-dimensional cellular automaton + following Wolfram's Rule 30. + + Each cell's next state is determined by its left, center, and right neighbors. + + Args: + current (list[int]): The current generation as a list of 0s (dead) + and 1s (alive). + + Returns: + list[int]: The next generation as a list of 0s and 1s. + + Example: + >>> rule_30_step([0, 0, 1, 0, 0]) + [0, 1, 1, 1, 0] + """ + next_gen = [] + for i in range(len(current)): + left = current[i - 1] if i > 0 else 0 + center = current[i] + right = current[i + 1] if i < len(current) - 1 else 0 + + # Combine neighbors into a 3-bit pattern + pattern = (left << 2) | (center << 1) | right + + # Rule 30 binary: 00011110 (bitwise representation of 30) + next_gen.append((30 >> pattern) & 1) + + return next_gen + + +def generate_rule_30(size: int = 31, generations: int = 15) -> list[list[int]]: + """ + Generate multiple generations of Rule 30 automaton. + + Args: + size (int): Number of cells in one generation. Default is 31. + generations (int): Number of generations to evolve. Default is 15. + + Returns: + list[list[int]]: A list of generations (each a list of 0s and 1s). + + Example: + >>> len(generate_rule_30(15, 5)) + 5 + """ + grid = [[0] * size for _ in range(generations)] + grid[0][size // 2] = 1 # Start with a single live cell in the middle + + for i in range(1, generations): + grid[i] = rule_30_step(grid[i - 1]) + + return grid + + +if __name__ == "__main__": + # Run an example simulation + generations = generate_rule_30(31, 15) + for row in generations: + print("".join("â–ˆ" if cell else " " for cell in row)) diff --git a/cellular_automata/game_of_life.py b/cellular_automata/game_of_life.py new file mode 100644 index 000000000000..8fef54add69a --- /dev/null +++ b/cellular_automata/game_of_life.py @@ -0,0 +1,205 @@ +"""Conway's Game Of Life, Author Anurag Kumar(mailto:anuragkumarak95@gmail.com) + +Requirements: + - numpy + - random + - time + - matplotlib + +Python: + - 3.5 + +Usage: + - $python3 game_of_life + +Game-Of-Life Rules: + + 1. + Any live cell with fewer than two live neighbours + dies, as if caused by under-population. + 2. + Any live cell with two or three live neighbours lives + on to the next generation. + 3. + Any live cell with more than three live neighbours + dies, as if by over-population. + 4. + Any dead cell with exactly three live neighbours be- + comes a live cell, as if by reproduction. +""" + +import random +import sys + +import numpy as np +from matplotlib import pyplot as plt +from matplotlib.colors import ListedColormap + +usage_doc = "Usage of script: script_name " + +choice = [0] * 100 + [1] * 10 +random.shuffle(choice) + + +def create_canvas(size: int) -> list[list[bool]]: + """ + Create a square canvas of given size filled with False (dead cells). + + Args: + size: The dimension of the square canvas + + Returns: + A size x size 2D list of boolean values, all initialized to False + + >>> canvas = create_canvas(3) + >>> len(canvas) + 3 + >>> len(canvas[0]) + 3 + >>> all(all(not cell for cell in row) for row in canvas) + True + >>> create_canvas(1) + [[False]] + >>> create_canvas(0) + [] + """ + canvas = [[False for i in range(size)] for j in range(size)] + return canvas + + +def seed(canvas: list[list[bool]]) -> None: + for i, row in enumerate(canvas): + for j, _ in enumerate(row): + canvas[i][j] = bool(random.getrandbits(1)) + + +def run(canvas: list[list[bool]]) -> list[list[bool]]: + """ + Run one generation of Conway's Game of Life on the canvas. + + Applies the Game of Life rules to all cells simultaneously to produce + the next generation. + + Args: + canvas: 2D list representing current state of cells + + Returns: + 2D list representing the next generation state + + >>> blinker = [[False, False, False, False, False], + ... [False, False, True, False, False], + ... [False, False, True, False, False], + ... [False, False, True, False, False], + ... [False, False, False, False, False]] + >>> result = run(blinker) + >>> result[2] + [False, True, True, True, False] + >>> run([[False, False, False], [False, False, False], [False, False, False]]) + [[False, False, False], [False, False, False], [False, False, False]] + >>> block = [[False, False, False, False], + ... [False, True, True, False], + ... [False, True, True, False], + ... [False, False, False, False]] + >>> run(block)[1] + [False, True, True, False] + """ + current_canvas = np.array(canvas) + next_gen_canvas = np.array(create_canvas(current_canvas.shape[0])) + for r, row in enumerate(current_canvas): + for c, pt in enumerate(row): + next_gen_canvas[r][c] = __judge_point( + pt, current_canvas[r - 1 : r + 2, c - 1 : c + 2] + ) + + return next_gen_canvas.tolist() + + +def __judge_point(pt: bool, neighbours: list[list[bool]]) -> bool: + """ + Apply Conway's Game of Life rules to determine the next state of a cell. + + Args: + pt: Current state of the cell (True=alive, False=dead) + neighbours: 3x3 grid including the cell and its 8 neighbors + + Returns: + The next state of the cell + + Rules: + 1. Live cell with <2 live neighbours dies (under-population) + 2. Live cell with 2-3 live neighbours survives + 3. Live cell with >3 live neighbours dies (over-population) + 4. Dead cell with exactly 3 live neighbours becomes alive + + >>> __judge_point( + ... True, [[True, True, False], [False, True, False], [False, False, False]] + ... ) + True + >>> __judge_point( + ... True, [[True, False, False], [False, True, False], [False, False, False]] + ... ) + False + >>> __judge_point( + ... True, [[True, True, True], [True, True, False], [False, False, False]] + ... ) + False + >>> __judge_point( + ... False, [[True, True, False], [True, False, False], [False, False, False]] + ... ) + True + >>> __judge_point( + ... False, [[True, False, False], [False, False, False], [False, False, False]] + ... ) + False + """ + dead = 0 + alive = 0 + # finding dead or alive neighbours count. + for i in neighbours: + for status in i: + if status: + alive += 1 + else: + dead += 1 + + # handling duplicate entry for focus pt. + if pt: + alive -= 1 + else: + dead -= 1 + + # running the rules of game here. + state = pt + if pt: + if alive < 2: + state = False + elif alive in {2, 3}: + state = True + elif alive > 3: + state = False + elif alive == 3: + state = True + + return state + + +if __name__ == "__main__": + if len(sys.argv) != 2: + raise Exception(usage_doc) + + canvas_size = int(sys.argv[1]) + # main working structure of this module. + c = create_canvas(canvas_size) + seed(c) + fig, ax = plt.subplots() + fig.show() + cmap = ListedColormap(["w", "k"]) + try: + while True: + c = run(c) + ax.matshow(c, cmap=cmap) + fig.canvas.draw() + ax.cla() + except KeyboardInterrupt: + # do nothing. + pass diff --git a/cellular_automata/langtons_ant.py b/cellular_automata/langtons_ant.py new file mode 100644 index 000000000000..9847c50a5c3e --- /dev/null +++ b/cellular_automata/langtons_ant.py @@ -0,0 +1,106 @@ +""" +Langton's ant + +@ https://en.wikipedia.org/wiki/Langton%27s_ant +@ https://upload.wikimedia.org/wikipedia/commons/0/09/LangtonsAntAnimated.gif +""" + +from functools import partial + +from matplotlib import pyplot as plt +from matplotlib.animation import FuncAnimation + +WIDTH = 80 +HEIGHT = 80 + + +class LangtonsAnt: + """ + Represents the main LangonsAnt algorithm. + + >>> la = LangtonsAnt(2, 2) + >>> la.board + [[True, True], [True, True]] + >>> la.ant_position + (1, 1) + """ + + def __init__(self, width: int, height: int) -> None: + # Each square is either True or False where True is white and False is black + self.board = [[True] * width for _ in range(height)] + self.ant_position: tuple[int, int] = (width // 2, height // 2) + + # Initially pointing left (similar to the wikipedia image) + # (0 = 0° | 1 = 90° | 2 = 180 ° | 3 = 270°) + self.ant_direction: int = 3 + + def move_ant(self, axes: plt.Axes | None, display: bool, _frame: int) -> None: + """ + Performs three tasks: + 1. The ant turns either clockwise or anti-clockwise according to the colour + of the square that it is currently on. If the square is white, the ant + turns clockwise, and if the square is black the ant turns anti-clockwise + 2. The ant moves one square in the direction that it is currently facing + 3. The square the ant was previously on is inverted (White -> Black and + Black -> White) + + If display is True, the board will also be displayed on the axes + + >>> la = LangtonsAnt(2, 2) + >>> la.move_ant(None, True, 0) + >>> la.board + [[True, True], [True, False]] + >>> la.move_ant(None, True, 0) + >>> la.board + [[True, False], [True, False]] + """ + directions = { + 0: (-1, 0), # 0° + 1: (0, 1), # 90° + 2: (1, 0), # 180° + 3: (0, -1), # 270° + } + x, y = self.ant_position + + # Turn clockwise or anti-clockwise according to colour of square + if self.board[x][y] is True: + # The square is white so turn 90° clockwise + self.ant_direction = (self.ant_direction + 1) % 4 + else: + # The square is black so turn 90° anti-clockwise + self.ant_direction = (self.ant_direction - 1) % 4 + + # Move ant + move_x, move_y = directions[self.ant_direction] + self.ant_position = (x + move_x, y + move_y) + + # Flip colour of square + self.board[x][y] = not self.board[x][y] + + if display and axes: + # Display the board on the axes + axes.get_xaxis().set_ticks([]) + axes.get_yaxis().set_ticks([]) + axes.imshow(self.board, cmap="gray", interpolation="nearest") + + def display(self, frames: int = 100_000) -> None: + """ + Displays the board without delay in a matplotlib plot + to visually understand and track the ant. + + >>> _ = LangtonsAnt(WIDTH, HEIGHT) + """ + fig, ax = plt.subplots() + # Assign animation to a variable to prevent it from getting garbage collected + self.animation = FuncAnimation( + fig, partial(self.move_ant, ax, True), frames=frames, interval=1 + ) + plt.show() + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + LangtonsAnt(WIDTH, HEIGHT).display() diff --git a/cellular_automata/nagel_schrekenberg.py b/cellular_automata/nagel_schrekenberg.py new file mode 100644 index 000000000000..bcdca902afee --- /dev/null +++ b/cellular_automata/nagel_schrekenberg.py @@ -0,0 +1,140 @@ +""" +Simulate the evolution of a highway with only one road that is a loop. +The highway is divided in cells, each cell can have at most one car in it. +The highway is a loop so when a car comes to one end, it will come out on the other. +Each car is represented by its speed (from 0 to 5). + +Some information about speed: + -1 means that the cell on the highway is empty + 0 to 5 are the speed of the cars with 0 being the lowest and 5 the highest + +highway: list[int] Where every position and speed of every car will be stored +probability The probability that a driver will slow down +initial_speed The speed of the cars a the start +frequency How many cells there are between two cars at the start +max_speed The maximum speed a car can go to +number_of_cells How many cell are there in the highway +number_of_update How many times will the position be updated + +More information here: https://en.wikipedia.org/wiki/Nagel%E2%80%93Schreckenberg_model + +Examples for doctest: +>>> simulate(construct_highway(6, 3, 0), 2, 0, 2) +[[0, -1, -1, 0, -1, -1], [-1, 1, -1, -1, 1, -1], [-1, -1, 1, -1, -1, 1]] +>>> simulate(construct_highway(5, 2, -2), 3, 0, 2) +[[0, -1, 0, -1, 0], [0, -1, 0, -1, -1], [0, -1, -1, 1, -1], [-1, 1, -1, 0, -1]] +""" + +from random import randint, random + + +def construct_highway( + number_of_cells: int, + frequency: int, + initial_speed: int, + random_frequency: bool = False, + random_speed: bool = False, + max_speed: int = 5, +) -> list: + """ + Build the highway following the parameters given + >>> construct_highway(10, 2, 6) + [[6, -1, 6, -1, 6, -1, 6, -1, 6, -1]] + >>> construct_highway(10, 10, 2) + [[2, -1, -1, -1, -1, -1, -1, -1, -1, -1]] + """ + + highway = [[-1] * number_of_cells] # Create a highway without any car + i = 0 + initial_speed = max(initial_speed, 0) + while i < number_of_cells: + highway[0][i] = ( + randint(0, max_speed) if random_speed else initial_speed + ) # Place the cars + i += ( + randint(1, max_speed * 2) if random_frequency else frequency + ) # Arbitrary number, may need tuning + return highway + + +def get_distance(highway_now: list, car_index: int) -> int: + """ + Get the distance between a car (at index car_index) and the next car + >>> get_distance([6, -1, 6, -1, 6], 2) + 1 + >>> get_distance([2, -1, -1, -1, 3, 1, 0, 1, 3, 2], 0) + 3 + >>> get_distance([-1, -1, -1, -1, 2, -1, -1, -1, 3], -1) + 4 + """ + + distance = 0 + cells = highway_now[car_index + 1 :] + for cell in range(len(cells)): # May need a better name for this + if cells[cell] != -1: # If the cell is not empty then + return distance # we have the distance we wanted + distance += 1 + # Here if the car is near the end of the highway + return distance + get_distance(highway_now, -1) + + +def update(highway_now: list, probability: float, max_speed: int) -> list: + """ + Update the speed of the cars + >>> update([-1, -1, -1, -1, -1, 2, -1, -1, -1, -1, 3], 0.0, 5) + [-1, -1, -1, -1, -1, 3, -1, -1, -1, -1, 4] + >>> update([-1, -1, 2, -1, -1, -1, -1, 3], 0.0, 5) + [-1, -1, 3, -1, -1, -1, -1, 1] + """ + + number_of_cells = len(highway_now) + # Beforce calculations, the highway is empty + next_highway = [-1] * number_of_cells + + for car_index in range(number_of_cells): + if highway_now[car_index] != -1: + # Add 1 to the current speed of the car and cap the speed + next_highway[car_index] = min(highway_now[car_index] + 1, max_speed) + # Number of empty cell before the next car + dn = get_distance(highway_now, car_index) - 1 + # We can't have the car causing an accident + next_highway[car_index] = min(next_highway[car_index], dn) + if random() < probability: + # Randomly, a driver will slow down + next_highway[car_index] = max(next_highway[car_index] - 1, 0) + return next_highway + + +def simulate( + highway: list, number_of_update: int, probability: float, max_speed: int +) -> list: + """ + The main function, it will simulate the evolution of the highway + >>> simulate([[-1, 2, -1, -1, -1, 3]], 2, 0.0, 3) + [[-1, 2, -1, -1, -1, 3], [-1, -1, -1, 2, -1, 0], [1, -1, -1, 0, -1, -1]] + >>> simulate([[-1, 2, -1, 3]], 4, 0.0, 3) + [[-1, 2, -1, 3], [-1, 0, -1, 0], [-1, 0, -1, 0], [-1, 0, -1, 0], [-1, 0, -1, 0]] + """ + + number_of_cells = len(highway[0]) + + for i in range(number_of_update): + next_speeds_calculated = update(highway[i], probability, max_speed) + real_next_speeds = [-1] * number_of_cells + + for car_index in range(number_of_cells): + speed = next_speeds_calculated[car_index] + if speed != -1: + # Change the position based on the speed (with % to create the loop) + index = (car_index + speed) % number_of_cells + # Commit the change of position + real_next_speeds[index] = speed + highway.append(real_next_speeds) + + return highway + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/cellular_automata/one_dimensional.py b/cellular_automata/one_dimensional.py new file mode 100644 index 000000000000..04d55c2473e8 --- /dev/null +++ b/cellular_automata/one_dimensional.py @@ -0,0 +1,75 @@ +""" +Return an image of 16 generations of one-dimensional cellular automata based on a given +ruleset number +https://mathworld.wolfram.com/ElementaryCellularAutomaton.html +""" + +from __future__ import annotations + +from PIL import Image + +# Define the first generation of cells +# fmt: off +CELLS = [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] +# fmt: on + + +def format_ruleset(ruleset: int) -> list[int]: + """ + >>> format_ruleset(11100) + [0, 0, 0, 1, 1, 1, 0, 0] + >>> format_ruleset(0) + [0, 0, 0, 0, 0, 0, 0, 0] + >>> format_ruleset(11111111) + [1, 1, 1, 1, 1, 1, 1, 1] + """ + return [int(c) for c in f"{ruleset:08}"[:8]] + + +def new_generation(cells: list[list[int]], rule: list[int], time: int) -> list[int]: + population = len(cells[0]) # 31 + next_generation = [] + for i in range(population): + # Get the neighbors of each cell + # Handle neighbours outside bounds by using 0 as their value + left_neighbor = 0 if i == 0 else cells[time][i - 1] + right_neighbor = 0 if i == population - 1 else cells[time][i + 1] + # Define a new cell and add it to the new generation + situation = 7 - int(f"{left_neighbor}{cells[time][i]}{right_neighbor}", 2) + next_generation.append(rule[situation]) + return next_generation + + +def generate_image(cells: list[list[int]]) -> Image.Image: + """ + Convert the cells into a greyscale PIL.Image.Image and return it to the caller. + >>> from random import random + >>> cells = [[random() for w in range(31)] for h in range(16)] + >>> img = generate_image(cells) + >>> isinstance(img, Image.Image) + True + >>> img.width, img.height + (31, 16) + """ + # Create the output image + img = Image.new("RGB", (len(cells[0]), len(cells))) + pixels = img.load() + assert pixels is not None + # Generates image + for w in range(img.width): + for h in range(img.height): + color = 255 - int(255 * cells[h][w]) + pixels[w, h] = (color, color, color) + return img + + +if __name__ == "__main__": + rule_num = bin(int(input("Rule:\n").strip()))[2:] + rule = format_ruleset(int(rule_num)) + for time in range(16): + CELLS.append(new_generation(CELLS, rule, time)) + img = generate_image(CELLS) + # Uncomment to save the image + # img.save(f"rule_{rule_num}.png") + img.show() diff --git a/cellular_automata/wa_tor.py b/cellular_automata/wa_tor.py new file mode 100644 index 000000000000..29f7ea510bfe --- /dev/null +++ b/cellular_automata/wa_tor.py @@ -0,0 +1,548 @@ +""" +Wa-Tor algorithm (1984) + +| @ https://en.wikipedia.org/wiki/Wa-Tor +| @ https://beltoforion.de/en/wator/ +| @ https://beltoforion.de/en/wator/images/wator_medium.webm + +This solution aims to completely remove any systematic approach +to the Wa-Tor planet, and utilise fully random methods. + +The constants are a working set that allows the Wa-Tor planet +to result in one of the three possible results. +""" + +from collections.abc import Callable +from random import randint, shuffle +from time import sleep +from typing import Literal + +WIDTH = 50 # Width of the Wa-Tor planet +HEIGHT = 50 # Height of the Wa-Tor planet + +PREY_INITIAL_COUNT = 30 # The initial number of prey entities +PREY_REPRODUCTION_TIME = 5 # The chronons before reproducing + +PREDATOR_INITIAL_COUNT = 50 # The initial number of predator entities +# The initial energy value of predator entities +PREDATOR_INITIAL_ENERGY_VALUE = 15 +# The energy value provided when consuming prey +PREDATOR_FOOD_VALUE = 5 +PREDATOR_REPRODUCTION_TIME = 20 # The chronons before reproducing + +MAX_ENTITIES = 500 # The max number of organisms on the board +# The number of entities to delete from the unbalanced side +DELETE_UNBALANCED_ENTITIES = 50 + + +class Entity: + """ + Represents an entity (either prey or predator). + + >>> e = Entity(True, coords=(0, 0)) + >>> e.prey + True + >>> e.coords + (0, 0) + >>> e.alive + True + """ + + def __init__(self, prey: bool, coords: tuple[int, int]) -> None: + self.prey = prey + # The (row, col) pos of the entity + self.coords = coords + + self.remaining_reproduction_time = ( + PREY_REPRODUCTION_TIME if prey else PREDATOR_REPRODUCTION_TIME + ) + self.energy_value = None if prey is True else PREDATOR_INITIAL_ENERGY_VALUE + self.alive = True + + def reset_reproduction_time(self) -> None: + """ + >>> e = Entity(True, coords=(0, 0)) + >>> e.reset_reproduction_time() + >>> e.remaining_reproduction_time == PREY_REPRODUCTION_TIME + True + >>> e = Entity(False, coords=(0, 0)) + >>> e.reset_reproduction_time() + >>> e.remaining_reproduction_time == PREDATOR_REPRODUCTION_TIME + True + """ + self.remaining_reproduction_time = ( + PREY_REPRODUCTION_TIME if self.prey is True else PREDATOR_REPRODUCTION_TIME + ) + + def __repr__(self) -> str: + """ + >>> Entity(prey=True, coords=(1, 1)) + Entity(prey=True, coords=(1, 1), remaining_reproduction_time=5) + >>> Entity(prey=False, coords=(2, 1)) # doctest: +NORMALIZE_WHITESPACE + Entity(prey=False, coords=(2, 1), + remaining_reproduction_time=20, energy_value=15) + """ + repr_ = ( + f"Entity(prey={self.prey}, coords={self.coords}, " + f"remaining_reproduction_time={self.remaining_reproduction_time}" + ) + if self.energy_value is not None: + repr_ += f", energy_value={self.energy_value}" + return f"{repr_})" + + +class WaTor: + """ + Represents the main Wa-Tor algorithm. + + :attr time_passed: A function that is called every time + time passes (a chronon) in order to visually display + the new Wa-Tor planet. The `time_passed` function can block + using ``time.sleep`` to slow the algorithm progression. + + >>> wt = WaTor(10, 15) + >>> wt.width + 10 + >>> wt.height + 15 + >>> len(wt.planet) + 15 + >>> len(wt.planet[0]) + 10 + >>> len(wt.get_entities()) == PREDATOR_INITIAL_COUNT + PREY_INITIAL_COUNT + True + """ + + time_passed: Callable[["WaTor", int], None] | None + + def __init__(self, width: int, height: int) -> None: + self.width = width + self.height = height + self.time_passed = None + + self.planet: list[list[Entity | None]] = [[None] * width for _ in range(height)] + + # Populate planet with predators and prey randomly + for _ in range(PREY_INITIAL_COUNT): + self.add_entity(prey=True) + for _ in range(PREDATOR_INITIAL_COUNT): + self.add_entity(prey=False) + self.set_planet(self.planet) + + def set_planet(self, planet: list[list[Entity | None]]) -> None: + """ + Ease of access for testing + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> planet = [ + ... [None, None, None], + ... [None, Entity(True, coords=(1, 1)), None] + ... ] + >>> wt.set_planet(planet) + >>> wt.planet == planet + True + >>> wt.width + 3 + >>> wt.height + 2 + """ + self.planet = planet + self.width = len(planet[0]) + self.height = len(planet) + + def add_entity(self, prey: bool) -> None: + """ + Adds an entity, making sure the entity does + not override another entity + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.set_planet([[None, None], [None, None]]) + >>> wt.add_entity(True) + >>> len(wt.get_entities()) + 1 + >>> wt.add_entity(False) + >>> len(wt.get_entities()) + 2 + """ + while True: + row, col = randint(0, self.height - 1), randint(0, self.width - 1) + if self.planet[row][col] is None: + self.planet[row][col] = Entity(prey=prey, coords=(row, col)) + return + + def get_entities(self) -> list[Entity]: + """ + Returns a list of all the entities within the planet. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> len(wt.get_entities()) == PREDATOR_INITIAL_COUNT + PREY_INITIAL_COUNT + True + """ + return [entity for column in self.planet for entity in column if entity] + + def balance_predators_and_prey(self) -> None: + """ + Balances predators and preys so that prey + can not dominate the predators, blocking up + space for them to reproduce. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> for i in range(2000): + ... row, col = i // HEIGHT, i % WIDTH + ... wt.planet[row][col] = Entity(True, coords=(row, col)) + >>> entities = len(wt.get_entities()) + >>> wt.balance_predators_and_prey() + >>> len(wt.get_entities()) == entities + False + """ + entities = self.get_entities() + shuffle(entities) + + if len(entities) >= MAX_ENTITIES - MAX_ENTITIES / 10: + prey = [entity for entity in entities if entity.prey] + predators = [entity for entity in entities if not entity.prey] + + prey_count, predator_count = len(prey), len(predators) + + entities_to_purge = ( + prey[:DELETE_UNBALANCED_ENTITIES] + if prey_count > predator_count + else predators[:DELETE_UNBALANCED_ENTITIES] + ) + for entity in entities_to_purge: + self.planet[entity.coords[0]][entity.coords[1]] = None + + def get_surrounding_prey(self, entity: Entity) -> list[Entity]: + """ + Returns all the prey entities around (N, S, E, W) a predator entity. + + Subtly different to the `move_and_reproduce`. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.set_planet([ + ... [None, Entity(True, (0, 1)), None], + ... [None, Entity(False, (1, 1)), None], + ... [None, Entity(True, (2, 1)), None]]) + >>> wt.get_surrounding_prey( + ... Entity(False, (1, 1))) # doctest: +NORMALIZE_WHITESPACE + [Entity(prey=True, coords=(0, 1), remaining_reproduction_time=5), + Entity(prey=True, coords=(2, 1), remaining_reproduction_time=5)] + >>> wt.set_planet([[Entity(False, (0, 0))]]) + >>> wt.get_surrounding_prey(Entity(False, (0, 0))) + [] + >>> wt.set_planet([ + ... [Entity(True, (0, 0)), Entity(False, (1, 0)), Entity(False, (2, 0))], + ... [None, Entity(False, (1, 1)), Entity(True, (2, 1))], + ... [None, None, None]]) + >>> wt.get_surrounding_prey(Entity(False, (1, 0))) + [Entity(prey=True, coords=(0, 0), remaining_reproduction_time=5)] + """ + row, col = entity.coords + adjacent: list[tuple[int, int]] = [ + (row - 1, col), # North + (row + 1, col), # South + (row, col - 1), # West + (row, col + 1), # East + ] + + return [ + ent + for r, c in adjacent + if 0 <= r < self.height + and 0 <= c < self.width + and (ent := self.planet[r][c]) is not None + and ent.prey + ] + + def move_and_reproduce( + self, entity: Entity, direction_orders: list[Literal["N", "E", "S", "W"]] + ) -> None: + """ + Attempts to move to an unoccupied neighbouring square + in either of the four directions (North, South, East, West). + If the move was successful and the `remaining_reproduction_time` is + equal to 0, then a new prey or predator can also be created + in the previous square. + + :param direction_orders: Ordered list (like priority queue) depicting + order to attempt to move. Removes any systematic + approach of checking neighbouring squares. + + >>> planet = [ + ... [None, None, None], + ... [None, Entity(True, coords=(1, 1)), None], + ... [None, None, None] + ... ] + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.set_planet(planet) + >>> wt.move_and_reproduce(Entity(True, coords=(1, 1)), direction_orders=["N"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[None, Entity(prey=True, coords=(0, 1), remaining_reproduction_time=4), None], + [None, None, None], + [None, None, None]] + >>> wt.planet[0][0] = Entity(True, coords=(0, 0)) + >>> wt.move_and_reproduce(Entity(True, coords=(0, 1)), + ... direction_orders=["N", "W", "E", "S"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[Entity(prey=True, coords=(0, 0), remaining_reproduction_time=5), None, + Entity(prey=True, coords=(0, 2), remaining_reproduction_time=4)], + [None, None, None], + [None, None, None]] + >>> wt.planet[0][1] = wt.planet[0][2] + >>> wt.planet[0][2] = None + >>> wt.move_and_reproduce(Entity(True, coords=(0, 1)), + ... direction_orders=["N", "W", "S", "E"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[Entity(prey=True, coords=(0, 0), remaining_reproduction_time=5), None, None], + [None, Entity(prey=True, coords=(1, 1), remaining_reproduction_time=4), None], + [None, None, None]] + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> reproducable_entity = Entity(False, coords=(0, 1)) + >>> reproducable_entity.remaining_reproduction_time = 0 + >>> wt.planet = [[None, reproducable_entity]] + >>> wt.move_and_reproduce(reproducable_entity, + ... direction_orders=["N", "W", "S", "E"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[Entity(prey=False, coords=(0, 0), + remaining_reproduction_time=20, energy_value=15), + Entity(prey=False, coords=(0, 1), remaining_reproduction_time=20, + energy_value=15)]] + """ + row, col = coords = entity.coords + + adjacent_squares: dict[Literal["N", "E", "S", "W"], tuple[int, int]] = { + "N": (row - 1, col), # North + "S": (row + 1, col), # South + "W": (row, col - 1), # West + "E": (row, col + 1), # East + } + # Weight adjacent locations + adjacent: list[tuple[int, int]] = [] + for order in direction_orders: + adjacent.append(adjacent_squares[order]) + + for r, c in adjacent: + if ( + 0 <= r < self.height + and 0 <= c < self.width + and self.planet[r][c] is None + ): + # Move entity to empty adjacent square + self.planet[r][c] = entity + self.planet[row][col] = None + entity.coords = (r, c) + break + + # (2.) See if it possible to reproduce in previous square + if coords != entity.coords and entity.remaining_reproduction_time <= 0: + # Check if the entities on the planet is less than the max limit + if len(self.get_entities()) < MAX_ENTITIES: + # Reproduce in previous square + self.planet[row][col] = Entity(prey=entity.prey, coords=coords) + entity.reset_reproduction_time() + else: + entity.remaining_reproduction_time -= 1 + + def perform_prey_actions( + self, entity: Entity, direction_orders: list[Literal["N", "E", "S", "W"]] + ) -> None: + """ + Performs the actions for a prey entity + + For prey the rules are: + 1. At each chronon, a prey moves randomly to one of the adjacent unoccupied + squares. If there are no free squares, no movement takes place. + 2. Once a prey has survived a certain number of chronons it may reproduce. + This is done as it moves to a neighbouring square, + leaving behind a new prey in its old position. + Its reproduction time is also reset to zero. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> reproducable_entity = Entity(True, coords=(0, 1)) + >>> reproducable_entity.remaining_reproduction_time = 0 + >>> wt.planet = [[None, reproducable_entity]] + >>> wt.perform_prey_actions(reproducable_entity, + ... direction_orders=["N", "W", "S", "E"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[Entity(prey=True, coords=(0, 0), remaining_reproduction_time=5), + Entity(prey=True, coords=(0, 1), remaining_reproduction_time=5)]] + """ + self.move_and_reproduce(entity, direction_orders) + + def perform_predator_actions( + self, + entity: Entity, + occupied_by_prey_coords: tuple[int, int] | None, + direction_orders: list[Literal["N", "E", "S", "W"]], + ) -> None: + """ + Performs the actions for a predator entity + + :param occupied_by_prey_coords: Move to this location if there is prey there + + For predators the rules are: + 1. At each chronon, a predator moves randomly to an adjacent square occupied + by a prey. If there is none, the predator moves to a random adjacent + unoccupied square. If there are no free squares, no movement takes place. + 2. At each chronon, each predator is deprived of a unit of energy. + 3. Upon reaching zero energy, a predator dies. + 4. If a predator moves to a square occupied by a prey, + it eats the prey and earns a certain amount of energy. + 5. Once a predator has survived a certain number of chronons + it may reproduce in exactly the same way as the prey. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.set_planet([[Entity(True, coords=(0, 0)), Entity(False, coords=(0, 1))]]) + >>> wt.perform_predator_actions(Entity(False, coords=(0, 1)), (0, 0), []) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[Entity(prey=False, coords=(0, 0), + remaining_reproduction_time=20, energy_value=19), None]] + """ + assert entity.energy_value is not None # [type checking] + + # (3.) If the entity has 0 energy, it will die + if entity.energy_value == 0: + self.planet[entity.coords[0]][entity.coords[1]] = None + return + + # (1.) Move to entity if possible + if occupied_by_prey_coords is not None: + # Kill the prey + prey = self.planet[occupied_by_prey_coords[0]][occupied_by_prey_coords[1]] + assert prey is not None + prey.alive = False + + # Move onto prey + self.planet[occupied_by_prey_coords[0]][occupied_by_prey_coords[1]] = entity + self.planet[entity.coords[0]][entity.coords[1]] = None + + entity.coords = occupied_by_prey_coords + # (4.) Eats the prey and earns energy + entity.energy_value += PREDATOR_FOOD_VALUE + else: + # (5.) If it has survived the certain number of chronons it will also + # reproduce in this function + self.move_and_reproduce(entity, direction_orders) + + # (2.) Each chronon, the predator is deprived of a unit of energy + entity.energy_value -= 1 + + def run(self, *, iteration_count: int) -> None: + """ + Emulate time passing by looping `iteration_count` times + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.run(iteration_count=PREDATOR_INITIAL_ENERGY_VALUE - 1) + >>> len(list(filter(lambda entity: entity.prey is False, + ... wt.get_entities()))) >= PREDATOR_INITIAL_COUNT + True + """ + for iter_num in range(iteration_count): + # Generate list of all entities in order to randomly + # pop an entity at a time to simulate true randomness + # This removes the systematic approach of iterating + # through each entity width by height + all_entities = self.get_entities() + + for __ in range(len(all_entities)): + entity = all_entities.pop(randint(0, len(all_entities) - 1)) + if entity.alive is False: + continue + + directions: list[Literal["N", "E", "S", "W"]] = ["N", "E", "S", "W"] + shuffle(directions) # Randomly shuffle directions + + if entity.prey: + self.perform_prey_actions(entity, directions) + else: + # Create list of surrounding prey + surrounding_prey = self.get_surrounding_prey(entity) + surrounding_prey_coords = None + + if surrounding_prey: + # Again, randomly shuffle directions + shuffle(surrounding_prey) + surrounding_prey_coords = surrounding_prey[0].coords + + self.perform_predator_actions( + entity, surrounding_prey_coords, directions + ) + + # Balance out the predators and prey + self.balance_predators_and_prey() + + if self.time_passed is not None: + # Call time_passed function for Wa-Tor planet + # visualisation in a terminal or a graph. + self.time_passed(self, iter_num) + + +def visualise(wt: WaTor, iter_number: int, *, colour: bool = True) -> None: + """ + Visually displays the Wa-Tor planet using + an ascii code in terminal to clear and re-print + the Wa-Tor planet at intervals. + + Uses ascii colour codes to colourfully display the predators and prey: + * (0x60f197) Prey = ``#`` + * (0xfffff) Predator = ``x`` + + >>> wt = WaTor(30, 30) + >>> wt.set_planet([ + ... [Entity(True, coords=(0, 0)), Entity(False, coords=(0, 1)), None], + ... [Entity(False, coords=(1, 0)), None, Entity(False, coords=(1, 2))], + ... [None, Entity(True, coords=(2, 1)), None] + ... ]) + >>> visualise(wt, 0, colour=False) # doctest: +NORMALIZE_WHITESPACE + # x . + x . x + . # . + + Iteration: 0 | Prey count: 2 | Predator count: 3 | + """ + if colour: + __import__("os").system("") + print("\x1b[0;0H\x1b[2J\x1b[?25l") + + reprint = "\x1b[0;0H" if colour else "" + ansi_colour_end = "\x1b[0m " if colour else " " + + planet = wt.planet + output = "" + + # Iterate over every entity in the planet + for row in planet: + for entity in row: + if entity is None: + output += " . " + else: + if colour is True: + output += ( + "\x1b[38;2;96;241;151m" + if entity.prey + else "\x1b[38;2;255;255;15m" + ) + output += f" {'#' if entity.prey else 'x'}{ansi_colour_end}" + + output += "\n" + + entities = wt.get_entities() + prey_count = sum(entity.prey for entity in entities) + + print( + f"{output}\n Iteration: {iter_number} | Prey count: {prey_count} | " + f"Predator count: {len(entities) - prey_count} | {reprint}" + ) + # Block the thread to be able to visualise seeing the algorithm + sleep(0.05) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + wt = WaTor(WIDTH, HEIGHT) + wt.time_passed = visualise + wt.run(iteration_count=100_000) diff --git a/ciphers/README.md b/ciphers/README.md new file mode 100644 index 000000000000..fa09874f38e5 --- /dev/null +++ b/ciphers/README.md @@ -0,0 +1,7 @@ +# Ciphers + +Ciphers are used to protect data from people that are not allowed to have it. They are everywhere on the internet to protect your connections. + +* +* +* diff --git a/ciphers/__init__.py b/ciphers/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/ciphers/a1z26.py b/ciphers/a1z26.py new file mode 100644 index 000000000000..d99efe186519 --- /dev/null +++ b/ciphers/a1z26.py @@ -0,0 +1,49 @@ +""" +Convert a string of characters to a sequence of numbers +corresponding to the character's position in the alphabet. + +https://www.dcode.fr/letter-number-cipher +http://bestcodes.weebly.com/a1z26.html +""" + +from __future__ import annotations + + +def encode(plain: str) -> list[int]: + """ + >>> encode("myname") + [13, 25, 14, 1, 13, 5] + >>> encode("abCd") + Traceback (most recent call last): + ... + ValueError: plain must contain only lowercase letters (a-z) + >>> encode("n0w") + Traceback (most recent call last): + ... + ValueError: plain must contain only lowercase letters (a-z) + >>> encode("later!") + Traceback (most recent call last): + ... + ValueError: plain must contain only lowercase letters (a-z) + """ + if not plain.islower() or not plain.isalpha(): + raise ValueError("plain must contain only lowercase letters (a-z)") + return [ord(elem) - 96 for elem in plain] + + +def decode(encoded: list[int]) -> str: + """ + >>> decode([13, 25, 14, 1, 13, 5]) + 'myname' + """ + return "".join(chr(elem + 96) for elem in encoded) + + +def main() -> None: + encoded = encode(input("-> ").strip().lower()) + print("Encoded: ", encoded) + print("Decoded:", decode(encoded)) + + +if __name__ == "__main__": + main() diff --git a/ciphers/affine_cipher.py b/ciphers/affine_cipher.py index b74ec6f4ed60..10d16367cced 100644 --- a/ciphers/affine_cipher.py +++ b/ciphers/affine_cipher.py @@ -1,76 +1,109 @@ -import sys, random, cryptomath_module as cryptoMath - -SYMBOLS = """ !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~""" - -def main(): - message = input('Enter message: ') - key = int(input('Enter key [2000 - 9000]: ')) - mode = input('Encrypt/Decrypt [E/D]: ') - - if mode.lower().startswith('e'): - mode = 'encrypt' - translated = encryptMessage(key, message) - elif mode.lower().startswith('d'): - mode = 'decrypt' - translated = decryptMessage(key, message) - print('\n%sed text: \n%s' % (mode.title(), translated)) - -def getKeyParts(key): - keyA = key // len(SYMBOLS) - keyB = key % len(SYMBOLS) - return (keyA, keyB) - -def checkKeys(keyA, keyB, mode): - if keyA == 1 and mode == 'encrypt': - sys.exit('The affine cipher becomes weak when key A is set to 1. Choose different key') - if keyB == 0 and mode == 'encrypt': - sys.exit('The affine cipher becomes weak when key A is set to 1. Choose different key') - if keyA < 0 or keyB < 0 or keyB > len(SYMBOLS) - 1: - sys.exit('Key A must be greater than 0 and key B must be between 0 and %s.' % (len(SYMBOLS) - 1)) - if cryptoMath.gcd(keyA, len(SYMBOLS)) != 1: - sys.exit('Key A %s and the symbol set size %s are not relatively prime. Choose a different key.' % (keyA, len(SYMBOLS))) - -def encryptMessage(key, message): - ''' - >>> encryptMessage(4545, 'The affine cipher is a type of monoalphabetic substitution cipher.') +import random +import sys + +from maths.greatest_common_divisor import gcd_by_iterative + +from . import cryptomath_module as cryptomath + +SYMBOLS = ( + r""" !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`""" + r"""abcdefghijklmnopqrstuvwxyz{|}~""" +) + + +def check_keys(key_a: int, key_b: int, mode: str) -> None: + if mode == "encrypt": + if key_a == 1: + sys.exit( + "The affine cipher becomes weak when key " + "A is set to 1. Choose different key" + ) + if key_b == 0: + sys.exit( + "The affine cipher becomes weak when key " + "B is set to 0. Choose different key" + ) + if key_a < 0 or key_b < 0 or key_b > len(SYMBOLS) - 1: + sys.exit( + "Key A must be greater than 0 and key B must " + f"be between 0 and {len(SYMBOLS) - 1}." + ) + if gcd_by_iterative(key_a, len(SYMBOLS)) != 1: + sys.exit( + f"Key A {key_a} and the symbol set size {len(SYMBOLS)} " + "are not relatively prime. Choose a different key." + ) + + +def encrypt_message(key: int, message: str) -> str: + """ + >>> encrypt_message(4545, 'The affine cipher is a type of monoalphabetic ' + ... 'substitution cipher.') 'VL}p MM{I}p~{HL}Gp{vp pFsH}pxMpyxIx JHL O}F{~pvuOvF{FuF{xIp~{HL}Gi' - ''' - keyA, keyB = getKeyParts(key) - checkKeys(keyA, keyB, 'encrypt') - cipherText = '' + """ + key_a, key_b = divmod(key, len(SYMBOLS)) + check_keys(key_a, key_b, "encrypt") + cipher_text = "" for symbol in message: if symbol in SYMBOLS: - symIndex = SYMBOLS.find(symbol) - cipherText += SYMBOLS[(symIndex * keyA + keyB) % len(SYMBOLS)] + sym_index = SYMBOLS.find(symbol) + cipher_text += SYMBOLS[(sym_index * key_a + key_b) % len(SYMBOLS)] else: - cipherText += symbol - return cipherText + cipher_text += symbol + return cipher_text -def decryptMessage(key, message): - ''' - >>> decryptMessage(4545, 'VL}p MM{I}p~{HL}Gp{vp pFsH}pxMpyxIx JHL O}F{~pvuOvF{FuF{xIp~{HL}Gi') + +def decrypt_message(key: int, message: str) -> str: + """ + >>> decrypt_message(4545, 'VL}p MM{I}p~{HL}Gp{vp pFsH}pxMpyxIx JHL O}F{~pvuOvF{FuF' + ... '{xIp~{HL}Gi') 'The affine cipher is a type of monoalphabetic substitution cipher.' - ''' - keyA, keyB = getKeyParts(key) - checkKeys(keyA, keyB, 'decrypt') - plainText = '' - modInverseOfkeyA = cryptoMath.findModInverse(keyA, len(SYMBOLS)) + """ + key_a, key_b = divmod(key, len(SYMBOLS)) + check_keys(key_a, key_b, "decrypt") + plain_text = "" + mod_inverse_of_key_a = cryptomath.find_mod_inverse(key_a, len(SYMBOLS)) for symbol in message: if symbol in SYMBOLS: - symIndex = SYMBOLS.find(symbol) - plainText += SYMBOLS[(symIndex - keyB) * modInverseOfkeyA % len(SYMBOLS)] + sym_index = SYMBOLS.find(symbol) + plain_text += SYMBOLS[ + (sym_index - key_b) * mod_inverse_of_key_a % len(SYMBOLS) + ] else: - plainText += symbol - return plainText + plain_text += symbol + return plain_text + -def getRandomKey(): +def get_random_key() -> int: while True: - keyA = random.randint(2, len(SYMBOLS)) - keyB = random.randint(2, len(SYMBOLS)) - if cryptoMath.gcd(keyA, len(SYMBOLS)) == 1: - return keyA * len(SYMBOLS) + keyB + key_b = random.randint(2, len(SYMBOLS)) + key_b = random.randint(2, len(SYMBOLS)) + if gcd_by_iterative(key_b, len(SYMBOLS)) == 1 and key_b % len(SYMBOLS) != 0: + return key_b * len(SYMBOLS) + key_b + + +def main() -> None: + """ + >>> key = get_random_key() + >>> msg = "This is a test!" + >>> decrypt_message(key, encrypt_message(key, msg)) == msg + True + """ + message = input("Enter message: ").strip() + key = int(input("Enter key [2000 - 9000]: ").strip()) + mode = input("Encrypt/Decrypt [E/D]: ").strip().lower() -if __name__ == '__main__': + if mode.startswith("e"): + mode = "encrypt" + translated = encrypt_message(key, message) + elif mode.startswith("d"): + mode = "decrypt" + translated = decrypt_message(key, message) + print(f"\n{mode.title()}ed text: \n{translated}") + + +if __name__ == "__main__": import doctest + doctest.testmod() - main() + # main() diff --git a/ciphers/atbash.py b/ciphers/atbash.py new file mode 100644 index 000000000000..4e8f663ed02d --- /dev/null +++ b/ciphers/atbash.py @@ -0,0 +1,54 @@ +"""https://en.wikipedia.org/wiki/Atbash""" + +import string + + +def atbash_slow(sequence: str) -> str: + """ + >>> atbash_slow("ABCDEFG") + 'ZYXWVUT' + + >>> atbash_slow("aW;;123BX") + 'zD;;123YC' + """ + output = "" + for i in sequence: + extract = ord(i) + if 65 <= extract <= 90: + output += chr(155 - extract) + elif 97 <= extract <= 122: + output += chr(219 - extract) + else: + output += i + return output + + +def atbash(sequence: str) -> str: + """ + >>> atbash("ABCDEFG") + 'ZYXWVUT' + + >>> atbash("aW;;123BX") + 'zD;;123YC' + """ + letters = string.ascii_letters + letters_reversed = string.ascii_lowercase[::-1] + string.ascii_uppercase[::-1] + return "".join( + letters_reversed[letters.index(c)] if c in letters else c for c in sequence + ) + + +def benchmark() -> None: + """Let's benchmark our functions side-by-side...""" + from timeit import timeit + + print("Running performance benchmarks...") + setup = "from string import printable ; from __main__ import atbash, atbash_slow" + print(f"> atbash_slow(): {timeit('atbash_slow(printable)', setup=setup)} seconds") + print(f"> atbash(): {timeit('atbash(printable)', setup=setup)} seconds") + + +if __name__ == "__main__": + for example in ("ABCDEFGH", "123GGjj", "testStringtest", "with space"): + print(f"{example} encrypted in atbash: {atbash(example)}") + benchmark() diff --git a/ciphers/autokey.py b/ciphers/autokey.py new file mode 100644 index 000000000000..0b269dc0adad --- /dev/null +++ b/ciphers/autokey.py @@ -0,0 +1,151 @@ +""" +https://en.wikipedia.org/wiki/Autokey_cipher + +An autokey cipher (also known as the autoclave cipher) is a cipher that +incorporates the message (the plaintext) into the key. +The key is generated from the message in some automated fashion, +sometimes by selecting certain letters from the text or, more commonly, +by adding a short primer key to the front of the message. +""" + + +def encrypt(plaintext: str, key: str) -> str: + """ + Encrypt a given `plaintext` (string) and `key` (string), returning the + encrypted ciphertext. + + >>> encrypt("hello world", "coffee") + 'jsqqs avvwo' + >>> encrypt("coffee is good as python", "TheAlgorithms") + 'vvjfpk wj ohvp su ddylsv' + >>> encrypt("coffee is good as python", 2) + Traceback (most recent call last): + ... + TypeError: key must be a string + >>> encrypt("", "TheAlgorithms") + Traceback (most recent call last): + ... + ValueError: plaintext is empty + >>> encrypt("coffee is good as python", "") + Traceback (most recent call last): + ... + ValueError: key is empty + >>> encrypt(527.26, "TheAlgorithms") + Traceback (most recent call last): + ... + TypeError: plaintext must be a string + """ + if not isinstance(plaintext, str): + raise TypeError("plaintext must be a string") + if not isinstance(key, str): + raise TypeError("key must be a string") + + if not plaintext: + raise ValueError("plaintext is empty") + if not key: + raise ValueError("key is empty") + + key += plaintext + plaintext = plaintext.lower() + key = key.lower() + plaintext_iterator = 0 + key_iterator = 0 + ciphertext = "" + while plaintext_iterator < len(plaintext): + if ( + ord(plaintext[plaintext_iterator]) < 97 + or ord(plaintext[plaintext_iterator]) > 122 + ): + ciphertext += plaintext[plaintext_iterator] + plaintext_iterator += 1 + elif ord(key[key_iterator]) < 97 or ord(key[key_iterator]) > 122: + key_iterator += 1 + else: + ciphertext += chr( + ( + (ord(plaintext[plaintext_iterator]) - 97 + ord(key[key_iterator])) + - 97 + ) + % 26 + + 97 + ) + key_iterator += 1 + plaintext_iterator += 1 + return ciphertext + + +def decrypt(ciphertext: str, key: str) -> str: + """ + Decrypt a given `ciphertext` (string) and `key` (string), returning the decrypted + ciphertext. + + >>> decrypt("jsqqs avvwo", "coffee") + 'hello world' + >>> decrypt("vvjfpk wj ohvp su ddylsv", "TheAlgorithms") + 'coffee is good as python' + >>> decrypt("vvjfpk wj ohvp su ddylsv", "") + Traceback (most recent call last): + ... + ValueError: key is empty + >>> decrypt(527.26, "TheAlgorithms") + Traceback (most recent call last): + ... + TypeError: ciphertext must be a string + >>> decrypt("", "TheAlgorithms") + Traceback (most recent call last): + ... + ValueError: ciphertext is empty + >>> decrypt("vvjfpk wj ohvp su ddylsv", 2) + Traceback (most recent call last): + ... + TypeError: key must be a string + """ + if not isinstance(ciphertext, str): + raise TypeError("ciphertext must be a string") + if not isinstance(key, str): + raise TypeError("key must be a string") + + if not ciphertext: + raise ValueError("ciphertext is empty") + if not key: + raise ValueError("key is empty") + + key = key.lower() + ciphertext_iterator = 0 + key_iterator = 0 + plaintext = "" + while ciphertext_iterator < len(ciphertext): + if ( + ord(ciphertext[ciphertext_iterator]) < 97 + or ord(ciphertext[ciphertext_iterator]) > 122 + ): + plaintext += ciphertext[ciphertext_iterator] + else: + plaintext += chr( + (ord(ciphertext[ciphertext_iterator]) - ord(key[key_iterator])) % 26 + + 97 + ) + key += chr( + (ord(ciphertext[ciphertext_iterator]) - ord(key[key_iterator])) % 26 + + 97 + ) + key_iterator += 1 + ciphertext_iterator += 1 + return plaintext + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + operation = int(input("Type 1 to encrypt or 2 to decrypt:")) + match operation: + case 1: + plaintext = input("Typeplaintext to be encrypted:\n") + key = input("Type the key:\n") + print(encrypt(plaintext, key)) + case 2: + ciphertext = input("Type the ciphertext to be decrypted:\n") + key = input("Type the key:\n") + print(decrypt(ciphertext, key)) + decrypt("jsqqs avvwo", "coffee") diff --git a/ciphers/baconian_cipher.py b/ciphers/baconian_cipher.py new file mode 100644 index 000000000000..f146ba91b78f --- /dev/null +++ b/ciphers/baconian_cipher.py @@ -0,0 +1,89 @@ +""" +Program to encode and decode Baconian or Bacon's Cipher +Wikipedia reference : https://en.wikipedia.org/wiki/Bacon%27s_cipher +""" + +encode_dict = { + "a": "AAAAA", + "b": "AAAAB", + "c": "AAABA", + "d": "AAABB", + "e": "AABAA", + "f": "AABAB", + "g": "AABBA", + "h": "AABBB", + "i": "ABAAA", + "j": "BBBAA", + "k": "ABAAB", + "l": "ABABA", + "m": "ABABB", + "n": "ABBAA", + "o": "ABBAB", + "p": "ABBBA", + "q": "ABBBB", + "r": "BAAAA", + "s": "BAAAB", + "t": "BAABA", + "u": "BAABB", + "v": "BBBAB", + "w": "BABAA", + "x": "BABAB", + "y": "BABBA", + "z": "BABBB", + " ": " ", +} + + +decode_dict = {value: key for key, value in encode_dict.items()} + + +def encode(word: str) -> str: + """ + Encodes to Baconian cipher + + >>> encode("hello") + 'AABBBAABAAABABAABABAABBAB' + >>> encode("hello world") + 'AABBBAABAAABABAABABAABBAB BABAAABBABBAAAAABABAAAABB' + >>> encode("hello world!") + Traceback (most recent call last): + ... + Exception: encode() accepts only letters of the alphabet and spaces + """ + encoded = "" + for letter in word.lower(): + if letter.isalpha() or letter == " ": + encoded += encode_dict[letter] + else: + raise Exception("encode() accepts only letters of the alphabet and spaces") + return encoded + + +def decode(coded: str) -> str: + """ + Decodes from Baconian cipher + + >>> decode("AABBBAABAAABABAABABAABBAB BABAAABBABBAAAAABABAAAABB") + 'hello world' + >>> decode("AABBBAABAAABABAABABAABBAB") + 'hello' + >>> decode("AABBBAABAAABABAABABAABBAB BABAAABBABBAAAAABABAAAABB!") + Traceback (most recent call last): + ... + Exception: decode() accepts only 'A', 'B' and spaces + """ + if set(coded) - {"A", "B", " "} != set(): + raise Exception("decode() accepts only 'A', 'B' and spaces") + decoded = "" + for word in coded.split(): + while len(word) != 0: + decoded += decode_dict[word[:5]] + word = word[5:] + decoded += " " + return decoded.strip() + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/ciphers/base16.py b/ciphers/base16.py new file mode 100644 index 000000000000..6cd62846fc87 --- /dev/null +++ b/ciphers/base16.py @@ -0,0 +1,66 @@ +def base16_encode(data: bytes) -> str: + """ + Encodes the given bytes into base16. + + >>> base16_encode(b'Hello World!') + '48656C6C6F20576F726C6421' + >>> base16_encode(b'HELLO WORLD!') + '48454C4C4F20574F524C4421' + >>> base16_encode(b'') + '' + """ + # Turn the data into a list of integers (where each integer is a byte), + # Then turn each byte into its hexadecimal representation, make sure + # it is uppercase, and then join everything together and return it. + return "".join([hex(byte)[2:].zfill(2).upper() for byte in list(data)]) + + +def base16_decode(data: str) -> bytes: + """ + Decodes the given base16 encoded data into bytes. + + >>> base16_decode('48656C6C6F20576F726C6421') + b'Hello World!' + >>> base16_decode('48454C4C4F20574F524C4421') + b'HELLO WORLD!' + >>> base16_decode('') + b'' + >>> base16_decode('486') + Traceback (most recent call last): + ... + ValueError: Base16 encoded data is invalid: + Data does not have an even number of hex digits. + >>> base16_decode('48656c6c6f20576f726c6421') + Traceback (most recent call last): + ... + ValueError: Base16 encoded data is invalid: + Data is not uppercase hex or it contains invalid characters. + >>> base16_decode('This is not base64 encoded data.') + Traceback (most recent call last): + ... + ValueError: Base16 encoded data is invalid: + Data is not uppercase hex or it contains invalid characters. + """ + # Check data validity, following RFC3548 + # https://www.ietf.org/rfc/rfc3548.txt + if (len(data) % 2) != 0: + raise ValueError( + """Base16 encoded data is invalid: +Data does not have an even number of hex digits.""" + ) + # Check the character set - the standard base16 alphabet + # is uppercase according to RFC3548 section 6 + if not set(data) <= set("0123456789ABCDEF"): + raise ValueError( + """Base16 encoded data is invalid: +Data is not uppercase hex or it contains invalid characters.""" + ) + # For every two hexadecimal digits (= a byte), turn it into an integer. + # Then, string the result together into bytes, and return it. + return bytes(int(data[i] + data[i + 1], 16) for i in range(0, len(data), 2)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/ciphers/base32.py b/ciphers/base32.py new file mode 100644 index 000000000000..911afa2452c0 --- /dev/null +++ b/ciphers/base32.py @@ -0,0 +1,46 @@ +""" +Base32 encoding and decoding + +https://en.wikipedia.org/wiki/Base32 +""" + +B32_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" + + +def base32_encode(data: bytes) -> bytes: + """ + >>> base32_encode(b"Hello World!") + b'JBSWY3DPEBLW64TMMQQQ====' + >>> base32_encode(b"123456") + b'GEZDGNBVGY======' + >>> base32_encode(b"some long complex string") + b'ONXW2ZJANRXW4ZZAMNXW24DMMV4CA43UOJUW4ZY=' + """ + binary_data = "".join(bin(ord(d))[2:].zfill(8) for d in data.decode("utf-8")) + binary_data = binary_data.ljust(5 * ((len(binary_data) // 5) + 1), "0") + b32_chunks = map("".join, zip(*[iter(binary_data)] * 5)) + b32_result = "".join(B32_CHARSET[int(chunk, 2)] for chunk in b32_chunks) + return bytes(b32_result.ljust(8 * ((len(b32_result) // 8) + 1), "="), "utf-8") + + +def base32_decode(data: bytes) -> bytes: + """ + >>> base32_decode(b'JBSWY3DPEBLW64TMMQQQ====') + b'Hello World!' + >>> base32_decode(b'GEZDGNBVGY======') + b'123456' + >>> base32_decode(b'ONXW2ZJANRXW4ZZAMNXW24DMMV4CA43UOJUW4ZY=') + b'some long complex string' + """ + binary_chunks = "".join( + bin(B32_CHARSET.index(_d))[2:].zfill(5) + for _d in data.decode("utf-8").strip("=") + ) + binary_data = list(map("".join, zip(*[iter(binary_chunks)] * 8))) + return bytes("".join([chr(int(_d, 2)) for _d in binary_data]), "utf-8") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/ciphers/base58.py b/ciphers/base58.py new file mode 100644 index 000000000000..038d13963d95 --- /dev/null +++ b/ciphers/base58.py @@ -0,0 +1,142 @@ +B64_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + + +def base64_encode(data: bytes) -> bytes: + """Encodes data according to RFC4648. + + The data is first transformed to binary and appended with binary digits so that its + length becomes a multiple of 6, then each 6 binary digits will match a character in + the B64_CHARSET string. The number of appended binary digits would later determine + how many "=" signs should be added, the padding. + For every 2 binary digits added, a "=" sign is added in the output. + We can add any binary digits to make it a multiple of 6, for instance, consider the + following example: + "AA" -> 0010100100101001 -> 001010 010010 1001 + As can be seen above, 2 more binary digits should be added, so there's 4 + possibilities here: 00, 01, 10 or 11. + That being said, Base64 encoding can be used in Steganography to hide data in these + appended digits. + + >>> from base64 import b64encode + >>> a = b"This pull request is part of Hacktoberfest20!" + >>> b = b"https://tools.ietf.org/html/rfc4648" + >>> c = b"A" + >>> base64_encode(a) == b64encode(a) + True + >>> base64_encode(b) == b64encode(b) + True + >>> base64_encode(c) == b64encode(c) + True + >>> base64_encode("abc") + Traceback (most recent call last): + ... + TypeError: a bytes-like object is required, not 'str' + """ + # Make sure the supplied data is a bytes-like object + if not isinstance(data, bytes): + msg = f"a bytes-like object is required, not '{data.__class__.__name__}'" + raise TypeError(msg) + + binary_stream = "".join(bin(byte)[2:].zfill(8) for byte in data) + + padding_needed = len(binary_stream) % 6 != 0 + + if padding_needed: + # The padding that will be added later + padding = b"=" * ((6 - len(binary_stream) % 6) // 2) + + # Append binary_stream with arbitrary binary digits (0's by default) to make its + # length a multiple of 6. + binary_stream += "0" * (6 - len(binary_stream) % 6) + else: + padding = b"" + + # Encode every 6 binary digits to their corresponding Base64 character + return ( + "".join( + B64_CHARSET[int(binary_stream[index : index + 6], 2)] + for index in range(0, len(binary_stream), 6) + ).encode() + + padding + ) + + +def base64_decode(encoded_data: str) -> bytes: + """Decodes data according to RFC4648. + + This does the reverse operation of base64_encode. + We first transform the encoded data back to a binary stream, take off the + previously appended binary digits according to the padding, at this point we + would have a binary stream whose length is multiple of 8, the last step is + to convert every 8 bits to a byte. + + >>> from base64 import b64decode + >>> a = "VGhpcyBwdWxsIHJlcXVlc3QgaXMgcGFydCBvZiBIYWNrdG9iZXJmZXN0MjAh" + >>> b = "aHR0cHM6Ly90b29scy5pZXRmLm9yZy9odG1sL3JmYzQ2NDg=" + >>> c = "QQ==" + >>> base64_decode(a) == b64decode(a) + True + >>> base64_decode(b) == b64decode(b) + True + >>> base64_decode(c) == b64decode(c) + True + >>> base64_decode("abc") + Traceback (most recent call last): + ... + AssertionError: Incorrect padding + """ + # Make sure encoded_data is either a string or a bytes-like object + if not isinstance(encoded_data, bytes) and not isinstance(encoded_data, str): + msg = ( + "argument should be a bytes-like object or ASCII string, " + f"not '{encoded_data.__class__.__name__}'" + ) + raise TypeError(msg) + + # In case encoded_data is a bytes-like object, make sure it contains only + # ASCII characters so we convert it to a string object + if isinstance(encoded_data, bytes): + try: + encoded_data = encoded_data.decode("utf-8") + except UnicodeDecodeError: + raise ValueError("base64 encoded data should only contain ASCII characters") + + padding = encoded_data.count("=") + + # Check if the encoded string contains non base64 characters + if padding: + assert all(char in B64_CHARSET for char in encoded_data[:-padding]), ( + "Invalid base64 character(s) found." + ) + else: + assert all(char in B64_CHARSET for char in encoded_data), ( + "Invalid base64 character(s) found." + ) + + # Check the padding + assert len(encoded_data) % 4 == 0 and padding < 3, "Incorrect padding" + + if padding: + # Remove padding if there is one + encoded_data = encoded_data[:-padding] + + binary_stream = "".join( + bin(B64_CHARSET.index(char))[2:].zfill(6) for char in encoded_data + )[: -padding * 2] + else: + binary_stream = "".join( + bin(B64_CHARSET.index(char))[2:].zfill(6) for char in encoded_data + ) + + data = [ + int(binary_stream[index : index + 8], 2) + for index in range(0, len(binary_stream), 8) + ] + + return bytes(data) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/ciphers/base64_cipher.py b/ciphers/base64_cipher.py new file mode 100644 index 000000000000..038d13963d95 --- /dev/null +++ b/ciphers/base64_cipher.py @@ -0,0 +1,142 @@ +B64_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + + +def base64_encode(data: bytes) -> bytes: + """Encodes data according to RFC4648. + + The data is first transformed to binary and appended with binary digits so that its + length becomes a multiple of 6, then each 6 binary digits will match a character in + the B64_CHARSET string. The number of appended binary digits would later determine + how many "=" signs should be added, the padding. + For every 2 binary digits added, a "=" sign is added in the output. + We can add any binary digits to make it a multiple of 6, for instance, consider the + following example: + "AA" -> 0010100100101001 -> 001010 010010 1001 + As can be seen above, 2 more binary digits should be added, so there's 4 + possibilities here: 00, 01, 10 or 11. + That being said, Base64 encoding can be used in Steganography to hide data in these + appended digits. + + >>> from base64 import b64encode + >>> a = b"This pull request is part of Hacktoberfest20!" + >>> b = b"https://tools.ietf.org/html/rfc4648" + >>> c = b"A" + >>> base64_encode(a) == b64encode(a) + True + >>> base64_encode(b) == b64encode(b) + True + >>> base64_encode(c) == b64encode(c) + True + >>> base64_encode("abc") + Traceback (most recent call last): + ... + TypeError: a bytes-like object is required, not 'str' + """ + # Make sure the supplied data is a bytes-like object + if not isinstance(data, bytes): + msg = f"a bytes-like object is required, not '{data.__class__.__name__}'" + raise TypeError(msg) + + binary_stream = "".join(bin(byte)[2:].zfill(8) for byte in data) + + padding_needed = len(binary_stream) % 6 != 0 + + if padding_needed: + # The padding that will be added later + padding = b"=" * ((6 - len(binary_stream) % 6) // 2) + + # Append binary_stream with arbitrary binary digits (0's by default) to make its + # length a multiple of 6. + binary_stream += "0" * (6 - len(binary_stream) % 6) + else: + padding = b"" + + # Encode every 6 binary digits to their corresponding Base64 character + return ( + "".join( + B64_CHARSET[int(binary_stream[index : index + 6], 2)] + for index in range(0, len(binary_stream), 6) + ).encode() + + padding + ) + + +def base64_decode(encoded_data: str) -> bytes: + """Decodes data according to RFC4648. + + This does the reverse operation of base64_encode. + We first transform the encoded data back to a binary stream, take off the + previously appended binary digits according to the padding, at this point we + would have a binary stream whose length is multiple of 8, the last step is + to convert every 8 bits to a byte. + + >>> from base64 import b64decode + >>> a = "VGhpcyBwdWxsIHJlcXVlc3QgaXMgcGFydCBvZiBIYWNrdG9iZXJmZXN0MjAh" + >>> b = "aHR0cHM6Ly90b29scy5pZXRmLm9yZy9odG1sL3JmYzQ2NDg=" + >>> c = "QQ==" + >>> base64_decode(a) == b64decode(a) + True + >>> base64_decode(b) == b64decode(b) + True + >>> base64_decode(c) == b64decode(c) + True + >>> base64_decode("abc") + Traceback (most recent call last): + ... + AssertionError: Incorrect padding + """ + # Make sure encoded_data is either a string or a bytes-like object + if not isinstance(encoded_data, bytes) and not isinstance(encoded_data, str): + msg = ( + "argument should be a bytes-like object or ASCII string, " + f"not '{encoded_data.__class__.__name__}'" + ) + raise TypeError(msg) + + # In case encoded_data is a bytes-like object, make sure it contains only + # ASCII characters so we convert it to a string object + if isinstance(encoded_data, bytes): + try: + encoded_data = encoded_data.decode("utf-8") + except UnicodeDecodeError: + raise ValueError("base64 encoded data should only contain ASCII characters") + + padding = encoded_data.count("=") + + # Check if the encoded string contains non base64 characters + if padding: + assert all(char in B64_CHARSET for char in encoded_data[:-padding]), ( + "Invalid base64 character(s) found." + ) + else: + assert all(char in B64_CHARSET for char in encoded_data), ( + "Invalid base64 character(s) found." + ) + + # Check the padding + assert len(encoded_data) % 4 == 0 and padding < 3, "Incorrect padding" + + if padding: + # Remove padding if there is one + encoded_data = encoded_data[:-padding] + + binary_stream = "".join( + bin(B64_CHARSET.index(char))[2:].zfill(6) for char in encoded_data + )[: -padding * 2] + else: + binary_stream = "".join( + bin(B64_CHARSET.index(char))[2:].zfill(6) for char in encoded_data + ) + + data = [ + int(binary_stream[index : index + 8], 2) + for index in range(0, len(binary_stream), 8) + ] + + return bytes(data) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/ciphers/base85.py b/ciphers/base85.py new file mode 100644 index 000000000000..f0228e5052dd --- /dev/null +++ b/ciphers/base85.py @@ -0,0 +1,58 @@ +""" +Base85 (Ascii85) encoding and decoding + +https://en.wikipedia.org/wiki/Ascii85 +""" + + +def _base10_to_85(d: int) -> str: + return "".join(chr(d % 85 + 33)) + _base10_to_85(d // 85) if d > 0 else "" + + +def _base85_to_10(digits: list) -> int: + return sum(char * 85**i for i, char in enumerate(reversed(digits))) + + +def ascii85_encode(data: bytes) -> bytes: + """ + >>> ascii85_encode(b"") + b'' + >>> ascii85_encode(b"12345") + b'0etOA2#' + >>> ascii85_encode(b"base 85") + b'@UX=h+?24' + """ + binary_data = "".join(bin(ord(d))[2:].zfill(8) for d in data.decode("utf-8")) + null_values = (32 * ((len(binary_data) // 32) + 1) - len(binary_data)) // 8 + binary_data = binary_data.ljust(32 * ((len(binary_data) // 32) + 1), "0") + b85_chunks = [int(_s, 2) for _s in map("".join, zip(*[iter(binary_data)] * 32))] + result = "".join(_base10_to_85(chunk)[::-1] for chunk in b85_chunks) + return bytes(result[:-null_values] if null_values % 4 != 0 else result, "utf-8") + + +def ascii85_decode(data: bytes) -> bytes: + """ + >>> ascii85_decode(b"") + b'' + >>> ascii85_decode(b"0etOA2#") + b'12345' + >>> ascii85_decode(b"@UX=h+?24") + b'base 85' + """ + null_values = 5 * ((len(data) // 5) + 1) - len(data) + binary_data = data.decode("utf-8") + "u" * null_values + b85_chunks = map("".join, zip(*[iter(binary_data)] * 5)) + b85_segments = [[ord(_s) - 33 for _s in chunk] for chunk in b85_chunks] + results = [bin(_base85_to_10(chunk))[2::].zfill(32) for chunk in b85_segments] + char_chunks = [ + [chr(int(_s, 2)) for _s in map("".join, zip(*[iter(r)] * 8))] for r in results + ] + result = "".join("".join(char) for char in char_chunks) + offset = int(null_values % 5 == 0) + return bytes(result[: offset - null_values], "utf-8") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/ciphers/beaufort_cipher.py b/ciphers/beaufort_cipher.py new file mode 100644 index 000000000000..788fc72b89c3 --- /dev/null +++ b/ciphers/beaufort_cipher.py @@ -0,0 +1,82 @@ +""" +Author: Mohit Radadiya +""" + +from string import ascii_uppercase + +dict1 = {char: i for i, char in enumerate(ascii_uppercase)} +dict2 = dict(enumerate(ascii_uppercase)) + + +# This function generates the key in +# a cyclic manner until it's length isn't +# equal to the length of original text +def generate_key(message: str, key: str) -> str: + """ + >>> generate_key("THE GERMAN ATTACK","SECRET") + 'SECRETSECRETSECRE' + """ + x = len(message) + i = 0 + while True: + if x == i: + i = 0 + if len(key) == len(message): + break + key += key[i] + i += 1 + return key + + +# This function returns the encrypted text +# generated with the help of the key +def cipher_text(message: str, key_new: str) -> str: + """ + >>> cipher_text("THE GERMAN ATTACK","SECRETSECRETSECRE") + 'BDC PAYUWL JPAIYI' + """ + cipher_text = "" + i = 0 + for letter in message: + if letter == " ": + cipher_text += " " + else: + x = (dict1[letter] - dict1[key_new[i]]) % 26 + i += 1 + cipher_text += dict2[x] + return cipher_text + + +# This function decrypts the encrypted text +# and returns the original text +def original_text(cipher_text: str, key_new: str) -> str: + """ + >>> original_text("BDC PAYUWL JPAIYI","SECRETSECRETSECRE") + 'THE GERMAN ATTACK' + """ + or_txt = "" + i = 0 + for letter in cipher_text: + if letter == " ": + or_txt += " " + else: + x = (dict1[letter] + dict1[key_new[i]] + 26) % 26 + i += 1 + or_txt += dict2[x] + return or_txt + + +def main() -> None: + message = "THE GERMAN ATTACK" + key = "SECRET" + key_new = generate_key(message, key) + s = cipher_text(message, key_new) + print(f"Encrypted Text = {s}") + print(f"Original Text = {original_text(s, key_new)}") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/ciphers/bifid.py b/ciphers/bifid.py new file mode 100644 index 000000000000..a15b381640aa --- /dev/null +++ b/ciphers/bifid.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 + +""" +The Bifid Cipher uses a Polybius Square to encipher a message in a way that +makes it fairly difficult to decipher without knowing the secret. + +https://www.braingle.com/brainteasers/codes/bifid.php +""" + +import numpy as np + +SQUARE = [ + ["a", "b", "c", "d", "e"], + ["f", "g", "h", "i", "k"], + ["l", "m", "n", "o", "p"], + ["q", "r", "s", "t", "u"], + ["v", "w", "x", "y", "z"], +] + + +class BifidCipher: + def __init__(self) -> None: + self.SQUARE = np.array(SQUARE) + + def letter_to_numbers(self, letter: str) -> np.ndarray: + """ + Return the pair of numbers that represents the given letter in the + polybius square + + >>> np.array_equal(BifidCipher().letter_to_numbers('a'), [1,1]) + True + + >>> np.array_equal(BifidCipher().letter_to_numbers('u'), [4,5]) + True + """ + index1, index2 = np.where(letter == self.SQUARE) + indexes = np.concatenate([index1 + 1, index2 + 1]) + return indexes + + def numbers_to_letter(self, index1: int, index2: int) -> str: + """ + Return the letter corresponding to the position [index1, index2] in + the polybius square + + >>> BifidCipher().numbers_to_letter(4, 5) == "u" + True + + >>> BifidCipher().numbers_to_letter(1, 1) == "a" + True + """ + letter = self.SQUARE[index1 - 1, index2 - 1] + return letter + + def encode(self, message: str) -> str: + """ + Return the encoded version of message according to the polybius cipher + + >>> BifidCipher().encode('testmessage') == 'qtltbdxrxlk' + True + + >>> BifidCipher().encode('Test Message') == 'qtltbdxrxlk' + True + + >>> BifidCipher().encode('test j') == BifidCipher().encode('test i') + True + """ + message = message.lower() + message = message.replace(" ", "") + message = message.replace("j", "i") + + first_step = np.empty((2, len(message))) + for letter_index in range(len(message)): + numbers = self.letter_to_numbers(message[letter_index]) + + first_step[0, letter_index] = numbers[0] + first_step[1, letter_index] = numbers[1] + + second_step = first_step.reshape(2 * len(message)) + encoded_message = "" + for numbers_index in range(len(message)): + index1 = int(second_step[numbers_index * 2]) + index2 = int(second_step[(numbers_index * 2) + 1]) + letter = self.numbers_to_letter(index1, index2) + encoded_message = encoded_message + letter + + return encoded_message + + def decode(self, message: str) -> str: + """ + Return the decoded version of message according to the polybius cipher + + >>> BifidCipher().decode('qtltbdxrxlk') == 'testmessage' + True + """ + message = message.lower() + message.replace(" ", "") + first_step = np.empty(2 * len(message)) + for letter_index in range(len(message)): + numbers = self.letter_to_numbers(message[letter_index]) + first_step[letter_index * 2] = numbers[0] + first_step[letter_index * 2 + 1] = numbers[1] + + second_step = first_step.reshape((2, len(message))) + decoded_message = "" + for numbers_index in range(len(message)): + index1 = int(second_step[0, numbers_index]) + index2 = int(second_step[1, numbers_index]) + letter = self.numbers_to_letter(index1, index2) + decoded_message = decoded_message + letter + + return decoded_message diff --git a/ciphers/brute-force_caesar_cipher.py b/ciphers/brute_force_caesar_cipher.py similarity index 77% rename from ciphers/brute-force_caesar_cipher.py rename to ciphers/brute_force_caesar_cipher.py index 3e6e975c8297..458d08db2628 100644 --- a/ciphers/brute-force_caesar_cipher.py +++ b/ciphers/brute_force_caesar_cipher.py @@ -1,4 +1,7 @@ -def decrypt(message): +import string + + +def decrypt(message: str) -> None: """ >>> decrypt('TMDETUX PMDVU') Decryption using Key #0: TMDETUX PMDVU @@ -28,26 +31,28 @@ def decrypt(message): Decryption using Key #24: VOFGVWZ ROFXW Decryption using Key #25: UNEFUVY QNEWV """ - LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - for key in range(len(LETTERS)): + for key in range(len(string.ascii_uppercase)): translated = "" for symbol in message: - if symbol in LETTERS: - num = LETTERS.find(symbol) + if symbol in string.ascii_uppercase: + num = string.ascii_uppercase.find(symbol) num = num - key if num < 0: - num = num + len(LETTERS) - translated = translated + LETTERS[num] + num = num + len(string.ascii_uppercase) + translated = translated + string.ascii_uppercase[num] else: translated = translated + symbol - print("Decryption using Key #%s: %s" % (key, translated)) + print(f"Decryption using Key #{key}: {translated}") + -def main(): +def main() -> None: message = input("Encrypted message: ") message = message.upper() decrypt(message) -if __name__ == '__main__': + +if __name__ == "__main__": import doctest + doctest.testmod() main() diff --git a/ciphers/caesar_cipher.py b/ciphers/caesar_cipher.py index b590f81f901b..ef5f49313ee7 100644 --- a/ciphers/caesar_cipher.py +++ b/ciphers/caesar_cipher.py @@ -1,44 +1,256 @@ -# The Caesar Cipher Algorithm - -def main(): - message = input("Enter message: ") - key = int(input("Key [1-26]: ")) - mode = input("Encrypt or Decrypt [e/d]: ") - - if mode.lower().startswith('e'): - mode = "encrypt" - elif mode.lower().startswith('d'): - mode = "decrypt" - - translated = encdec(message, key, mode) - if mode == "encrypt": - print("Encryption:", translated) - elif mode == "decrypt": - print("Decryption:", translated) - -def encdec(message, key, mode): - message = message.upper() - translated = "" - LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - for symbol in message: - if symbol in LETTERS: - num = LETTERS.find(symbol) - if mode == "encrypt": - num = num + key - elif mode == "decrypt": - num = num - key - - if num >= len(LETTERS): - num -= len(LETTERS) - elif num < 0: - num += len(LETTERS) - - translated += LETTERS[num] +from __future__ import annotations + +from string import ascii_letters + + +def encrypt(input_string: str, key: int, alphabet: str | None = None) -> str: + """ + encrypt + ======= + + Encodes a given string with the caesar cipher and returns the encoded + message + + Parameters: + ----------- + + * `input_string`: the plain-text that needs to be encoded + * `key`: the number of letters to shift the message by + + Optional: + + * `alphabet` (``None``): the alphabet used to encode the cipher, if not + specified, the standard english alphabet with upper and lowercase + letters is used + + Returns: + + * A string containing the encoded cipher-text + + More on the caesar cipher + ========================= + + The caesar cipher is named after Julius Caesar who used it when sending + secret military messages to his troops. This is a simple substitution cipher + where every character in the plain-text is shifted by a certain number known + as the "key" or "shift". + + Example: + Say we have the following message: + ``Hello, captain`` + + And our alphabet is made up of lower and uppercase letters: + ``abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`` + + And our shift is ``2`` + + We can then encode the message, one letter at a time. ``H`` would become ``J``, + since ``J`` is two letters away, and so on. If the shift is ever too large, or + our letter is at the end of the alphabet, we just start at the beginning + (``Z`` would shift to ``a`` then ``b`` and so on). + + Our final message would be ``Jgnnq, ecrvckp`` + + Further reading + =============== + + * https://en.m.wikipedia.org/wiki/Caesar_cipher + + Doctests + ======== + + >>> encrypt('The quick brown fox jumps over the lazy dog', 8) + 'bpm yCqks jzwEv nwF rCuxA wDmz Bpm tiHG lwo' + + >>> encrypt('A very large key', 8000) + 's nWjq dSjYW cWq' + + >>> encrypt('a lowercase alphabet', 5, 'abcdefghijklmnopqrstuvwxyz') + 'f qtbjwhfxj fqumfgjy' + """ + # Set default alphabet to lower and upper case english chars + alpha = alphabet or ascii_letters + + # The final result string + result = "" + + for character in input_string: + if character not in alpha: + # Append without encryption if character is not in the alphabet + result += character else: - translated += symbol - return translated + # Get the index of the new key and make sure it isn't too large + new_key = (alpha.index(character) + key) % len(alpha) + + # Append the encoded character to the alphabet + result += alpha[new_key] + + return result + + +def decrypt(input_string: str, key: int, alphabet: str | None = None) -> str: + """ + decrypt + ======= + + Decodes a given string of cipher-text and returns the decoded plain-text + + Parameters: + ----------- + + * `input_string`: the cipher-text that needs to be decoded + * `key`: the number of letters to shift the message backwards by to decode + + Optional: + + * `alphabet` (``None``): the alphabet used to decode the cipher, if not + specified, the standard english alphabet with upper and lowercase + letters is used + + Returns: + + * A string containing the decoded plain-text + + More on the caesar cipher + ========================= + + The caesar cipher is named after Julius Caesar who used it when sending + secret military messages to his troops. This is a simple substitution cipher + where very character in the plain-text is shifted by a certain number known + as the "key" or "shift". Please keep in mind, here we will be focused on + decryption. + + Example: + Say we have the following cipher-text: + ``Jgnnq, ecrvckp`` + + And our alphabet is made up of lower and uppercase letters: + ``abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`` + + And our shift is ``2`` + + To decode the message, we would do the same thing as encoding, but in + reverse. The first letter, ``J`` would become ``H`` (remember: we are decoding) + because ``H`` is two letters in reverse (to the left) of ``J``. We would + continue doing this. A letter like ``a`` would shift back to the end of + the alphabet, and would become ``Z`` or ``Y`` and so on. + + Our final message would be ``Hello, captain`` + + Further reading + =============== + + * https://en.m.wikipedia.org/wiki/Caesar_cipher + + Doctests + ======== + + >>> decrypt('bpm yCqks jzwEv nwF rCuxA wDmz Bpm tiHG lwo', 8) + 'The quick brown fox jumps over the lazy dog' + + >>> decrypt('s nWjq dSjYW cWq', 8000) + 'A very large key' + + >>> decrypt('f qtbjwhfxj fqumfgjy', 5, 'abcdefghijklmnopqrstuvwxyz') + 'a lowercase alphabet' + """ + # Turn on decode mode by making the key negative + key *= -1 + + return encrypt(input_string, key, alphabet) + + +def brute_force(input_string: str, alphabet: str | None = None) -> dict[int, str]: + """ + brute_force + =========== + + Returns all the possible combinations of keys and the decoded strings in the + form of a dictionary + + Parameters: + ----------- + + * `input_string`: the cipher-text that needs to be used during brute-force + + Optional: + + * `alphabet` (``None``): the alphabet used to decode the cipher, if not + specified, the standard english alphabet with upper and lowercase + letters is used + + More about brute force + ====================== + + Brute force is when a person intercepts a message or password, not knowing + the key and tries every single combination. This is easy with the caesar + cipher since there are only all the letters in the alphabet. The more + complex the cipher, the larger amount of time it will take to do brute force + + Ex: + Say we have a ``5`` letter alphabet (``abcde``), for simplicity and we intercepted + the following message: ``dbc``, + we could then just write out every combination: + ``ecd``... and so on, until we reach a combination that makes sense: + ``cab`` + + Further reading + =============== + + * https://en.wikipedia.org/wiki/Brute_force + + Doctests + ======== + + >>> brute_force("jFyuMy xIH'N vLONy zILwy Gy!")[20] + "Please don't brute force me!" + + >>> brute_force(1) + Traceback (most recent call last): + TypeError: 'int' object is not iterable + """ + # Set default alphabet to lower and upper case english chars + alpha = alphabet or ascii_letters + + # To store data on all the combinations + brute_force_data = {} + + # Cycle through each combination + for key in range(1, len(alpha) + 1): + # Decrypt the message and store the result in the data + brute_force_data[key] = decrypt(input_string, key, alpha) + + return brute_force_data + + +if __name__ == "__main__": + while True: + print(f"\n{'-' * 10}\n Menu\n{'-' * 10}") + print(*["1.Encrypt", "2.Decrypt", "3.BruteForce", "4.Quit"], sep="\n") + + # get user input + choice = input("\nWhat would you like to do?: ").strip() or "4" + + # run functions based on what the user chose + if choice not in ("1", "2", "3", "4"): + print("Invalid choice, please enter a valid choice") + elif choice == "1": + input_string = input("Please enter the string to be encrypted: ") + key = int(input("Please enter off-set: ").strip()) + + print(encrypt(input_string, key)) + elif choice == "2": + input_string = input("Please enter the string to be decrypted: ") + key = int(input("Please enter off-set: ").strip()) + + print(decrypt(input_string, key)) + elif choice == "3": + input_string = input("Please enter the string to be decrypted: ") + brute_force_data = brute_force(input_string) + + for key, value in brute_force_data.items(): + print(f"Key: {key} | Message: {value}") -if __name__ == '__main__': - import doctest - doctest.testmod() - main() + elif choice == "4": + print("Goodbye.") + break diff --git a/ciphers/columnar_transposition.py b/ciphers/columnar_transposition.py new file mode 100644 index 000000000000..d697db6e1176 --- /dev/null +++ b/ciphers/columnar_transposition.py @@ -0,0 +1,127 @@ +"""Columnar Transposition cipher. + +This classical cipher writes the plaintext in rows under a keyword and reads +columns in the order of the alphabetical rank of the keyword letters. + +Reference: https://en.wikipedia.org/wiki/Transposition_cipher#Columnar_transposition + +We keep spaces and punctuation. Key must be alphabetic (case-insensitive). + +>>> pt = "WE ARE DISCOVERED. FLEE AT ONCE" +>>> ct = encrypt(pt, "ZEBRAS") +>>> decrypt(ct, "ZEBRAS") == pt +True + +Edge cases: +>>> encrypt("HELLO", "A") +'HELLO' +>>> decrypt("HELLO", "A") +'HELLO' +>>> encrypt("HELLO", "HELLO") +'EHLLO' +>>> decrypt("EHLLO", "HELLO") +'HELLO' +>>> encrypt("HELLO", "") +Traceback (most recent call last): + ... +ValueError: Key must be a non-empty alphabetic string +""" + +from __future__ import annotations + + +def _normalize_key(key: str) -> str: + k = "".join(ch for ch in key.upper() if ch.isalpha()) + if not k: + raise ValueError("Key must be a non-empty alphabetic string") + return k + + +def _column_order(key: str) -> list[int]: + # Stable sort by character then original index to handle duplicates + indexed = list(enumerate(key)) + return [ + i + for i, _ in sorted( + indexed, key=lambda indexed_pair: (indexed_pair[1], indexed_pair[0]) + ) + ] + + +def encrypt(plaintext: str, key: str) -> str: + """Encrypt using columnar transposition. + + :param plaintext: Input text (any characters) + :param key: Alphabetic keyword + :return: Ciphertext + :raises ValueError: on invalid key + """ + k = _normalize_key(key) + cols = len(k) + if cols == 1: + return plaintext + + order = _column_order(k) + + # Build ragged rows without padding + rows = (len(plaintext) + cols - 1) // cols + grid: list[str] = [plaintext[i * cols : (i + 1) * cols] for i in range(rows)] + + # Read columns in sorted order, skipping missing cells + out: list[str] = [] + for col in order: + for r in range(rows): + if col < len(grid[r]): + out.append(grid[r][col]) + return "".join(out) + + +def decrypt(ciphertext: str, key: str) -> str: + """Decrypt columnar transposition ciphertext. + + :param ciphertext: Encrypted text + :param key: Alphabetic keyword + :return: Decrypted plaintext + :raises ValueError: on invalid key + """ + k = _normalize_key(key) + cols = len(k) + if cols == 1: + return ciphertext + + order = _column_order(k) + text_len = len(ciphertext) + rows = (text_len + cols - 1) // cols + r = text_len % cols + + # Column lengths based on ragged last row (no padding during encryption) + col_lengths: list[int] = [] + for c in range(cols): + if r == 0: + col_lengths.append(rows) + else: + col_lengths.append(rows if c < r else rows - 1) + + # Slice ciphertext into columns following the sorted order + columns: list[str] = [""] * cols + idx = 0 + for col in order: + ln = col_lengths[col] + columns[col] = ciphertext[idx : idx + ln] + idx += ln + + # Rebuild plaintext row-wise + out: list[str] = [] + pointers = [0] * cols + for _ in range(rows * cols): + c = len(out) % cols + if pointers[c] < len(columns[c]): + out.append(columns[c][pointers[c]]) + pointers[c] += 1 + return "".join(out) + + +if __name__ == "__main__": # pragma: no cover + import doctest + + doctest.testmod() diff --git a/ciphers/cryptomath_module.py b/ciphers/cryptomath_module.py index 3e8e71b117ed..bf813c8c0167 100644 --- a/ciphers/cryptomath_module.py +++ b/ciphers/cryptomath_module.py @@ -1,14 +1,82 @@ -def gcd(a, b): - while a != 0: - a, b = b % a, a - return b - -def findModInverse(a, m): - if gcd(a, m) != 1: - return None +""" +Cryptographic mathematics module for modular arithmetic operations. + +This module provides utilities for cryptographic computations, +particularly modular multiplicative inverse. +""" + +from maths.greatest_common_divisor import gcd_by_iterative + + +def find_mod_inverse(a: int, m: int) -> int: + """ + Find the modular multiplicative inverse of a modulo m. + + The modular multiplicative inverse of a modulo m is an integer x such that: + (a * x) % m = 1 + + This function uses the Extended Euclidean Algorithm to find the inverse. + An inverse exists if and only if a and m are coprime (gcd(a, m) = 1). + + Args: + a: The integer to find the inverse of + m: The modulus + + Returns: + The modular multiplicative inverse of a modulo m + + Raises: + ValueError: If gcd(a, m) != 1 (inverse does not exist) + + Reference: + https://en.wikipedia.org/wiki/Modular_multiplicative_inverse + + Examples: + >>> find_mod_inverse(3, 7) + 5 + >>> (3 * 5) % 7 # Verify: 3 * 5 ≡ 1 (mod 7) + 1 + >>> find_mod_inverse(3, 10) + 7 + >>> (3 * 7) % 10 # Verify: 3 * 7 ≡ 1 (mod 10) + 1 + >>> find_mod_inverse(4, 11) + 3 + >>> (4 * 3) % 11 # Verify: 4 * 3 ≡ 1 (mod 11) + 1 + >>> find_mod_inverse(7, 26) + 15 + >>> (7 * 15) % 26 # Verify: 7 * 15 ≡ 1 (mod 26) + 1 + >>> find_mod_inverse(1, 5) + 1 + >>> find_mod_inverse(2, 7) + 4 + >>> find_mod_inverse(3, 11) + 4 + >>> find_mod_inverse(5, 11) + 9 + >>> find_mod_inverse(5, 17) + 7 + >>> find_mod_inverse(2, 4) + Traceback (most recent call last): + ... + ValueError: mod inverse of 2 and 4 does not exist + >>> find_mod_inverse(6, 9) + Traceback (most recent call last): + ... + ValueError: mod inverse of 6 and 9 does not exist + >>> find_mod_inverse(10, 20) + Traceback (most recent call last): + ... + ValueError: mod inverse of 10 and 20 does not exist + """ + if gcd_by_iterative(a, m) != 1: + msg = f"mod inverse of {a!r} and {m!r} does not exist" + raise ValueError(msg) u1, u2, u3 = 1, 0, a v1, v2, v3 = 0, 1, m while v3 != 0: q = u3 // v3 - v1, v2, v3, u1, u2, u3 = (u1 - q * v1), (u2 - q * v2), (u3 - q *v3), v1, v2, v3 - return u1 % m + v1, v2, v3, u1, u2, u3 = (u1 - q * v1), (u2 - q * v2), (u3 - q * v3), v1, v2, v3 + return u1 % m diff --git a/ciphers/decrypt_caesar_with_chi_squared.py b/ciphers/decrypt_caesar_with_chi_squared.py new file mode 100644 index 000000000000..fb95c0f90628 --- /dev/null +++ b/ciphers/decrypt_caesar_with_chi_squared.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +from __future__ import annotations + + +def decrypt_caesar_with_chi_squared( + ciphertext: str, + cipher_alphabet: list[str] | None = None, + frequencies_dict: dict[str, float] | None = None, + case_sensitive: bool = False, +) -> tuple[int, float, str]: + """ + Basic Usage + =========== + + Arguments: + * `ciphertext` (str): the text to decode (encoded with the caesar cipher) + + Optional Arguments: + * `cipher_alphabet` (list): the alphabet used for the cipher (each letter is + a string separated by commas) + * `frequencies_dict` (dict): a dictionary of word frequencies where keys are + the letters and values are a percentage representation of the frequency as + a decimal/float + * `case_sensitive` (bool): a boolean value: ``True`` if the case matters during + decryption, ``False`` if it doesn't + + Returns: + * A tuple in the form of: + (`most_likely_cipher`, `most_likely_cipher_chi_squared_value`, + `decoded_most_likely_cipher`) + + where... + - `most_likely_cipher` is an integer representing the shift of the smallest + chi-squared statistic (most likely key) + - `most_likely_cipher_chi_squared_value` is a float representing the + chi-squared statistic of the most likely shift + - `decoded_most_likely_cipher` is a string with the decoded cipher + (decoded by the most_likely_cipher key) + + + The Chi-squared test + ==================== + + The caesar cipher + ----------------- + + The caesar cipher is a very insecure encryption algorithm, however it has + been used since Julius Caesar. The cipher is a simple substitution cipher + where each character in the plain text is replaced by a character in the + alphabet a certain number of characters after the original character. The + number of characters away is called the shift or key. For example: + + | Plain text: ``hello`` + | Key: ``1`` + | Cipher text: ``ifmmp`` + | (each letter in ``hello`` has been shifted one to the right in the eng. alphabet) + + As you can imagine, this doesn't provide lots of security. In fact + decrypting ciphertext by brute-force is extremely easy even by hand. However + one way to do that is the chi-squared test. + + The chi-squared test + -------------------- + + Each letter in the english alphabet has a frequency, or the amount of times + it shows up compared to other letters (usually expressed as a decimal + representing the percentage likelihood). The most common letter in the + english language is ``e`` with a frequency of ``0.11162`` or ``11.162%``. + The test is completed in the following fashion. + + 1. The ciphertext is decoded in a brute force way (every combination of the + ``26`` possible combinations) + 2. For every combination, for each letter in the combination, the average + amount of times the letter should appear the message is calculated by + multiplying the total number of characters by the frequency of the letter. + + | For example: + | In a message of ``100`` characters, ``e`` should appear around ``11.162`` + times. + + 3. Then, to calculate the margin of error (the amount of times the letter + SHOULD appear with the amount of times the letter DOES appear), we use + the chi-squared test. The following formula is used: + + Let: + - n be the number of times the letter actually appears + - p be the predicted value of the number of times the letter should + appear (see item ``2``) + - let v be the chi-squared test result (referred to here as chi-squared + value/statistic) + + :: + + (n - p)^2 + --------- = v + p + + 4. Each chi squared value for each letter is then added up to the total. + The total is the chi-squared statistic for that encryption key. + 5. The encryption key with the lowest chi-squared value is the most likely + to be the decoded answer. + + Further Reading + =============== + + * http://practicalcryptography.com/cryptanalysis/text-characterisation/chi-squared-statistic/ + * https://en.wikipedia.org/wiki/Letter_frequency + * https://en.wikipedia.org/wiki/Chi-squared_test + * https://en.m.wikipedia.org/wiki/Caesar_cipher + + Doctests + ======== + + >>> decrypt_caesar_with_chi_squared( + ... 'dof pz aol jhlzhy jpwoly zv wvwbshy? pa pz avv lhzf av jyhjr!' + ... ) # doctest: +NORMALIZE_WHITESPACE + (7, 3129.228005747531, + 'why is the caesar cipher so popular? it is too easy to crack!') + + >>> decrypt_caesar_with_chi_squared('crybd cdbsxq') + (10, 233.35343938980898, 'short string') + + >>> decrypt_caesar_with_chi_squared('Crybd Cdbsxq', case_sensitive=True) + (10, 233.35343938980898, 'Short String') + + >>> decrypt_caesar_with_chi_squared(12) + Traceback (most recent call last): + AttributeError: 'int' object has no attribute 'lower' + """ + alphabet_letters = cipher_alphabet or [chr(i) for i in range(97, 123)] + + # If the argument is None or the user provided an empty dictionary + if not frequencies_dict: + # Frequencies of letters in the english language (how much they show up) + frequencies = { + "a": 0.08497, + "b": 0.01492, + "c": 0.02202, + "d": 0.04253, + "e": 0.11162, + "f": 0.02228, + "g": 0.02015, + "h": 0.06094, + "i": 0.07546, + "j": 0.00153, + "k": 0.01292, + "l": 0.04025, + "m": 0.02406, + "n": 0.06749, + "o": 0.07507, + "p": 0.01929, + "q": 0.00095, + "r": 0.07587, + "s": 0.06327, + "t": 0.09356, + "u": 0.02758, + "v": 0.00978, + "w": 0.02560, + "x": 0.00150, + "y": 0.01994, + "z": 0.00077, + } + else: + # Custom frequencies dictionary + frequencies = frequencies_dict + + if not case_sensitive: + ciphertext = ciphertext.lower() + + # Chi squared statistic values + chi_squared_statistic_values: dict[int, tuple[float, str]] = {} + + # cycle through all of the shifts + for shift in range(len(alphabet_letters)): + decrypted_with_shift = "" + + # decrypt the message with the shift + for letter in ciphertext: + try: + # Try to index the letter in the alphabet + new_key = (alphabet_letters.index(letter.lower()) - shift) % len( + alphabet_letters + ) + decrypted_with_shift += ( + alphabet_letters[new_key].upper() + if case_sensitive and letter.isupper() + else alphabet_letters[new_key] + ) + except ValueError: + # Append the character if it isn't in the alphabet + decrypted_with_shift += letter + + chi_squared_statistic = 0.0 + + # Loop through each letter in the decoded message with the shift + for letter in decrypted_with_shift: + if case_sensitive: + letter = letter.lower() + if letter in frequencies: + # Get the amount of times the letter occurs in the message + occurrences = decrypted_with_shift.lower().count(letter) + + # Get the excepcted amount of times the letter should appear based + # on letter frequencies + expected = frequencies[letter] * occurrences + + # Complete the chi squared statistic formula + chi_letter_value = ((occurrences - expected) ** 2) / expected + + # Add the margin of error to the total chi squared statistic + chi_squared_statistic += chi_letter_value + elif letter.lower() in frequencies: + # Get the amount of times the letter occurs in the message + occurrences = decrypted_with_shift.count(letter) + + # Get the excepcted amount of times the letter should appear based + # on letter frequencies + expected = frequencies[letter] * occurrences + + # Complete the chi squared statistic formula + chi_letter_value = ((occurrences - expected) ** 2) / expected + + # Add the margin of error to the total chi squared statistic + chi_squared_statistic += chi_letter_value + + # Add the data to the chi_squared_statistic_values dictionary + chi_squared_statistic_values[shift] = ( + chi_squared_statistic, + decrypted_with_shift, + ) + + # Get the most likely cipher by finding the cipher with the smallest chi squared + # statistic + def chi_squared_statistic_values_sorting_key(key: int) -> tuple[float, str]: + return chi_squared_statistic_values[key] + + most_likely_cipher: int = min( + chi_squared_statistic_values, + key=chi_squared_statistic_values_sorting_key, + ) + + # Get all the data from the most likely cipher (key, decoded message) + ( + most_likely_cipher_chi_squared_value, + decoded_most_likely_cipher, + ) = chi_squared_statistic_values[most_likely_cipher] + + # Return the data on the most likely shift + return ( + most_likely_cipher, + most_likely_cipher_chi_squared_value, + decoded_most_likely_cipher, + ) diff --git a/ciphers/des_ecb.py b/ciphers/des_ecb.py new file mode 100644 index 000000000000..559aee9b764a --- /dev/null +++ b/ciphers/des_ecb.py @@ -0,0 +1,562 @@ +""" +Python program for DES (Data Encryption Standard) using Electronic Codebook (ECB) mode. + +DES is a symmetric-key block cipher that encrypts data in fixed-size blocks (64 bits). +In ECB mode, the plaintext is divided into 64-bit blocks, and each block is encrypted +independently using the same key. This makes ECB the simplest block cipher mode, but +also one of the least secure, as identical plaintext blocks will produce identical +ciphertext blocks. + +This implementation of DES includes key scheduling, encryption, and decryption. +It uses standard DES operations such as initial and final permutations, expansion, +permutation, and S-box lookups. Padding is applied to ensure the plaintext length is +a multiple of 64 bits. + +Warning: ECB mode is not secure for most use cases due to its vulnerability to block +repetition analysis. Consider using a more secure mode of operation, such as CBC +(Cipher Block Chaining), for sensitive data encryption. + +References: +- Handbook of Applied Cryptography (Algorithm 7.82) +- Handbook of Applied Cryptography (Algorithm 7.83) +- Handbook of Applied Cryptography (Algorithm 9.29) +- https://en.wikipedia.org/wiki/Data_Encryption_Standard +""" + +import random + +# fmt: off + +# Initial Permutation Table +IP = [58, 50, 42, 34, 26, 18, 10, 2, + 60, 52, 44, 36, 28, 20, 12, 4, + 62, 54, 46, 38, 30, 22, 14, 6, + 64, 56, 48, 40, 32, 24, 16, 8, + 57, 49, 41, 33, 25, 17, 9, 1, + 59, 51, 43, 35, 27, 19, 11, 3, + 61, 53, 45, 37, 29, 21, 13, 5, + 63, 55, 47, 39, 31, 23, 15, 7] + +# Final Permutation Table +IP_INV = [40, 8, 48, 16, 56, 24, 64, 32, + 39, 7, 47, 15, 55, 23, 63, 31, + 38, 6, 46, 14, 54, 22, 62, 30, + 37, 5, 45, 13, 53, 21, 61, 29, + 36, 4, 44, 12, 52, 20, 60, 28, + 35, 3, 43, 11, 51, 19, 59, 27, + 34, 2, 42, 10, 50, 18, 58, 26, + 33, 1, 41, 9, 49, 17, 57, 25] + +# Expansion Table +E = [32, 1, 2, 3, 4, 5, + 4, 5, 6, 7, 8, 9, + 8, 9, 10, 11, 12, 13, + 12, 13, 14, 15, 16, 17, + 16, 17, 18, 19, 20, 21, + 20, 21, 22, 23, 24, 25, + 24, 25, 26, 27, 28, 29, + 28, 29, 30, 31, 32, 1] + +# Permutation Table +P = [16, 7, 20, 21, 29, 12, 28, 17, + 1, 15, 23, 26, 5, 18, 31, 10, + 2, 8, 24, 14, 32, 27, 3, 9, + 19, 13, 30, 6, 22, 11, 4, 25] + +# S-boxes (Substitution Boxes) +S_BOXES = { + "S1": [[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7], + [0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8], + [4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0], + [15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13]], + + "S2": [[15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10], + [3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5], + [0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15], + [13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9]], + + "S3": [[10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8], + [13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1], + [13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7], + [1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12]], + + "S4": [[7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15], + [13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9], + [10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4], + [3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14]], + + "S5": [[2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9], + [14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6], + [4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14], + [11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3]], + + "S6": [[12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11], + [10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8], + [9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6], + [4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13]], + + "S7": [[4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1], + [13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6], + [1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2], + [6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12]], + + "S8": [[13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7], + [1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2], + [7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8], + [2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11]] +} + +# Permuted Choice 1 Table +PC1 = [57, 49, 41, 33, 25, 17, 9, + 1, 58, 50, 42, 34, 26, 18, + 10, 2, 59, 51, 43, 35, 27, + 19, 11, 3, 60, 52, 44, 36, + 63, 55, 47, 39, 31, 23, 15, + 7, 62, 54, 46, 38, 30, 22, + 14, 6, 61, 53, 45, 37, 29, + 21, 13, 5, 28, 20, 12, 4] + +# Permuted Choice 2 Table +PC2 = [14, 17, 11, 24, 1, 5, + 3, 28, 15, 6, 21, 10, + 23, 19, 12, 4, 26, 8, + 16, 7, 27, 20, 13, 2, + 41, 52, 31, 37, 47, 55, + 30, 40, 51, 45, 33, 48, + 44, 49, 39, 56, 34, 53, + 46, 42, 50, 36, 29, 32] +# fmt: on + + +class Operations: + @staticmethod + def string_to_bitset(string: str) -> list: + """ + Converts a string into a list of binary digits (bitset). + + Args: + string (str): The input string to be converted. + + Returns: + list: A list of binary digits representing the string. + + Examples: + >>> Operations.string_to_bitset('A') + ['0', '1', '0', '0', '0', '0', '0', '1'] + >>> len(Operations.string_to_bitset('ab')) + 16 + >>> Operations.string_to_bitset(' ') + ['0', '0', '1', '0', '0', '0', '0', '0'] + """ + return list("".join(format(ord(char), "08b") for char in string)) + + @staticmethod + def pad_right_to_multiple_of_n(bitset: list, length: int) -> list: + """ + Pads the bitset with zeros on the right until + its length is a multiple of `length`. + + Args: + bitset (list): A list of binary digits (as strings) to be padded. + + Returns: + list: The padded bitset, with a length that is a multiple of n. + + Examples: + >>> Operations.pad_right_to_multiple_of_n(['1', '0', '1'], 4) + ['1', '0', '1', '0'] + >>> len(Operations.pad_right_to_multiple_of_n(['1'] * 64, 64)) % 64 + 0 + >>> len(Operations.pad_right_to_multiple_of_n(['0'] * 63, 64)) % 64 + 0 + """ + if len(bitset) % length != 0: + bitset += list("0" * (length - len(bitset) % length)) + return bitset + + @staticmethod + def pad_left_to_multiple_of_n(bitset: list, length: int) -> list: + """ + Pads the bitset with zeros on the left until + its length is a multiple of length. + + Args: + bitset (list): A list of binary digits (as strings) to be padded. + + Returns: + list: The padded bitset, with a length that is a multiple of n. + + Examples: + >>> Operations.pad_left_to_multiple_of_n(['1', '0', '1'], 4) + ['0', '1', '0', '1'] + >>> len(Operations.pad_left_to_multiple_of_n(['1'] * 64, 64)) % 64 + 0 + >>> len(Operations.pad_left_to_multiple_of_n(['0'] * 63, 64)) % 64 + 0 + """ + if len(bitset) % length != 0: + bitset = list("0" * (length - len(bitset) % length)) + bitset + return bitset + + @staticmethod + def bitset_to_hex(bitset: list) -> str: + """ + Converts a list of binary digits + into its 16 digit hexadecimal representation. + + Args: + bitset (list): A list of binary digits (as strings)\ + representing a binary number. + + Returns: + str: The 16 digit hexadecimal representation of the binary number. + + Examples: + >>> Operations.bitset_to_hex(['1', '0', '1', '0', '1', '1', '1', '0']) + '00000000000000ae' + >>> Operations.bitset_to_hex(['1', '1', '1', '1'] * 16) + 'ffffffffffffffff' + >>> Operations.bitset_to_hex(['0'] * 64) + '0000000000000000' + """ + return format(int("".join(bitset), 2), "016x") + + @staticmethod + def hex_to_bitset(hex_string: str, left_pad: int) -> list: + """ + Converts a hexadecimal string to a bitset + and pads the bitset to a specified length. + + Args: + hex_string (str): The hexadecimal string to convert. + left_pad (int): The length to pad the bitset on the left. + + Returns: + list: The padded bitset. + + Examples: + >>> Operations.hex_to_bitset('ae', 8) + ['1', '0', '1', '0', '1', '1', '1', '0'] + >>> Operations.hex_to_bitset('1f', 10) + ['0', '0', '0', '0', '0', '1', '1', '1', '1', '1'] + """ + return Operations.pad_left_to_multiple_of_n( + list(format(int(hex_string, 16), f"0{left_pad}b")), left_pad + ) + + @staticmethod + def bitset_to_string(bitset: list) -> str: + """ + Converts a bitset into a string by interpreting every 8 bits as a character. + + Args: + bitset (list): The list of binary digits (bitset). + + Returns: + str: The decoded string. + + Examples: + >>> Operations.bitset_to_string(['0', '1', '0', '0', '0', '0', '0', '1']) + 'A' + >>> Operations.bitset_to_string(['0', '1', '0', '0', '0', '1',\ +'0', '0', '0', '1', '0', '1', '0', '1', '1', '0']) + 'DV' + """ + return "".join( + chr(int("".join(bitset[i : i + 8]), 2)) for i in range(0, len(bitset), 8) + ) + + @staticmethod + def xor(bitset1: list, bitset2: list) -> list: + """ + Applies a bitwise XOR operation between two bitsets of the same length. + + Args: + bitset1 (list): The first bitset. + bitset2 (list): The second bitset. + + Returns: + list: The result of the XOR operation as a new bitset. + + Examples: + >>> Operations.xor(['0', '1', '0', '1'], ['1', '0', '1', '1']) + ['1', '1', '1', '0'] + >>> Operations.xor(['1', '0', '1', '0'], ['0', '0', '0', '1']) + ['1', '0', '1', '1'] + >>> Operations.xor(['1', '0', '1', '0', '1'], ['0', '0', '0', '1']) + Traceback (most recent call last): + ... + ValueError: Bitsets must be of the same length + >>> Operations.xor(['1', '0', '1', '0'], ['0', '0', '0', '1', '1']) + Traceback (most recent call last): + ... + ValueError: Bitsets must be of the same length + """ + if len(bitset1) != len(bitset2): + raise ValueError("Bitsets must be of the same length") + return [str(int(bitset1[i]) ^ int(bitset2[i])) for i in range(len(bitset1))] + + @staticmethod + def shift_left(bitset: list, position: int) -> list: + """ + Performs a rotated left shift on a bitset by `position` positions. + + Args: + bitset (list): The bitset to be shifted. + n (int): The number of positions to shift. + + Returns: + list: The left-shifted bitset. + + Examples: + >>> Operations.shift_left(['1', '0', '0', '1'], 2) + ['0', '1', '1', '0'] + >>> Operations.shift_left(['0', '1', '1', '1'], 1) + ['1', '1', '1', '0'] + >>> Operations.shift_left(['0', '1', '1', '1'], 7) + ['1', '0', '1', '1'] + """ + position = position % len(bitset) + return bitset[position:] + bitset[:position] + + @staticmethod + def permute(bitset: list, permutation: list) -> list: + """ + Permutes a bitset according to a given permutation table. + + Args: + bitset (list): The bitset to be permuted. + permutation (list): The permutation table specifying the new order. + + Returns: + list: The permuted bitset. + + Examples: + >>> Operations.permute(['1', '0', '1', '0', '1', '1'], [6, 5, 4, 3, 2, 1]) + ['1', '1', '0', '1', '0', '1'] + >>> Operations.permute(['0', '1', '1', '0', '1', '0'], [3, 1, 6, 5, 4, 2]) + ['1', '0', '0', '1', '0', '1'] + >>> Operations.permute(['0', '1', '1', '0', '1', '0'], \ +[3, 1, 6, 5, 4, 2, 7]) + Traceback (most recent call last): + ... + ValueError: Permutation values must be within the range of the bitset + """ + if not all(0 < i <= len(bitset) for i in permutation): + raise ValueError( + "Permutation values must be within the range of the bitset" + ) + return [bitset[i - 1] for i in permutation] + + +class Des: + @staticmethod + def generate_key() -> str: + """ + Generates a random hexadecimal key of 16 characters (64 bits). + + Returns: + str: A random hexadecimal key. + + Examples: + >>> key = Des.generate_key() + >>> len(key) # Check if the key length is correct + 16 + >>> # Ensure key only contains valid hex characters + >>> all(c in "0123456789abcdef" for c in key) + True + """ + return "".join(random.choice("0123456789abcdef") for i in range(16)) + + @staticmethod + def key_schedule(key: str) -> list: + """ + Generates 16 subkeys (round keys) from a given + 64-bit hexadecimal key using the DES key schedule. + + Args: + key (str): A 16-character hexadecimal string representing a 64-bit key. + + Returns: + list: A list of 16 subkeys, each of which is a permuted bitset. + + Examples: + >>> subkeys = Des.key_schedule('133457799BBCDFF1') + >>> len(subkeys) # Check that 16 subkeys are generated + 16 + >>> # Ensure each subkey is 48 bits long + >>> all(len(subkey) == 48 for subkey in subkeys) + True + """ + key_bitset = Operations.hex_to_bitset(key, 64) + key_permuted = Operations.permute(key_bitset, PC1) + + left_key = key_permuted[:28] + right_key = key_permuted[28:] + + keys = [] + + shift_list = [1, 2, 9, 16] + + for i in range(1, 17): + if i in shift_list: + left_key = Operations.shift_left(left_key, 1) + right_key = Operations.shift_left(right_key, 1) + else: + left_key = Operations.shift_left(left_key, 2) + right_key = Operations.shift_left(right_key, 2) + keys.append(Operations.permute(left_key + right_key, PC2)) + + return keys + + @staticmethod + def des(keys: list, plain_bitset: list) -> list: + """ + Encrypts a plain bitset using the provided + subkeys with DES encryption algorithm. + + Args: + keys (list): A list of 16 subkeys, each 48 bits long. + plain_bitset (list): A bitset representing the plain text,\ + which should be divisible into 64-bit blocks. + + Returns: + list: The encrypted bitset. + + Examples: + >>> plain_bitset = Operations.string_to_bitset("Test string") + >>> plain_bitset = Operations.pad_right_to_multiple_of_n(plain_bitset, 64) + >>> keys = Des.key_schedule(Des.generate_key()) + >>> cipher_bitset = Des.des(keys, plain_bitset) + >>> # The output length should be the same as the input + >>> len(cipher_bitset) == len(plain_bitset) + True + >>> # Decryption should be the inverse of encryption + >>> plain_bitset == Des.des(keys[::-1], cipher_bitset) + True + """ + + no_of_blocks = len(plain_bitset) // 64 + + cipher_bitset = [] + + for i in range(no_of_blocks): + block = plain_bitset[i * 64 : (i + 1) * 64] + initial_permutation = Operations.permute(block, IP) + + left_bits = initial_permutation[:32] + right_bits = initial_permutation[32:] + + for i in range(16): + right_expanded = Operations.permute(right_bits, E) + right_xor_key = Operations.xor(right_expanded, keys[i]) + right_s_box = [] + for j in range(8): + s_box_input = right_xor_key[j * 6 : (j + 1) * 6] + row = 2 * int(s_box_input[0]) + int(s_box_input[5]) + col = int("".join(s_box_input[1:5]), 2) + s_box_output = format(S_BOXES[f"S{j + 1}"][row][col], "04b") + right_s_box += list(s_box_output) + + right_permuted = Operations.permute(right_s_box, P) + + left_bits, right_bits = ( + right_bits, + Operations.xor(left_bits, right_permuted), + ) + + left_bits, right_bits = right_bits, left_bits + cipher_bitset += Operations.permute(left_bits + right_bits, IP_INV) + return cipher_bitset + + @staticmethod + def encrypt(key: str, input_string: str) -> str: + """ + Encrypts a given input string using the DES encryption algorithm. + + Args: + key (str): A 16-character hexadecimal string representing a 64-bit key. + input_string (str): The plain text string to be encrypted. + + Returns: + str: A hexadecimal string representing the encrypted data. + + Examples: + >>> key = '133457799BBCDFF1' + >>> input_string = 'Test string' + >>> encrypted = Des.encrypt(key, input_string) + >>> encrypted # Checking the cipher to ensure consistency + 'c84e3c8fb646872720b224896db4f60' + """ + plain_bitset = Operations.string_to_bitset(input_string) + padded_bitset = Operations.pad_right_to_multiple_of_n(plain_bitset, 64) + keys = Des.key_schedule(key) + cipher_bitset = Des.des(keys, padded_bitset) + return Operations.bitset_to_hex(cipher_bitset) + + @staticmethod + def decrypt(key: str, cipher_text: str) -> str: + """ + Decrypts the given cipher text using DES decryption. + + Args: + key (str): A 16-character hexadecimal string representing a 64-bit key. + cipher_text (str): A hexadecimal string representing the encrypted data. + + Returns: + str: The decrypted plain text string. + + Examples: + >>> key = '133457799BBCDFF1' + >>> encrypted_string = 'c84e3c8fb646872720b224896db4f60' + >>> decrypted = Des.decrypt(key, encrypted_string) + >>> decrypted # Checking the cipher to ensure consistency + 'Test string' + """ + cipher_bitset = Operations.hex_to_bitset(cipher_text, 64) + keys = Des.key_schedule(key)[::-1] + plain_bitset = Des.des(keys, cipher_bitset) + return Operations.bitset_to_string(plain_bitset).replace("\x00", "") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + while True: + print() + print("################ DES Algorithm ################") + print() + print("Select an option:") + print("1. To encrypt a string, enter 'e'") + print("2. To decrypt a string, enter 'd'") + print("3. To generate a key, enter 'k'") + print("4. To Quit, enter 'q'") + mode = input("Enter the option: ").strip().lower() + if mode not in ["e", "d", "k", "q"]: + print("Invalid option. Please try again.") + continue + match mode: + case "q": + break + case "k": + print(f"Generated key: {Des.generate_key()}") + case "e": + key = input("Enter the key: ").strip() + if len(key) != 16 and not all( + char in "0123456789abcdef" for char in key + ): + print("Invalid key. Please try again.") + continue + input_string = input("Enter the string to encrypt: ").strip() + print(f"Encrypted string: {Des.encrypt(key, input_string)}") + case "d": + key = input("Enter the key: ").strip() + if len(key) != 16 and not all( + char in "0123456789abcdef" for char in key + ): + print("Invalid key. Please try again.") + continue + cipher_text = input("Enter the cipher text to decrypt: ").strip() + print(f"Decrypted string: {Des.decrypt(key, cipher_text)}") diff --git a/ciphers/deterministic_miller_rabin.py b/ciphers/deterministic_miller_rabin.py new file mode 100644 index 000000000000..2191caf630a7 --- /dev/null +++ b/ciphers/deterministic_miller_rabin.py @@ -0,0 +1,137 @@ +"""Created by Nathan Damon, @bizzfitch on github +>>> test_miller_rabin() +""" + + +def miller_rabin(n: int, allow_probable: bool = False) -> bool: + """Deterministic Miller-Rabin algorithm for primes ~< 3.32e24. + + Uses numerical analysis results to return whether or not the passed number + is prime. If the passed number is above the upper limit, and + allow_probable is True, then a return value of True indicates that n is + probably prime. This test does not allow False negatives- a return value + of False is ALWAYS composite. + + Parameters + ---------- + n : int + The integer to be tested. Since we usually care if a number is prime, + n < 2 returns False instead of raising a ValueError. + allow_probable: bool, default False + Whether or not to test n above the upper bound of the deterministic test. + + Raises + ------ + ValueError + + Reference + --------- + https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test + """ + if n == 2: + return True + if not n % 2 or n < 2: + return False + if n > 5 and n % 10 not in (1, 3, 7, 9): # can quickly check last digit + return False + if n > 3_317_044_064_679_887_385_961_981 and not allow_probable: + raise ValueError( + "Warning: upper bound of deterministic test is exceeded. " + "Pass allow_probable=True to allow probabilistic test. " + "A return value of True indicates a probable prime." + ) + # array bounds provided by analysis + bounds = [ + 2_047, + 1_373_653, + 25_326_001, + 3_215_031_751, + 2_152_302_898_747, + 3_474_749_660_383, + 341_550_071_728_321, + 1, + 3_825_123_056_546_413_051, + 1, + 1, + 318_665_857_834_031_151_167_461, + 3_317_044_064_679_887_385_961_981, + ] + + primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41] + for idx, _p in enumerate(bounds, 1): + if n < _p: + # then we have our last prime to check + plist = primes[:idx] + break + d, s = n - 1, 0 + # break up n -1 into a power of 2 (s) and + # remaining odd component + # essentially, solve for d * 2 ** s == n - 1 + while d % 2 == 0: + d //= 2 + s += 1 + for prime in plist: + pr = False + for r in range(s): + m = pow(prime, d * 2**r, n) + # see article for analysis explanation for m + if (r == 0 and m == 1) or ((m + 1) % n == 0): + pr = True + # this loop will not determine compositeness + break + if pr: + continue + # if pr is False, then the above loop never evaluated to true, + # and the n MUST be composite + return False + return True + + +def test_miller_rabin() -> None: + """Testing a nontrivial (ends in 1, 3, 7, 9) composite + and a prime in each range. + """ + assert not miller_rabin(561) + assert miller_rabin(563) + # 2047 + + assert not miller_rabin(838_201) + assert miller_rabin(838_207) + # 1_373_653 + + assert not miller_rabin(17_316_001) + assert miller_rabin(17_316_017) + # 25_326_001 + + assert not miller_rabin(3_078_386_641) + assert miller_rabin(3_078_386_653) + # 3_215_031_751 + + assert not miller_rabin(1_713_045_574_801) + assert miller_rabin(1_713_045_574_819) + # 2_152_302_898_747 + + assert not miller_rabin(2_779_799_728_307) + assert miller_rabin(2_779_799_728_327) + # 3_474_749_660_383 + + assert not miller_rabin(113_850_023_909_441) + assert miller_rabin(113_850_023_909_527) + # 341_550_071_728_321 + + assert not miller_rabin(1_275_041_018_848_804_351) + assert miller_rabin(1_275_041_018_848_804_391) + # 3_825_123_056_546_413_051 + + assert not miller_rabin(79_666_464_458_507_787_791_867) + assert miller_rabin(79_666_464_458_507_787_791_951) + # 318_665_857_834_031_151_167_461 + + assert not miller_rabin(552_840_677_446_647_897_660_333) + assert miller_rabin(552_840_677_446_647_897_660_359) + # 3_317_044_064_679_887_385_961_981 + # upper limit for probabilistic test + + +if __name__ == "__main__": + test_miller_rabin() diff --git a/ciphers/diffie.py b/ciphers/diffie.py new file mode 100644 index 000000000000..1e1e868999b6 --- /dev/null +++ b/ciphers/diffie.py @@ -0,0 +1,53 @@ +from __future__ import annotations + + +def find_primitive(modulus: int) -> int | None: + """ + Find a primitive root modulo modulus, if one exists. + + Args: + modulus : The modulus for which to find a primitive root. + + Returns: + The primitive root if one exists, or None if there is none. + + Examples: + >>> find_primitive(7) # Modulo 7 has primitive root 3 + 3 + >>> find_primitive(11) # Modulo 11 has primitive root 2 + 2 + >>> find_primitive(8) == None # Modulo 8 has no primitive root + True + """ + for r in range(1, modulus): + li = [] + for x in range(modulus - 1): + val = pow(r, x, modulus) + if val in li: + break + li.append(val) + else: + return r + return None + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + prime = int(input("Enter a prime number q: ")) + primitive_root = find_primitive(prime) + if primitive_root is None: + print(f"Cannot find the primitive for the value: {primitive_root!r}") + else: + a_private = int(input("Enter private key of A: ")) + a_public = pow(primitive_root, a_private, prime) + b_private = int(input("Enter private key of B: ")) + b_public = pow(primitive_root, b_private, prime) + + a_secret = pow(b_public, a_private, prime) + b_secret = pow(a_public, b_private, prime) + + print("The key value generated by A is: ", a_secret) + print("The key value generated by B is: ", b_secret) diff --git a/ciphers/diffie_hellman.py b/ciphers/diffie_hellman.py new file mode 100644 index 000000000000..aec7fb3eaf17 --- /dev/null +++ b/ciphers/diffie_hellman.py @@ -0,0 +1,267 @@ +from binascii import hexlify +from hashlib import sha256 +from os import urandom + +# RFC 3526 - More Modular Exponential (MODP) Diffie-Hellman groups for +# Internet Key Exchange (IKE) https://tools.ietf.org/html/rfc3526 + +primes = { + # 1536-bit + 5: { + "prime": int( + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + "670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF", + base=16, + ), + "generator": 2, + }, + # 2048-bit + 14: { + "prime": int( + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" + "15728E5A8AACAA68FFFFFFFFFFFFFFFF", + base=16, + ), + "generator": 2, + }, + # 3072-bit + 15: { + "prime": int( + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" + "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64" + "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" + "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B" + "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C" + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31" + "43DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF", + base=16, + ), + "generator": 2, + }, + # 4096-bit + 16: { + "prime": int( + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" + "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64" + "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" + "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B" + "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C" + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31" + "43DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D7" + "88719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA" + "2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6" + "287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED" + "1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA9" + "93B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199" + "FFFFFFFFFFFFFFFF", + base=16, + ), + "generator": 2, + }, + # 6144-bit + 17: { + "prime": int( + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08" + "8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B" + "302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9" + "A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6" + "49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8" + "FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D" + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C" + "180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718" + "3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D" + "04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D" + "B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226" + "1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C" + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC" + "E0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B26" + "99C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB" + "04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2" + "233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127" + "D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934028492" + "36C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406" + "AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918" + "DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B33205151" + "2BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03" + "F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97F" + "BEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AA" + "CC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58B" + "B7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632" + "387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E" + "6DCC4024FFFFFFFFFFFFFFFF", + base=16, + ), + "generator": 2, + }, + # 8192-bit + 18: { + "prime": int( + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" + "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64" + "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" + "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B" + "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C" + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31" + "43DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D7" + "88719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA" + "2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6" + "287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED" + "1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA9" + "93B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934028492" + "36C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BD" + "F8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831" + "179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1B" + "DB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF" + "5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6" + "D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F3" + "23A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AA" + "CC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE328" + "06A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55C" + "DA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE" + "12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E4" + "38777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300" + "741FA7BF8AFC47ED2576F6936BA424663AAB639C5AE4F568" + "3423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD9" + "22222E04A4037C0713EB57A81A23F0C73473FC646CEA306B" + "4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A" + "062B3CF5B3A278A66D2A13F83F44F82DDF310EE074AB6A36" + "4597E899A0255DC164F31CC50846851DF9AB48195DED7EA1" + "B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F92" + "4009438B481C6CD7889A002ED5EE382BC9190DA6FC026E47" + "9558E4475677E9AA9E3050E2765694DFC81F56E880B96E71" + "60C980DD98EDD3DFFFFFFFFFFFFFFFFF", + base=16, + ), + "generator": 2, + }, +} + + +class DiffieHellman: + """ + Class to represent the Diffie-Hellman key exchange protocol + + + >>> alice = DiffieHellman() + >>> bob = DiffieHellman() + + >>> alice_private = alice.get_private_key() + >>> alice_public = alice.generate_public_key() + + >>> bob_private = bob.get_private_key() + >>> bob_public = bob.generate_public_key() + + >>> # generating shared key using the DH object + >>> alice_shared = alice.generate_shared_key(bob_public) + >>> bob_shared = bob.generate_shared_key(alice_public) + + >>> assert alice_shared == bob_shared + + >>> # generating shared key using static methods + >>> alice_shared = DiffieHellman.generate_shared_key_static( + ... alice_private, bob_public + ... ) + >>> bob_shared = DiffieHellman.generate_shared_key_static( + ... bob_private, alice_public + ... ) + + >>> assert alice_shared == bob_shared + """ + + # Current minimum recommendation is 2048 bit (group 14) + def __init__(self, group: int = 14) -> None: + if group not in primes: + raise ValueError("Unsupported Group") + self.prime = primes[group]["prime"] + self.generator = primes[group]["generator"] + + self.__private_key = int(hexlify(urandom(32)), base=16) + + def get_private_key(self) -> str: + return hex(self.__private_key)[2:] + + def generate_public_key(self) -> str: + public_key = pow(self.generator, self.__private_key, self.prime) + return hex(public_key)[2:] + + def is_valid_public_key(self, key: int) -> bool: + # check if the other public key is valid based on NIST SP800-56 + return ( + 2 <= key <= self.prime - 2 + and pow(key, (self.prime - 1) // 2, self.prime) == 1 + ) + + def generate_shared_key(self, other_key_str: str) -> str: + other_key = int(other_key_str, base=16) + if not self.is_valid_public_key(other_key): + raise ValueError("Invalid public key") + shared_key = pow(other_key, self.__private_key, self.prime) + return sha256(str(shared_key).encode()).hexdigest() + + @staticmethod + def is_valid_public_key_static(remote_public_key_str: int, prime: int) -> bool: + # check if the other public key is valid based on NIST SP800-56 + return ( + 2 <= remote_public_key_str <= prime - 2 + and pow(remote_public_key_str, (prime - 1) // 2, prime) == 1 + ) + + @staticmethod + def generate_shared_key_static( + local_private_key_str: str, remote_public_key_str: str, group: int = 14 + ) -> str: + local_private_key = int(local_private_key_str, base=16) + remote_public_key = int(remote_public_key_str, base=16) + prime = primes[group]["prime"] + if not DiffieHellman.is_valid_public_key_static(remote_public_key, prime): + raise ValueError("Invalid public key") + shared_key = pow(remote_public_key, local_private_key, prime) + return sha256(str(shared_key).encode()).hexdigest() + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/ciphers/elgamal_key_generator.py b/ciphers/elgamal_key_generator.py new file mode 100644 index 000000000000..17ba55c0d013 --- /dev/null +++ b/ciphers/elgamal_key_generator.py @@ -0,0 +1,66 @@ +import os +import random +import sys + +from . import cryptomath_module as cryptomath +from . import rabin_miller + +min_primitive_root = 3 + + +# I have written my code naively same as definition of primitive root +# however every time I run this program, memory exceeded... +# so I used 4.80 Algorithm in +# Handbook of Applied Cryptography(CRC Press, ISBN : 0-8493-8523-7, October 1996) +# and it seems to run nicely! +def primitive_root(p_val: int) -> int: + print("Generating primitive root of p") + while True: + g = random.randrange(3, p_val) + if pow(g, 2, p_val) == 1: + continue + if pow(g, p_val, p_val) == 1: + continue + return g + + +def generate_key(key_size: int) -> tuple[tuple[int, int, int, int], tuple[int, int]]: + print("Generating prime p...") + p = rabin_miller.generate_large_prime(key_size) # select large prime number. + e_1 = primitive_root(p) # one primitive root on modulo p. + d = random.randrange(3, p) # private_key -> have to be greater than 2 for safety. + e_2 = cryptomath.find_mod_inverse(pow(e_1, d, p), p) + + public_key = (key_size, e_1, e_2, p) + private_key = (key_size, d) + + return public_key, private_key + + +def make_key_files(name: str, key_size: int) -> None: + if os.path.exists(f"{name}_pubkey.txt") or os.path.exists(f"{name}_privkey.txt"): + print("\nWARNING:") + print( + f'"{name}_pubkey.txt" or "{name}_privkey.txt" already exists. \n' + "Use a different name or delete these files and re-run this program." + ) + sys.exit() + + public_key, private_key = generate_key(key_size) + print(f"\nWriting public key to file {name}_pubkey.txt...") + with open(f"{name}_pubkey.txt", "w") as fo: + fo.write(f"{public_key[0]},{public_key[1]},{public_key[2]},{public_key[3]}") + + print(f"Writing private key to file {name}_privkey.txt...") + with open(f"{name}_privkey.txt", "w") as fo: + fo.write(f"{private_key[0]},{private_key[1]}") + + +def main() -> None: + print("Making key files...") + make_key_files("elgamal", 2048) + print("Key files generation successful") + + +if __name__ == "__main__": + main() diff --git a/ciphers/enigma_machine2.py b/ciphers/enigma_machine2.py new file mode 100644 index 000000000000..d94205e99ec1 --- /dev/null +++ b/ciphers/enigma_machine2.py @@ -0,0 +1,300 @@ +""" +| Wikipedia: https://en.wikipedia.org/wiki/Enigma_machine +| Video explanation: https://youtu.be/QwQVMqfoB2E +| Also check out Numberphile's and Computerphile's videos on this topic + +This module contains function ``enigma`` which emulates +the famous Enigma machine from WWII. + +Module includes: + +- ``enigma`` function +- showcase of function usage +- ``9`` randomly generated rotors +- reflector (aka static rotor) +- original alphabet + +Created by TrapinchO +""" + +from __future__ import annotations + +RotorPositionT = tuple[int, int, int] +RotorSelectionT = tuple[str, str, str] + + +# used alphabet -------------------------- +# from string.ascii_uppercase +abc = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + +# -------------------------- default selection -------------------------- +# rotors -------------------------- +rotor1 = "EGZWVONAHDCLFQMSIPJBYUKXTR" +rotor2 = "FOBHMDKEXQNRAULPGSJVTYICZW" +rotor3 = "ZJXESIUQLHAVRMDOYGTNFWPBKC" +# reflector -------------------------- +reflector = { + "A": "N", + "N": "A", + "B": "O", + "O": "B", + "C": "P", + "P": "C", + "D": "Q", + "Q": "D", + "E": "R", + "R": "E", + "F": "S", + "S": "F", + "G": "T", + "T": "G", + "H": "U", + "U": "H", + "I": "V", + "V": "I", + "J": "W", + "W": "J", + "K": "X", + "X": "K", + "L": "Y", + "Y": "L", + "M": "Z", + "Z": "M", +} + +# -------------------------- extra rotors -------------------------- +rotor4 = "RMDJXFUWGISLHVTCQNKYPBEZOA" +rotor5 = "SGLCPQWZHKXAREONTFBVIYJUDM" +rotor6 = "HVSICLTYKQUBXDWAJZOMFGPREN" +rotor7 = "RZWQHFMVDBKICJLNTUXAGYPSOE" +rotor8 = "LFKIJODBEGAMQPXVUHYSTCZRWN" +rotor9 = "KOAEGVDHXPQZMLFTYWJNBRCIUS" + + +def _validator( + rotpos: RotorPositionT, rotsel: RotorSelectionT, pb: str +) -> tuple[RotorPositionT, RotorSelectionT, dict[str, str]]: + """ + Checks if the values can be used for the ``enigma`` function + + >>> _validator((1,1,1), (rotor1, rotor2, rotor3), 'POLAND') + ((1, 1, 1), ('EGZWVONAHDCLFQMSIPJBYUKXTR', 'FOBHMDKEXQNRAULPGSJVTYICZW', \ +'ZJXESIUQLHAVRMDOYGTNFWPBKC'), \ +{'P': 'O', 'O': 'P', 'L': 'A', 'A': 'L', 'N': 'D', 'D': 'N'}) + + :param rotpos: rotor_positon + :param rotsel: rotor_selection + :param pb: plugb -> validated and transformed + :return: (`rotpos`, `rotsel`, `pb`) + """ + # Checks if there are 3 unique rotors + + if (unique_rotsel := len(set(rotsel))) < 3: + msg = f"Please use 3 unique rotors (not {unique_rotsel})" + raise Exception(msg) + + # Checks if rotor positions are valid + rotorpos1, rotorpos2, rotorpos3 = rotpos + if not 0 < rotorpos1 <= len(abc): + msg = f"First rotor position is not within range of 1..26 ({rotorpos1}" + raise ValueError(msg) + if not 0 < rotorpos2 <= len(abc): + msg = f"Second rotor position is not within range of 1..26 ({rotorpos2})" + raise ValueError(msg) + if not 0 < rotorpos3 <= len(abc): + msg = f"Third rotor position is not within range of 1..26 ({rotorpos3})" + raise ValueError(msg) + + # Validates string and returns dict + pbdict = _plugboard(pb) + + return rotpos, rotsel, pbdict + + +def _plugboard(pbstring: str) -> dict[str, str]: + """ + https://en.wikipedia.org/wiki/Enigma_machine#Plugboard + + >>> _plugboard('PICTURES') + {'P': 'I', 'I': 'P', 'C': 'T', 'T': 'C', 'U': 'R', 'R': 'U', 'E': 'S', 'S': 'E'} + >>> _plugboard('POLAND') + {'P': 'O', 'O': 'P', 'L': 'A', 'A': 'L', 'N': 'D', 'D': 'N'} + + In the code, ``pb`` stands for ``plugboard`` + + Pairs can be separated by spaces + + :param pbstring: string containing plugboard setting for the Enigma machine + :return: dictionary containing converted pairs + """ + + # tests the input string if it + # a) is type string + # b) has even length (so pairs can be made) + if not isinstance(pbstring, str): + msg = f"Plugboard setting isn't type string ({type(pbstring)})" + raise TypeError(msg) + if len(pbstring) % 2 != 0: + msg = f"Odd number of symbols ({len(pbstring)})" + raise Exception(msg) + if pbstring == "": + return {} + + pbstring.replace(" ", "") + + # Checks if all characters are unique + tmppbl = set() + for i in pbstring: + if i not in abc: + msg = f"'{i}' not in list of symbols" + raise Exception(msg) + if i in tmppbl: + msg = f"Duplicate symbol ({i})" + raise Exception(msg) + tmppbl.add(i) + del tmppbl + + # Created the dictionary + pb = {} + for j in range(0, len(pbstring) - 1, 2): + pb[pbstring[j]] = pbstring[j + 1] + pb[pbstring[j + 1]] = pbstring[j] + + return pb + + +def enigma( + text: str, + rotor_position: RotorPositionT, + rotor_selection: RotorSelectionT = (rotor1, rotor2, rotor3), + plugb: str = "", +) -> str: + """ + The only difference with real-world enigma is that ``I`` allowed string input. + All characters are converted to uppercase. (non-letter symbol are ignored) + + | How it works: + | (for every letter in the message) + + - Input letter goes into the plugboard. + If it is connected to another one, switch it. + + - Letter goes through ``3`` rotors. + Each rotor can be represented as ``2`` sets of symbol, where one is shuffled. + Each symbol from the first set has corresponding symbol in + the second set and vice versa. + + example:: + + | ABCDEFGHIJKLMNOPQRSTUVWXYZ | e.g. F=D and D=F + | VKLEPDBGRNWTFCJOHQAMUZYIXS | + + - Symbol then goes through reflector (static rotor). + There it is switched with paired symbol. + The reflector can be represented as ``2`` sets, each with half of the alphanet. + There are usually ``10`` pairs of letters. + + Example:: + + | ABCDEFGHIJKLM | e.g. E is paired to X + | ZYXWVUTSRQPON | so when E goes in X goes out and vice versa + + - Letter then goes through the rotors again + + - If the letter is connected to plugboard, it is switched. + + - Return the letter + + >>> enigma('Hello World!', (1, 2, 1), plugb='pictures') + 'KORYH JUHHI!' + >>> enigma('KORYH, juhhi!', (1, 2, 1), plugb='pictures') + 'HELLO, WORLD!' + >>> enigma('hello world!', (1, 1, 1), plugb='pictures') + 'FPNCZ QWOBU!' + >>> enigma('FPNCZ QWOBU', (1, 1, 1), plugb='pictures') + 'HELLO WORLD' + + + :param text: input message + :param rotor_position: tuple with ``3`` values in range ``1``.. ``26`` + :param rotor_selection: tuple with ``3`` rotors + :param plugb: string containing plugboard configuration (default ``''``) + :return: en/decrypted string + """ + + text = text.upper() + rotor_position, rotor_selection, plugboard = _validator( + rotor_position, rotor_selection, plugb.upper() + ) + + rotorpos1, rotorpos2, rotorpos3 = rotor_position + rotor1, rotor2, rotor3 = rotor_selection + rotorpos1 -= 1 + rotorpos2 -= 1 + rotorpos3 -= 1 + + result = [] + + # encryption/decryption process -------------------------- + for symbol in text: + if symbol in abc: + # 1st plugboard -------------------------- + if symbol in plugboard: + symbol = plugboard[symbol] + + # rotor ra -------------------------- + index = abc.index(symbol) + rotorpos1 + symbol = rotor1[index % len(abc)] + + # rotor rb -------------------------- + index = abc.index(symbol) + rotorpos2 + symbol = rotor2[index % len(abc)] + + # rotor rc -------------------------- + index = abc.index(symbol) + rotorpos3 + symbol = rotor3[index % len(abc)] + + # reflector -------------------------- + # this is the reason you don't need another machine to decipher + + symbol = reflector[symbol] + + # 2nd rotors + symbol = abc[rotor3.index(symbol) - rotorpos3] + symbol = abc[rotor2.index(symbol) - rotorpos2] + symbol = abc[rotor1.index(symbol) - rotorpos1] + + # 2nd plugboard + if symbol in plugboard: + symbol = plugboard[symbol] + + # moves/resets rotor positions + rotorpos1 += 1 + if rotorpos1 >= len(abc): + rotorpos1 = 0 + rotorpos2 += 1 + if rotorpos2 >= len(abc): + rotorpos2 = 0 + rotorpos3 += 1 + if rotorpos3 >= len(abc): + rotorpos3 = 0 + + # else: + # pass + # Error could be also raised + # raise ValueError( + # 'Invalid symbol('+repr(symbol)+')') + result.append(symbol) + + return "".join(result) + + +if __name__ == "__main__": + message = "This is my Python script that emulates the Enigma machine from WWII." + rotor_pos = (1, 1, 1) + pb = "pictures" + rotor_sel = (rotor2, rotor4, rotor8) + en = enigma(message, rotor_pos, rotor_sel, pb) + + print("Encrypted message:", en) + print("Decrypted message:", enigma(en, rotor_pos, rotor_sel, pb)) diff --git a/ciphers/fractionated_morse_cipher.py b/ciphers/fractionated_morse_cipher.py new file mode 100644 index 000000000000..6c4c415abac1 --- /dev/null +++ b/ciphers/fractionated_morse_cipher.py @@ -0,0 +1,168 @@ +""" +Python program for the Fractionated Morse Cipher. + +The Fractionated Morse cipher first converts the plaintext to Morse code, +then enciphers fixed-size blocks of Morse code back to letters. +This procedure means plaintext letters are mixed into the ciphertext letters, +making it more secure than substitution ciphers. + +http://practicalcryptography.com/ciphers/fractionated-morse-cipher/ +""" + +import string + +MORSE_CODE_DICT = { + "A": ".-", + "B": "-...", + "C": "-.-.", + "D": "-..", + "E": ".", + "F": "..-.", + "G": "--.", + "H": "....", + "I": "..", + "J": ".---", + "K": "-.-", + "L": ".-..", + "M": "--", + "N": "-.", + "O": "---", + "P": ".--.", + "Q": "--.-", + "R": ".-.", + "S": "...", + "T": "-", + "U": "..-", + "V": "...-", + "W": ".--", + "X": "-..-", + "Y": "-.--", + "Z": "--..", + " ": "", +} + +# Define possible trigrams of Morse code +MORSE_COMBINATIONS = [ + "...", + "..-", + "..x", + ".-.", + ".--", + ".-x", + ".x.", + ".x-", + ".xx", + "-..", + "-.-", + "-.x", + "--.", + "---", + "--x", + "-x.", + "-x-", + "-xx", + "x..", + "x.-", + "x.x", + "x-.", + "x--", + "x-x", + "xx.", + "xx-", + "xxx", +] + +# Create a reverse dictionary for Morse code +REVERSE_DICT = {value: key for key, value in MORSE_CODE_DICT.items()} + + +def encode_to_morse(plaintext: str) -> str: + """Encode a plaintext message into Morse code. + + Args: + plaintext: The plaintext message to encode. + + Returns: + The Morse code representation of the plaintext message. + + Example: + >>> encode_to_morse("defend the east") + '-..x.x..-.x.x-.x-..xx-x....x.xx.x.-x...x-' + """ + return "x".join([MORSE_CODE_DICT.get(letter.upper(), "") for letter in plaintext]) + + +def encrypt_fractionated_morse(plaintext: str, key: str) -> str: + """Encrypt a plaintext message using Fractionated Morse Cipher. + + Args: + plaintext: The plaintext message to encrypt. + key: The encryption key. + + Returns: + The encrypted ciphertext. + + Example: + >>> encrypt_fractionated_morse("defend the east","Roundtable") + 'ESOAVVLJRSSTRX' + + """ + morse_code = encode_to_morse(plaintext) + key = key.upper() + string.ascii_uppercase + key = "".join(sorted(set(key), key=key.find)) + + # Ensure morse_code length is a multiple of 3 + padding_length = 3 - (len(morse_code) % 3) + morse_code += "x" * padding_length + + fractionated_morse_dict = {v: k for k, v in zip(key, MORSE_COMBINATIONS)} + fractionated_morse_dict["xxx"] = "" + encrypted_text = "".join( + [ + fractionated_morse_dict[morse_code[i : i + 3]] + for i in range(0, len(morse_code), 3) + ] + ) + return encrypted_text + + +def decrypt_fractionated_morse(ciphertext: str, key: str) -> str: + """Decrypt a ciphertext message encrypted with Fractionated Morse Cipher. + + Args: + ciphertext: The ciphertext message to decrypt. + key: The decryption key. + + Returns: + The decrypted plaintext message. + + Example: + >>> decrypt_fractionated_morse("ESOAVVLJRSSTRX","Roundtable") + 'DEFEND THE EAST' + """ + key = key.upper() + string.ascii_uppercase + key = "".join(sorted(set(key), key=key.find)) + + inverse_fractionated_morse_dict = dict(zip(key, MORSE_COMBINATIONS)) + morse_code = "".join( + [inverse_fractionated_morse_dict.get(letter, "") for letter in ciphertext] + ) + decrypted_text = "".join( + [REVERSE_DICT[code] for code in morse_code.split("x")] + ).strip() + return decrypted_text + + +if __name__ == "__main__": + """ + Example usage of Fractionated Morse Cipher. + """ + plaintext = "defend the east" + print("Plain Text:", plaintext) + key = "ROUNDTABLE" + + ciphertext = encrypt_fractionated_morse(plaintext, key) + print("Encrypted:", ciphertext) + + decrypted_text = decrypt_fractionated_morse(ciphertext, key) + print("Decrypted:", decrypted_text) diff --git a/ciphers/gronsfeld_cipher.py b/ciphers/gronsfeld_cipher.py new file mode 100644 index 000000000000..a72b141bd502 --- /dev/null +++ b/ciphers/gronsfeld_cipher.py @@ -0,0 +1,45 @@ +from string import ascii_uppercase + + +def gronsfeld(text: str, key: str) -> str: + """ + Encrypt plaintext with the Gronsfeld cipher + + >>> gronsfeld('hello', '412') + 'LFNPP' + >>> gronsfeld('hello', '123') + 'IGOMQ' + >>> gronsfeld('', '123') + '' + >>> gronsfeld('yes, ¥€$ - _!@#%?', '0') + 'YES, ¥€$ - _!@#%?' + >>> gronsfeld('yes, ¥€$ - _!@#%?', '01') + 'YFS, ¥€$ - _!@#%?' + >>> gronsfeld('yes, ¥€$ - _!@#%?', '012') + 'YFU, ¥€$ - _!@#%?' + >>> gronsfeld('yes, ¥€$ - _!@#%?', '') + Traceback (most recent call last): + ... + ZeroDivisionError: division by zero + """ + ascii_len = len(ascii_uppercase) + key_len = len(key) + encrypted_text = "" + keys = [int(char) for char in key] + upper_case_text = text.upper() + + for i, char in enumerate(upper_case_text): + if char in ascii_uppercase: + new_position = (ascii_uppercase.index(char) + keys[i % key_len]) % ascii_len + shifted_letter = ascii_uppercase[new_position] + encrypted_text += shifted_letter + else: + encrypted_text += char + + return encrypted_text + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/ciphers/hill_cipher.py b/ciphers/hill_cipher.py new file mode 100644 index 000000000000..a6038e576c14 --- /dev/null +++ b/ciphers/hill_cipher.py @@ -0,0 +1,222 @@ +""" + +Hill Cipher: +The 'HillCipher' class below implements the Hill Cipher algorithm which uses +modern linear algebra techniques to encode and decode text using an encryption +key matrix. + +Algorithm: +Let the order of the encryption key be N (as it is a square matrix). +Your text is divided into batches of length N and converted to numerical vectors +by a simple mapping starting with A=0 and so on. + +The key is then multiplied with the newly created batch vector to obtain the +encoded vector. After each multiplication modular 36 calculations are performed +on the vectors so as to bring the numbers between 0 and 36 and then mapped with +their corresponding alphanumerics. + +While decrypting, the decrypting key is found which is the inverse of the +encrypting key modular 36. The same process is repeated for decrypting to get +the original message back. + +Constraints: +The determinant of the encryption key matrix must be relatively prime w.r.t 36. + +Note: +This implementation only considers alphanumerics in the text. If the length of +the text to be encrypted is not a multiple of the break key(the length of one +batch of letters), the last character of the text is added to the text until the +length of the text reaches a multiple of the break_key. So the text after +decrypting might be a little different than the original text. + +References: +https://apprendre-en-ligne.net/crypto/hill/Hillciph.pdf +https://www.youtube.com/watch?v=kfmNeskzs2o +https://www.youtube.com/watch?v=4RhLNDqcjpA + +""" + +import string + +import numpy as np + +from maths.greatest_common_divisor import greatest_common_divisor + + +class HillCipher: + key_string = string.ascii_uppercase + string.digits + # This cipher takes alphanumerics into account + # i.e. a total of 36 characters + + # take x and return x % len(key_string) + modulus = np.vectorize(lambda x: x % 36) + + to_int = np.vectorize(round) + + def __init__(self, encrypt_key: np.ndarray) -> None: + """ + encrypt_key is an NxN numpy array + """ + self.encrypt_key = self.modulus(encrypt_key) # mod36 calc's on the encrypt key + self.check_determinant() # validate the determinant of the encryption key + self.break_key = encrypt_key.shape[0] + + def replace_letters(self, letter: str) -> int: + """ + >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) + >>> hill_cipher.replace_letters('T') + 19 + >>> hill_cipher.replace_letters('0') + 26 + """ + return self.key_string.index(letter) + + def replace_digits(self, num: int) -> str: + """ + >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) + >>> hill_cipher.replace_digits(19) + 'T' + >>> hill_cipher.replace_digits(26) + '0' + >>> hill_cipher.replace_digits(26.1) + '0' + """ + return self.key_string[int(num)] + + def check_determinant(self) -> None: + """ + >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) + >>> hill_cipher.check_determinant() + """ + det = round(np.linalg.det(self.encrypt_key)) + + if det < 0: + det = det % len(self.key_string) + + req_l = len(self.key_string) + if greatest_common_divisor(det, len(self.key_string)) != 1: + msg = ( + f"determinant modular {req_l} of encryption key({det}) " + f"is not co prime w.r.t {req_l}.\nTry another key." + ) + raise ValueError(msg) + + def process_text(self, text: str) -> str: + """ + >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) + >>> hill_cipher.process_text('Testing Hill Cipher') + 'TESTINGHILLCIPHERR' + >>> hill_cipher.process_text('hello') + 'HELLOO' + """ + chars = [char for char in text.upper() if char in self.key_string] + + last = chars[-1] + while len(chars) % self.break_key != 0: + chars.append(last) + + return "".join(chars) + + def encrypt(self, text: str) -> str: + """ + >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) + >>> hill_cipher.encrypt('testing hill cipher') + 'WHXYJOLM9C6XT085LL' + >>> hill_cipher.encrypt('hello') + '85FF00' + """ + text = self.process_text(text.upper()) + encrypted = "" + + for i in range(0, len(text) - self.break_key + 1, self.break_key): + batch = text[i : i + self.break_key] + vec = [self.replace_letters(char) for char in batch] + batch_vec = np.array([vec]).T + batch_encrypted = self.modulus(self.encrypt_key.dot(batch_vec)).T.tolist()[ + 0 + ] + encrypted_batch = "".join( + self.replace_digits(num) for num in batch_encrypted + ) + encrypted += encrypted_batch + + return encrypted + + def make_decrypt_key(self) -> np.ndarray: + """ + >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) + >>> hill_cipher.make_decrypt_key() + array([[ 6, 25], + [ 5, 26]]) + """ + det = round(np.linalg.det(self.encrypt_key)) + + if det < 0: + det = det % len(self.key_string) + det_inv = None + for i in range(len(self.key_string)): + if (det * i) % len(self.key_string) == 1: + det_inv = i + break + + inv_key = ( + det_inv * np.linalg.det(self.encrypt_key) * np.linalg.inv(self.encrypt_key) + ) + + return self.to_int(self.modulus(inv_key)) + + def decrypt(self, text: str) -> str: + """ + >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]])) + >>> hill_cipher.decrypt('WHXYJOLM9C6XT085LL') + 'TESTINGHILLCIPHERR' + >>> hill_cipher.decrypt('85FF00') + 'HELLOO' + """ + decrypt_key = self.make_decrypt_key() + text = self.process_text(text.upper()) + decrypted = "" + + for i in range(0, len(text) - self.break_key + 1, self.break_key): + batch = text[i : i + self.break_key] + vec = [self.replace_letters(char) for char in batch] + batch_vec = np.array([vec]).T + batch_decrypted = self.modulus(decrypt_key.dot(batch_vec)).T.tolist()[0] + decrypted_batch = "".join( + self.replace_digits(num) for num in batch_decrypted + ) + decrypted += decrypted_batch + + return decrypted + + +def main() -> None: + n = int(input("Enter the order of the encryption key: ")) + hill_matrix = [] + + print("Enter each row of the encryption key with space separated integers") + for _ in range(n): + row = [int(x) for x in input().split()] + hill_matrix.append(row) + + hc = HillCipher(np.array(hill_matrix)) + + print("Would you like to encrypt or decrypt some text? (1 or 2)") + option = input("\n1. Encrypt\n2. Decrypt\n") + match option: + case "1": + text_e = input("What text would you like to encrypt?: ") + print("Your encrypted text is:") + print(hc.encrypt(text_e)) + case "2": + text_d = input("What text would you like to decrypt?: ") + print("Your decrypted text is:") + print(hc.decrypt(text_d)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + main() diff --git a/ciphers/mixed_keyword_cypher.py b/ciphers/mixed_keyword_cypher.py new file mode 100644 index 000000000000..1b186108a73e --- /dev/null +++ b/ciphers/mixed_keyword_cypher.py @@ -0,0 +1,75 @@ +from string import ascii_uppercase + + +def mixed_keyword( + keyword: str, plaintext: str, verbose: bool = False, alphabet: str = ascii_uppercase +) -> str: + """ + For keyword: hello + + H E L O + A B C D + F G I J + K M N P + Q R S T + U V W X + Y Z + and map vertically + + >>> mixed_keyword("college", "UNIVERSITY", True) # doctest: +NORMALIZE_WHITESPACE + {'A': 'C', 'B': 'A', 'C': 'I', 'D': 'P', 'E': 'U', 'F': 'Z', 'G': 'O', 'H': 'B', + 'I': 'J', 'J': 'Q', 'K': 'V', 'L': 'L', 'M': 'D', 'N': 'K', 'O': 'R', 'P': 'W', + 'Q': 'E', 'R': 'F', 'S': 'M', 'T': 'S', 'U': 'X', 'V': 'G', 'W': 'H', 'X': 'N', + 'Y': 'T', 'Z': 'Y'} + 'XKJGUFMJST' + + >>> mixed_keyword("college", "UNIVERSITY", False) # doctest: +NORMALIZE_WHITESPACE + 'XKJGUFMJST' + """ + keyword = keyword.upper() + plaintext = plaintext.upper() + alphabet_set = set(alphabet) + + # create a list of unique characters in the keyword - their order matters + # it determines how we will map plaintext characters to the ciphertext + unique_chars = [] + for char in keyword: + if char in alphabet_set and char not in unique_chars: + unique_chars.append(char) + # the number of those unique characters will determine the number of rows + num_unique_chars_in_keyword = len(unique_chars) + + # create a shifted version of the alphabet + shifted_alphabet = unique_chars + [ + char for char in alphabet if char not in unique_chars + ] + + # create a modified alphabet by splitting the shifted alphabet into rows + modified_alphabet = [ + shifted_alphabet[k : k + num_unique_chars_in_keyword] + for k in range(0, 26, num_unique_chars_in_keyword) + ] + + # map the alphabet characters to the modified alphabet characters + # going 'vertically' through the modified alphabet - consider columns first + mapping = {} + letter_index = 0 + for column in range(num_unique_chars_in_keyword): + for row in modified_alphabet: + # if current row (the last one) is too short, break out of loop + if len(row) <= column: + break + + # map current letter to letter in modified alphabet + mapping[alphabet[letter_index]] = row[column] + letter_index += 1 + + if verbose: + print(mapping) + # create the encrypted text by mapping the plaintext to the modified alphabet + return "".join(mapping.get(char, char) for char in plaintext) + + +if __name__ == "__main__": + # example use + print(mixed_keyword("college", "UNIVERSITY")) diff --git a/ciphers/mono_alphabetic_ciphers.py b/ciphers/mono_alphabetic_ciphers.py new file mode 100644 index 000000000000..f9594ca8af5f --- /dev/null +++ b/ciphers/mono_alphabetic_ciphers.py @@ -0,0 +1,64 @@ +from typing import Literal + +LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + +def translate_message( + key: str, message: str, mode: Literal["encrypt", "decrypt"] +) -> str: + """ + >>> translate_message("QWERTYUIOPASDFGHJKLZXCVBNM","Hello World","encrypt") + 'Pcssi Bidsm' + """ + chars_a = LETTERS if mode == "decrypt" else key + chars_b = key if mode == "decrypt" else LETTERS + translated = "" + # loop through each symbol in the message + for symbol in message: + if symbol.upper() in chars_a: + # encrypt/decrypt the symbol + sym_index = chars_a.find(symbol.upper()) + if symbol.isupper(): + translated += chars_b[sym_index].upper() + else: + translated += chars_b[sym_index].lower() + else: + # symbol is not in LETTERS, just add it + translated += symbol + return translated + + +def encrypt_message(key: str, message: str) -> str: + """ + >>> encrypt_message("QWERTYUIOPASDFGHJKLZXCVBNM", "Hello World") + 'Pcssi Bidsm' + """ + return translate_message(key, message, "encrypt") + + +def decrypt_message(key: str, message: str) -> str: + """ + >>> decrypt_message("QWERTYUIOPASDFGHJKLZXCVBNM", "Hello World") + 'Itssg Vgksr' + """ + return translate_message(key, message, "decrypt") + + +def main() -> None: + message = "Hello World" + key = "QWERTYUIOPASDFGHJKLZXCVBNM" + mode = "decrypt" # set to 'encrypt' or 'decrypt' + + match mode: + case "encrypt": + translated = encrypt_message(key, message) + case "decrypt": + translated = decrypt_message(key, message) + print(f"Using the key {key}, the {mode}ed message is: {translated}") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/ciphers/morse_code.py b/ciphers/morse_code.py new file mode 100644 index 000000000000..0370c26fe4a6 --- /dev/null +++ b/ciphers/morse_code.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +""" +Python program to translate to and from Morse code. + +https://en.wikipedia.org/wiki/Morse_code +""" + +# fmt: off +MORSE_CODE_DICT = { + "A": ".-", "B": "-...", "C": "-.-.", "D": "-..", "E": ".", "F": "..-.", "G": "--.", + "H": "....", "I": "..", "J": ".---", "K": "-.-", "L": ".-..", "M": "--", "N": "-.", + "O": "---", "P": ".--.", "Q": "--.-", "R": ".-.", "S": "...", "T": "-", "U": "..-", + "V": "...-", "W": ".--", "X": "-..-", "Y": "-.--", "Z": "--..", "1": ".----", + "2": "..---", "3": "...--", "4": "....-", "5": ".....", "6": "-....", "7": "--...", + "8": "---..", "9": "----.", "0": "-----", "&": ".-...", "@": ".--.-.", + ":": "---...", ",": "--..--", ".": ".-.-.-", "'": ".----.", '"': ".-..-.", + "?": "..--..", "/": "-..-.", "=": "-...-", "+": ".-.-.", "-": "-....-", + "(": "-.--.", ")": "-.--.-", "!": "-.-.--", " ": "/" +} # Exclamation mark is not in ITU-R recommendation +# fmt: on +REVERSE_DICT = {value: key for key, value in MORSE_CODE_DICT.items()} + + +def encrypt(message: str) -> str: + """ + >>> encrypt("Sos!") + '... --- ... -.-.--' + >>> encrypt("SOS!") == encrypt("sos!") + True + """ + return " ".join(MORSE_CODE_DICT[char] for char in message.upper()) + + +def decrypt(message: str) -> str: + """ + >>> decrypt('... --- ... -.-.--') + 'SOS!' + """ + return "".join(REVERSE_DICT[char] for char in message.split()) + + +def main() -> None: + """ + >>> s = "".join(MORSE_CODE_DICT) + >>> decrypt(encrypt(s)) == s + True + """ + message = "Morse code here!" + print(message) + message = encrypt(message) + print(message) + message = decrypt(message) + print(message) + + +if __name__ == "__main__": + main() diff --git a/ciphers/onepad_cipher.py b/ciphers/onepad_cipher.py new file mode 100644 index 000000000000..c4fb22e14a06 --- /dev/null +++ b/ciphers/onepad_cipher.py @@ -0,0 +1,64 @@ +import random + + +class Onepad: + @staticmethod + def encrypt(text: str) -> tuple[list[int], list[int]]: + """ + Function to encrypt text using pseudo-random numbers + >>> Onepad().encrypt("") + ([], []) + >>> Onepad().encrypt([]) + ([], []) + >>> random.seed(1) + >>> Onepad().encrypt(" ") + ([6969], [69]) + >>> random.seed(1) + >>> Onepad().encrypt("Hello") + ([9729, 114756, 4653, 31309, 10492], [69, 292, 33, 131, 61]) + >>> Onepad().encrypt(1) + Traceback (most recent call last): + ... + TypeError: 'int' object is not iterable + >>> Onepad().encrypt(1.1) + Traceback (most recent call last): + ... + TypeError: 'float' object is not iterable + """ + plain = [ord(i) for i in text] + key = [] + cipher = [] + for i in plain: + k = random.randint(1, 300) + c = (i + k) * k + cipher.append(c) + key.append(k) + return cipher, key + + @staticmethod + def decrypt(cipher: list[int], key: list[int]) -> str: + """ + Function to decrypt text using pseudo-random numbers. + >>> Onepad().decrypt([], []) + '' + >>> Onepad().decrypt([35], []) + '' + >>> Onepad().decrypt([], [35]) + Traceback (most recent call last): + ... + IndexError: list index out of range + >>> random.seed(1) + >>> Onepad().decrypt([9729, 114756, 4653, 31309, 10492], [69, 292, 33, 131, 61]) + 'Hello' + """ + plain = [] + for i in range(len(key)): + p = int((cipher[i] - (key[i]) ** 2) / key[i]) + plain.append(chr(p)) + return "".join(plain) + + +if __name__ == "__main__": + c, k = Onepad().encrypt("Hello") + print(c, k) + print(Onepad().decrypt(c, k)) diff --git a/ciphers/permutation_cipher.py b/ciphers/permutation_cipher.py new file mode 100644 index 000000000000..9e1c64a7b4ea --- /dev/null +++ b/ciphers/permutation_cipher.py @@ -0,0 +1,143 @@ +""" +The permutation cipher, also called the transposition cipher, is a simple encryption +technique that rearranges the characters in a message based on a secret key. It +divides the message into blocks and applies a permutation to the characters within +each block according to the key. The key is a sequence of unique integers that +determine the order of character rearrangement. + +For more info: https://www.nku.edu/~christensen/1402%20permutation%20ciphers.pdf +""" + +import random + + +def generate_valid_block_size(message_length: int) -> int: + """ + Generate a valid block size that is a factor of the message length. + + Args: + message_length (int): The length of the message. + + Returns: + int: A valid block size. + + Example: + >>> random.seed(1) + >>> generate_valid_block_size(12) + 3 + """ + block_sizes = [ + block_size + for block_size in range(2, message_length + 1) + if message_length % block_size == 0 + ] + return random.choice(block_sizes) + + +def generate_permutation_key(block_size: int) -> list[int]: + """ + Generate a random permutation key of a specified block size. + + Args: + block_size (int): The size of each permutation block. + + Returns: + list[int]: A list containing a random permutation of digits. + + Example: + >>> random.seed(0) + >>> generate_permutation_key(4) + [2, 0, 1, 3] + """ + digits = list(range(block_size)) + random.shuffle(digits) + return digits + + +def encrypt( + message: str, key: list[int] | None = None, block_size: int | None = None +) -> tuple[str, list[int]]: + """ + Encrypt a message using a permutation cipher with block rearrangement using a key. + + Args: + message (str): The plaintext message to be encrypted. + key (list[int]): The permutation key for decryption. + block_size (int): The size of each permutation block. + + Returns: + tuple: A tuple containing the encrypted message and the encryption key. + + Example: + >>> encrypted_message, key = encrypt("HELLO WORLD") + >>> decrypted_message = decrypt(encrypted_message, key) + >>> decrypted_message + 'HELLO WORLD' + """ + message = message.upper() + message_length = len(message) + + if key is None or block_size is None: + block_size = generate_valid_block_size(message_length) + key = generate_permutation_key(block_size) + + encrypted_message = "" + + for i in range(0, message_length, block_size): + block = message[i : i + block_size] + rearranged_block = [block[digit] for digit in key] + encrypted_message += "".join(rearranged_block) + + return encrypted_message, key + + +def decrypt(encrypted_message: str, key: list[int]) -> str: + """ + Decrypt an encrypted message using a permutation cipher with block rearrangement. + + Args: + encrypted_message (str): The encrypted message. + key (list[int]): The permutation key for decryption. + + Returns: + str: The decrypted plaintext message. + + Example: + >>> encrypted_message, key = encrypt("HELLO WORLD") + >>> decrypted_message = decrypt(encrypted_message, key) + >>> decrypted_message + 'HELLO WORLD' + """ + key_length = len(key) + decrypted_message = "" + + for i in range(0, len(encrypted_message), key_length): + block = encrypted_message[i : i + key_length] + original_block = [""] * key_length + for j, digit in enumerate(key): + original_block[digit] = block[j] + decrypted_message += "".join(original_block) + + return decrypted_message + + +def main() -> None: + """ + Driver function to pass message to get encrypted, then decrypted. + + Example: + >>> main() + Decrypted message: HELLO WORLD + """ + message = "HELLO WORLD" + encrypted_message, key = encrypt(message) + + decrypted_message = decrypt(encrypted_message, key) + print(f"Decrypted message: {decrypted_message}") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/ciphers/playfair_cipher.py b/ciphers/playfair_cipher.py index da547a337a94..d48f113f02e0 100644 --- a/ciphers/playfair_cipher.py +++ b/ciphers/playfair_cipher.py @@ -1,47 +1,69 @@ -import string +""" +https://en.wikipedia.org/wiki/Playfair_cipher#Description + +The Playfair cipher was developed by Charles Wheatstone in 1854 +It's use was heavily promotedby Lord Playfair, hence its name + +Some features of the Playfair cipher are: + +1) It was the first literal diagram substitution cipher +2) It is a manual symmetric encryption technique +3) It is a multiple letter encryption cipher + +The implementation in the code below encodes alphabets only. +It removes spaces, special characters and numbers from the +code. + +Playfair is no longer used by military forces because of known +insecurities and of the advent of automated encryption devices. +This cipher is regarded as insecure since before World War I. +""" + import itertools +import string +from collections.abc import Generator, Iterable + -def chunker(seq, size): +def chunker(seq: Iterable[str], size: int) -> Generator[tuple[str, ...]]: it = iter(seq) while True: - chunk = tuple(itertools.islice(it, size)) - if not chunk: - return - yield chunk - + chunk = tuple(itertools.islice(it, size)) + if not chunk: + return + yield chunk -def prepare_input(dirty): +def prepare_input(dirty: str) -> str: """ - Prepare the plaintext by uppcasing it - and seperating repeated letters with X's + Prepare the plaintext by up-casing it + and separating repeated letters with X's """ - - dirty = ''.join([c.upper() for c in dirty if c in string.ascii_letters]) + + dirty = "".join([c.upper() for c in dirty if c in string.ascii_letters]) clean = "" - + if len(dirty) < 2: return dirty - for i in range(len(dirty)-1): + for i in range(len(dirty) - 1): clean += dirty[i] - - if dirty[i] == dirty[i+1]: - clean += 'X' - + + if dirty[i] == dirty[i + 1]: + clean += "X" + clean += dirty[-1] if len(clean) & 1: - clean += 'X' + clean += "X" return clean -def generate_table(key): - # I and J are used interchangably to allow +def generate_table(key: str) -> list[str]: + # I and J are used interchangeably to allow # us to use a 5x5 table (25 letters) alphabet = "ABCDEFGHIKLMNOPQRSTUVWXYZ" - # we're using a list instead of a '2d' array because it makes the math + # we're using a list instead of a '2d' array because it makes the math # for setting up the table and doing the actual encoding/decoding simpler table = [] @@ -57,46 +79,81 @@ def generate_table(key): return table -def encode(plaintext, key): + +def encode(plaintext: str, key: str) -> str: + """ + Encode the given plaintext using the Playfair cipher. + Takes the plaintext and the key as input and returns the encoded string. + + >>> encode("Hello", "MONARCHY") + 'CFSUPM' + >>> encode("attack on the left flank", "EMERGENCY") + 'DQZSBYFSDZFMFNLOHFDRSG' + >>> encode("Sorry!", "SPECIAL") + 'AVXETX' + >>> encode("Number 1", "NUMBER") + 'UMBENF' + >>> encode("Photosynthesis!", "THE SUN") + 'OEMHQHVCHESUKE' + """ + table = generate_table(key) plaintext = prepare_input(plaintext) ciphertext = "" - # https://en.wikipedia.org/wiki/Playfair_cipher#Description for char1, char2 in chunker(plaintext, 2): row1, col1 = divmod(table.index(char1), 5) row2, col2 = divmod(table.index(char2), 5) if row1 == row2: - ciphertext += table[row1*5+(col1+1)%5] - ciphertext += table[row2*5+(col2+1)%5] + ciphertext += table[row1 * 5 + (col1 + 1) % 5] + ciphertext += table[row2 * 5 + (col2 + 1) % 5] elif col1 == col2: - ciphertext += table[((row1+1)%5)*5+col1] - ciphertext += table[((row2+1)%5)*5+col2] - else: # rectangle - ciphertext += table[row1*5+col2] - ciphertext += table[row2*5+col1] + ciphertext += table[((row1 + 1) % 5) * 5 + col1] + ciphertext += table[((row2 + 1) % 5) * 5 + col2] + else: # rectangle + ciphertext += table[row1 * 5 + col2] + ciphertext += table[row2 * 5 + col1] return ciphertext -def decode(ciphertext, key): +def decode(ciphertext: str, key: str) -> str: + """ + Decode the input string using the provided key. + + >>> decode("BMZFAZRZDH", "HAZARD") + 'FIREHAZARD' + >>> decode("HNBWBPQT", "AUTOMOBILE") + 'DRIVINGX' + >>> decode("SLYSSAQS", "CASTLE") + 'ATXTACKX' + """ + table = generate_table(key) plaintext = "" - # https://en.wikipedia.org/wiki/Playfair_cipher#Description - for char1, char2 in chunk(ciphertext, 2): + for char1, char2 in chunker(ciphertext, 2): row1, col1 = divmod(table.index(char1), 5) row2, col2 = divmod(table.index(char2), 5) if row1 == row2: - plaintext += table[row1*5+(col1-1)%5] - plaintext += table[row2*5+(col2-1)%5] + plaintext += table[row1 * 5 + (col1 - 1) % 5] + plaintext += table[row2 * 5 + (col2 - 1) % 5] elif col1 == col2: - plaintext += table[((row1-1)%5)*5+col1] - plaintext += table[((row2-1)%5)*5+col2] - else: # rectangle - plaintext += table[row1*5+col2] - plaintext += table[row2*5+col1] + plaintext += table[((row1 - 1) % 5) * 5 + col1] + plaintext += table[((row2 - 1) % 5) * 5 + col2] + else: # rectangle + plaintext += table[row1 * 5 + col2] + plaintext += table[row2 * 5 + col1] return plaintext + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + print("Encoded:", encode("BYE AND THANKS", "GREETING")) + print("Decoded:", decode("CXRBANRLBALQ", "GREETING")) diff --git a/ciphers/polybius.py b/ciphers/polybius.py new file mode 100644 index 000000000000..d83badf4ac0a --- /dev/null +++ b/ciphers/polybius.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + +""" +A Polybius Square is a table that allows someone to translate letters into numbers. + +https://www.braingle.com/brainteasers/codes/polybius.php +""" + +import numpy as np + +SQUARE = [ + ["a", "b", "c", "d", "e"], + ["f", "g", "h", "i", "k"], + ["l", "m", "n", "o", "p"], + ["q", "r", "s", "t", "u"], + ["v", "w", "x", "y", "z"], +] + + +class PolybiusCipher: + def __init__(self) -> None: + self.SQUARE = np.array(SQUARE) + + def letter_to_numbers(self, letter: str) -> np.ndarray: + """ + Return the pair of numbers that represents the given letter in the + polybius square + >>> np.array_equal(PolybiusCipher().letter_to_numbers('a'), [1,1]) + True + + >>> np.array_equal(PolybiusCipher().letter_to_numbers('u'), [4,5]) + True + """ + index1, index2 = np.where(letter == self.SQUARE) + indexes = np.concatenate([index1 + 1, index2 + 1]) + return indexes + + def numbers_to_letter(self, index1: int, index2: int) -> str: + """ + Return the letter corresponding to the position [index1, index2] in + the polybius square + + >>> PolybiusCipher().numbers_to_letter(4, 5) == "u" + True + + >>> PolybiusCipher().numbers_to_letter(1, 1) == "a" + True + """ + return self.SQUARE[index1 - 1, index2 - 1] + + def encode(self, message: str) -> str: + """ + Return the encoded version of message according to the polybius cipher + + >>> PolybiusCipher().encode("test message") == "44154344 32154343112215" + True + + >>> PolybiusCipher().encode("Test Message") == "44154344 32154343112215" + True + """ + message = message.lower() + message = message.replace("j", "i") + + encoded_message = "" + for letter_index in range(len(message)): + if message[letter_index] != " ": + numbers = self.letter_to_numbers(message[letter_index]) + encoded_message = encoded_message + str(numbers[0]) + str(numbers[1]) + elif message[letter_index] == " ": + encoded_message = encoded_message + " " + + return encoded_message + + def decode(self, message: str) -> str: + """ + Return the decoded version of message according to the polybius cipher + + >>> PolybiusCipher().decode("44154344 32154343112215") == "test message" + True + + >>> PolybiusCipher().decode("4415434432154343112215") == "testmessage" + True + """ + message = message.replace(" ", " ") + decoded_message = "" + for numbers_index in range(int(len(message) / 2)): + if message[numbers_index * 2] != " ": + index1 = message[numbers_index * 2] + index2 = message[numbers_index * 2 + 1] + + letter = self.numbers_to_letter(int(index1), int(index2)) + decoded_message = decoded_message + letter + elif message[numbers_index * 2] == " ": + decoded_message = decoded_message + " " + + return decoded_message diff --git a/ciphers/porta_cipher.py b/ciphers/porta_cipher.py new file mode 100644 index 000000000000..498ae294041e --- /dev/null +++ b/ciphers/porta_cipher.py @@ -0,0 +1,103 @@ +alphabet = { + "A": ("ABCDEFGHIJKLM", "NOPQRSTUVWXYZ"), + "B": ("ABCDEFGHIJKLM", "NOPQRSTUVWXYZ"), + "C": ("ABCDEFGHIJKLM", "ZNOPQRSTUVWXY"), + "D": ("ABCDEFGHIJKLM", "ZNOPQRSTUVWXY"), + "E": ("ABCDEFGHIJKLM", "YZNOPQRSTUVWX"), + "F": ("ABCDEFGHIJKLM", "YZNOPQRSTUVWX"), + "G": ("ABCDEFGHIJKLM", "XYZNOPQRSTUVW"), + "H": ("ABCDEFGHIJKLM", "XYZNOPQRSTUVW"), + "I": ("ABCDEFGHIJKLM", "WXYZNOPQRSTUV"), + "J": ("ABCDEFGHIJKLM", "WXYZNOPQRSTUV"), + "K": ("ABCDEFGHIJKLM", "VWXYZNOPQRSTU"), + "L": ("ABCDEFGHIJKLM", "VWXYZNOPQRSTU"), + "M": ("ABCDEFGHIJKLM", "UVWXYZNOPQRST"), + "N": ("ABCDEFGHIJKLM", "UVWXYZNOPQRST"), + "O": ("ABCDEFGHIJKLM", "TUVWXYZNOPQRS"), + "P": ("ABCDEFGHIJKLM", "TUVWXYZNOPQRS"), + "Q": ("ABCDEFGHIJKLM", "STUVWXYZNOPQR"), + "R": ("ABCDEFGHIJKLM", "STUVWXYZNOPQR"), + "S": ("ABCDEFGHIJKLM", "RSTUVWXYZNOPQ"), + "T": ("ABCDEFGHIJKLM", "RSTUVWXYZNOPQ"), + "U": ("ABCDEFGHIJKLM", "QRSTUVWXYZNOP"), + "V": ("ABCDEFGHIJKLM", "QRSTUVWXYZNOP"), + "W": ("ABCDEFGHIJKLM", "PQRSTUVWXYZNO"), + "X": ("ABCDEFGHIJKLM", "PQRSTUVWXYZNO"), + "Y": ("ABCDEFGHIJKLM", "OPQRSTUVWXYZN"), + "Z": ("ABCDEFGHIJKLM", "OPQRSTUVWXYZN"), +} + + +def generate_table(key: str) -> list[tuple[str, str]]: + """ + >>> generate_table('marvin') # doctest: +NORMALIZE_WHITESPACE + [('ABCDEFGHIJKLM', 'UVWXYZNOPQRST'), ('ABCDEFGHIJKLM', 'NOPQRSTUVWXYZ'), + ('ABCDEFGHIJKLM', 'STUVWXYZNOPQR'), ('ABCDEFGHIJKLM', 'QRSTUVWXYZNOP'), + ('ABCDEFGHIJKLM', 'WXYZNOPQRSTUV'), ('ABCDEFGHIJKLM', 'UVWXYZNOPQRST')] + """ + return [alphabet[char] for char in key.upper()] + + +def encrypt(key: str, words: str) -> str: + """ + >>> encrypt('marvin', 'jessica') + 'QRACRWU' + """ + cipher = "" + count = 0 + table = generate_table(key) + for char in words.upper(): + cipher += get_opponent(table[count], char) + count = (count + 1) % len(table) + return cipher + + +def decrypt(key: str, words: str) -> str: + """ + >>> decrypt('marvin', 'QRACRWU') + 'JESSICA' + """ + return encrypt(key, words) + + +def get_position(table: tuple[str, str], char: str) -> tuple[int, int]: + """ + >>> get_position(generate_table('marvin')[0], 'M') + (0, 12) + """ + # `char` is either in the 0th row or the 1st row + row = 0 if char in table[0] else 1 + col = table[row].index(char) + return row, col + + +def get_opponent(table: tuple[str, str], char: str) -> str: + """ + >>> get_opponent(generate_table('marvin')[0], 'M') + 'T' + """ + row, col = get_position(table, char.upper()) + if row == 1: + return table[0][col] + else: + return table[1][col] if row == 0 else char + + +if __name__ == "__main__": + import doctest + + doctest.testmod() # Fist ensure that all our tests are passing... + """ + Demo: + + Enter key: marvin + Enter text to encrypt: jessica + Encrypted: QRACRWU + Decrypted with key: JESSICA + """ + key = input("Enter key: ").strip() + text = input("Enter text to encrypt: ").strip() + cipher_text = encrypt(key, text) + + print(f"Encrypted: {cipher_text}") + print(f"Decrypted with key: {decrypt(key, cipher_text)}") diff --git a/ciphers/Prehistoric Men.txt b/ciphers/prehistoric_men.txt similarity index 88% rename from ciphers/Prehistoric Men.txt rename to ciphers/prehistoric_men.txt index 86c4de821bfc..8d1b2bd8c8d1 100644 --- a/ciphers/Prehistoric Men.txt +++ b/ciphers/prehistoric_men.txt @@ -3,9 +3,9 @@ Braidwood, Illustrated by Susan T. Richert This eBook is for the use of anyone anywhere in the United States and most -other parts of the world at no cost and with almost no restrictions +other parts of the world at no cost and with almost no restrictions whatsoever. You may copy it, give it away or re-use it under the terms of -the Project Gutenberg License included with this eBook or online at +the Project Gutenberg License included with this eBook or online at www.gutenberg.org. If you are not located in the United States, you'll have to check the laws of the country where you are located before using this ebook. @@ -40,8 +40,8 @@ Transcriber's note: version referred to above. One example of this might occur in the second paragraph under "Choppers and Adze-like Tools", page 46, which contains the phrase - “an adze cutting edge is ? shaped”. The symbol before - “shaped” looks like a sharply-italicized sans-serif “L”. + �an adze cutting edge is ? shaped�. The symbol before + �shaped� looks like a sharply-italicized sans-serif �L�. Devices that cannot display that symbol may substitute a question mark, a square, or other symbol. @@ -98,7 +98,7 @@ forced or pedantic; at least I have done my very best to tell the story simply and clearly. Many friends have aided in the preparation of the book. The whimsical -charm of Miss Susan Richert’s illustrations add enormously to the +charm of Miss Susan Richert�s illustrations add enormously to the spirit I wanted. She gave freely of her own time on the drawings and in planning the book with me. My colleagues at the University of Chicago, especially Professor Wilton M. Krogman (now of the University @@ -108,7 +108,7 @@ the Department of Anthropology, gave me counsel in matters bearing on their special fields, and the Department of Anthropology bore some of the expense of the illustrations. From Mrs. Irma Hunter and Mr. Arnold Maremont, who are not archeologists at all and have only an intelligent -layman’s notion of archeology, I had sound advice on how best to tell +layman�s notion of archeology, I had sound advice on how best to tell the story. I am deeply indebted to all these friends. While I was preparing the second edition, I had the great fortune @@ -117,13 +117,13 @@ Washburn, now of the Department of Anthropology of the University of California, and the fourth, fifth, and sixth chapters with Professor Hallum L. Movius, Jr., of the Peabody Museum, Harvard University. The book has gained greatly in accuracy thereby. In matters of dating, -Professor Movius and the indications of Professor W. F. Libby’s Carbon +Professor Movius and the indications of Professor W. F. Libby�s Carbon 14 chronology project have both encouraged me to choose the lowest dates now current for the events of the Pleistocene Ice Age. There is still no certain way of fixing a direct chronology for most of the -Pleistocene, but Professor Libby’s method appears very promising for +Pleistocene, but Professor Libby�s method appears very promising for its end range and for proto-historic dates. In any case, this book -names “periods,” and new dates may be written in against mine, if new +names �periods,� and new dates may be written in against mine, if new and better dating systems appear. I wish to thank Dr. Clifford C. Gregg, Director of Chicago Natural @@ -150,7 +150,7 @@ Clark Howell of the Department of Anthropology of the University of Chicago in reworking the earlier chapters, and he was very patient in the matter, which I sincerely appreciate. -All of Mrs. Susan Richert Allen’s original drawings appear, but a few +All of Mrs. Susan Richert Allen�s original drawings appear, but a few necessary corrections have been made in some of the charts and some new drawings have been added by Mr. John Pfiffner, Staff Artist, Chicago Natural History Museum. @@ -200,7 +200,7 @@ HOW WE LEARN about Prehistoric Men Prehistory means the time before written history began. Actually, more -than 99 per cent of man’s story is prehistory. Man is at least half a +than 99 per cent of man�s story is prehistory. Man is at least half a million years old, but he did not begin to write history (or to write anything) until about 5,000 years ago. @@ -216,7 +216,7 @@ The scientists who study the bones and teeth and any other parts they find of the bodies of prehistoric men, are called _physical anthropologists_. Physical anthropologists are trained, much like doctors, to know all about the human body. They study living people, -too; they know more about the biological facts of human “races” than +too; they know more about the biological facts of human �races� than anybody else. If the police find a badly decayed body in a trunk, they ask a physical anthropologist to tell them what the person originally looked like. The physical anthropologists who specialize in @@ -228,14 +228,14 @@ ARCHEOLOGISTS There is a kind of scientist who studies the things that prehistoric men made and did. Such a scientist is called an _archeologist_. It is -the archeologist’s business to look for the stone and metal tools, the +the archeologist�s business to look for the stone and metal tools, the pottery, the graves, and the caves or huts of the men who lived before history began. But there is more to archeology than just looking for things. In -Professor V. Gordon Childe’s words, archeology “furnishes a sort of +Professor V. Gordon Childe�s words, archeology �furnishes a sort of history of human activity, provided always that the actions have -produced concrete results and left recognizable material traces.” You +produced concrete results and left recognizable material traces.� You will see that there are at least three points in what Childe says: 1. The archeologists have to find the traces of things left behind by @@ -245,7 +245,7 @@ will see that there are at least three points in what Childe says: too soft or too breakable to last through the years. However, 3. The archeologist must use whatever he can find to tell a story--to - make a “sort of history”--from the objects and living-places and + make a �sort of history�--from the objects and living-places and graves that have escaped destruction. What I mean is this: Let us say you are walking through a dump yard, @@ -253,8 +253,8 @@ and you find a rusty old spark plug. If you want to think about what the spark plug means, you quickly remember that it is a part of an automobile motor. This tells you something about the man who threw the spark plug on the dump. He either had an automobile, or he knew -or lived near someone who did. He can’t have lived so very long ago, -you’ll remember, because spark plugs and automobiles are only about +or lived near someone who did. He can�t have lived so very long ago, +you�ll remember, because spark plugs and automobiles are only about sixty years old. When you think about the old spark plug in this way you have @@ -264,8 +264,8 @@ It is the same way with the man-made things we archeologists find and put in museums. Usually, only a few of these objects are pretty to look at; but each of them has some sort of story to tell. Making the interpretation of his finds is the most important part of the -archeologist’s job. It is the way he gets at the “sort of history of -human activity” which is expected of archeology. +archeologist�s job. It is the way he gets at the �sort of history of +human activity� which is expected of archeology. SOME OTHER SCIENTISTS @@ -274,7 +274,7 @@ There are many other scientists who help the archeologist and the physical anthropologist find out about prehistoric men. The geologists help us tell the age of the rocks or caves or gravel beds in which human bones or man-made objects are found. There are other scientists -with names which all begin with “paleo” (the Greek word for “old”). The +with names which all begin with �paleo� (the Greek word for �old�). The _paleontologists_ study fossil animals. There are also, for example, such scientists as _paleobotanists_ and _paleoclimatologists_, who study ancient plants and climates. These scientists help us to know @@ -306,20 +306,20 @@ systems. The rate of disappearance of radioactivity as time passes.[1]] [1] It is important that the limitations of the radioactive carbon - “dating” system be held in mind. As the statistics involved in + �dating� system be held in mind. As the statistics involved in the system are used, there are two chances in three that the - “date” of the sample falls within the range given as plus or - minus an added number of years. For example, the “date” for the - Jarmo village (see chart), given as 6750 ± 200 B.C., really + �date� of the sample falls within the range given as plus or + minus an added number of years. For example, the �date� for the + Jarmo village (see chart), given as 6750 � 200 B.C., really means that there are only two chances in three that the real date of the charcoal sampled fell between 6950 and 6550 B.C. We have also begun to suspect that there are ways in which the - samples themselves may have become “contaminated,” either on + samples themselves may have become �contaminated,� either on the early or on the late side. We now tend to be suspicious of single radioactive carbon determinations, or of determinations from one site alone. But as a fabric of consistent determinations for several or more sites of one archeological - period, we gain confidence in the “dates.” + period, we gain confidence in the dates. HOW THE SCIENTISTS FIND OUT @@ -330,9 +330,9 @@ about prehistoric men. We also need a word about _how_ they find out. All our finds came by accident until about a hundred years ago. Men digging wells, or digging in caves for fertilizer, often turned up ancient swords or pots or stone arrowheads. People also found some odd -pieces of stone that didn’t look like natural forms, but they also -didn’t look like any known tool. As a result, the people who found them -gave them queer names; for example, “thunderbolts.” The people thought +pieces of stone that didn�t look like natural forms, but they also +didn�t look like any known tool. As a result, the people who found them +gave them queer names; for example, �thunderbolts.� The people thought the strange stones came to earth as bolts of lightning. We know now that these strange stones were prehistoric stone tools. @@ -349,7 +349,7 @@ story of cave men on Mount Carmel, in Palestine, began to be known. Planned archeological digging is only about a century old. Even before this, however, a few men realized the significance of objects they dug from the ground; one of these early archeologists was our own Thomas -Jefferson. The first real mound-digger was a German grocer’s clerk, +Jefferson. The first real mound-digger was a German grocer�s clerk, Heinrich Schliemann. Schliemann made a fortune as a merchant, first in Europe and then in the California gold-rush of 1849. He became an American citizen. Then he retired and had both money and time to test @@ -389,16 +389,16 @@ used had been a soft, unbaked mud-brick, and most of the debris consisted of fallen or rain-melted mud from these mud-bricks. This idea of _stratification_, like the cake layers, was already a -familiar one to the geologists by Schliemann’s time. They could show +familiar one to the geologists by Schliemann�s time. They could show that their lowest layer of rock was oldest or earliest, and that the -overlying layers became more recent as one moved upward. Schliemann’s +overlying layers became more recent as one moved upward. Schliemann�s digging proved the same thing at Troy. His first (lowest and earliest) city had at least nine layers above it; he thought that the second -layer contained the remains of Homer’s Troy. We now know that Homeric +layer contained the remains of Homer�s Troy. We now know that Homeric Troy was layer VIIa from the bottom; also, we count eleven layers or sub-layers in total. -Schliemann’s work marks the beginnings of modern archeology. Scholars +Schliemann�s work marks the beginnings of modern archeology. Scholars soon set out to dig on ancient sites, from Egypt to Central America. @@ -410,21 +410,21 @@ Archeologists began to get ideas as to the kinds of objects that belonged together. If you compared a mail-order catalogue of 1890 with one of today, you would see a lot of differences. If you really studied the two catalogues hard, you would also begin to see that certain -objects “go together.” Horseshoes and metal buggy tires and pieces of +objects �go together.� Horseshoes and metal buggy tires and pieces of harness would begin to fit into a picture with certain kinds of coal stoves and furniture and china dishes and kerosene lamps. Our friend the spark plug, and radios and electric refrigerators and light bulbs would fit into a picture with different kinds of furniture and dishes -and tools. You won’t be old enough to remember the kind of hats that -women wore in 1890, but you’ve probably seen pictures of them, and you -know very well they couldn’t be worn with the fashions of today. +and tools. You won�t be old enough to remember the kind of hats that +women wore in 1890, but you�ve probably seen pictures of them, and you +know very well they couldn�t be worn with the fashions of today. This is one of the ways that archeologists study their materials. The various tools and weapons and jewelry, the pottery, the kinds of houses, and even the ways of burying the dead tend to fit into pictures. Some archeologists call all of the things that go together to make such a picture an _assemblage_. The assemblage of the first layer -of Schliemann’s Troy was as different from that of the seventh layer as +of Schliemann�s Troy was as different from that of the seventh layer as our 1900 mail-order catalogue is from the one of today. The archeologists who came after Schliemann began to notice other @@ -433,23 +433,23 @@ idea that people will buy better mousetraps goes back into very ancient times. Today, if we make good automobiles or radios, we can sell some of them in Turkey or even in Timbuktu. This means that a few present-day types of American automobiles and radios form part -of present-day “assemblages” in both Turkey and Timbuktu. The total -present-day “assemblage” of Turkey is quite different from that of +of present-day �assemblages� in both Turkey and Timbuktu. The total +present-day �assemblage� of Turkey is quite different from that of Timbuktu or that of America, but they have at least some automobiles and some radios in common. Now these automobiles and radios will eventually wear out. Let us suppose we could go to some remote part of Turkey or to Timbuktu in a -dream. We don’t know what the date is, in our dream, but we see all +dream. We don�t know what the date is, in our dream, but we see all sorts of strange things and ways of living in both places. Nobody tells us what the date is. But suddenly we see a 1936 Ford; so we know that in our dream it has to be at least the year 1936, and only as many years after that as we could reasonably expect a Ford to keep -in running order. The Ford would probably break down in twenty years’ -time, so the Turkish or Timbuktu “assemblage” we’re seeing in our dream +in running order. The Ford would probably break down in twenty years� +time, so the Turkish or Timbuktu �assemblage� we�re seeing in our dream has to date at about A.D. 1936-56. -Archeologists not only “date” their ancient materials in this way; they +Archeologists not only �date� their ancient materials in this way; they also see over what distances and between which peoples trading was done. It turns out that there was a good deal of trading in ancient times, probably all on a barter and exchange basis. @@ -480,13 +480,13 @@ site. They find the remains of everything that would last through time, in several different layers. They know that the assemblage in the bottom layer was laid down earlier than the assemblage in the next layer above, and so on up to the topmost layer, which is the latest. -They look at the results of other “digs” and find that some other +They look at the results of other �digs� and find that some other archeologist 900 miles away has found ax-heads in his lowest layer, exactly like the ax-heads of their fifth layer. This means that their fifth layer must have been lived in at about the same time as was the first layer in the site 200 miles away. It also may mean that the people who lived in the two layers knew and traded with each other. Or -it could mean that they didn’t necessarily know each other, but simply +it could mean that they didn�t necessarily know each other, but simply that both traded with a third group at about the same time. You can see that the more we dig and find, the more clearly the main @@ -501,8 +501,8 @@ those of domesticated animals, for instance, sheep or cattle, and therefore the people must have kept herds. More important than anything else--as our structure grows more -complicated and our materials increase--is the fact that “a sort -of history of human activity” does begin to appear. The habits or +complicated and our materials increase--is the fact that �a sort +of history of human activity� does begin to appear. The habits or traditions that men formed in the making of their tools and in the ways they did things, begin to stand out for us. How characteristic were these habits and traditions? What areas did they spread over? @@ -519,7 +519,7 @@ method--chemical tests of the bones--that will enable them to discover what the blood-type may have been. One thing is sure. We have never found a group of skeletons so absolutely similar among themselves--so cast from a single mould, so to speak--that we could claim to have a -“pure” race. I am sure we never shall. +�pure� race. I am sure we never shall. We become particularly interested in any signs of change--when new materials and tool types and ways of doing things replace old ones. We @@ -527,7 +527,7 @@ watch for signs of social change and progress in one way or another. We must do all this without one word of written history to aid us. Everything we are concerned with goes back to the time _before_ men -learned to write. That is the prehistorian’s job--to find out what +learned to write. That is the prehistorian�s job--to find out what happened before history began. @@ -538,9 +538,9 @@ THE CHANGING WORLD in which Prehistoric Men Lived [Illustration] -Mankind, we’ll say, is at least a half million years old. It is very +Mankind, we�ll say, is at least a half million years old. It is very hard to understand how long a time half a million years really is. -If we were to compare this whole length of time to one day, we’d get +If we were to compare this whole length of time to one day, we�d get something like this: The present time is midnight, and Jesus was born just five minutes and thirty-six seconds ago. Earliest history began less than fifteen minutes ago. Everything before 11:45 was in @@ -569,7 +569,7 @@ book; it would mainly affect the dates earlier than 25,000 years ago. CHANGES IN ENVIRONMENT -The earth probably hasn’t changed much in the last 5,000 years (250 +The earth probably hasn�t changed much in the last 5,000 years (250 generations). Men have built things on its surface and dug into it and drawn boundaries on maps of it, but the places where rivers, lakes, seas, and mountains now stand have changed very little. @@ -605,7 +605,7 @@ the glaciers covered most of Canada and the northern United States and reached down to southern England and France in Europe. Smaller ice sheets sat like caps on the Rockies, the Alps, and the Himalayas. The continental glaciation only happened north of the equator, however, so -remember that “Ice Age” is only half true. +remember that �Ice Age� is only half true. As you know, the amount of water on and about the earth does not vary. These large glaciers contained millions of tons of water frozen into @@ -677,9 +677,9 @@ their dead. At about the time when the last great glacier was finally melting away, men in the Near East made the first basic change in human economy. They began to plant grain, and they learned to raise and herd certain -animals. This meant that they could store food in granaries and “on the -hoof” against the bad times of the year. This first really basic change -in man’s way of living has been called the “food-producing revolution.” +animals. This meant that they could store food in granaries and �on the +hoof� against the bad times of the year. This first really basic change +in man�s way of living has been called the �food-producing revolution.� By the time it happened, a modern kind of climate was beginning. Men had already grown to look as they do now. Know-how in ways of living had developed and progressed, slowly but surely, up to a point. It was @@ -698,25 +698,25 @@ Prehistoric Men THEMSELVES DO WE KNOW WHERE MAN ORIGINATED? -For a long time some scientists thought the “cradle of mankind” was in +For a long time some scientists thought the �cradle of mankind� was in central Asia. Other scientists insisted it was in Africa, and still -others said it might have been in Europe. Actually, we don’t know -where it was. We don’t even know that there was only _one_ “cradle.” -If we had to choose a “cradle” at this moment, we would probably say +others said it might have been in Europe. Actually, we don�t know +where it was. We don�t even know that there was only _one_ �cradle.� +If we had to choose a �cradle� at this moment, we would probably say Africa. But the southern portions of Asia and Europe may also have been included in the general area. The scene of the early development of -mankind was certainly the Old World. It is pretty certain men didn’t +mankind was certainly the Old World. It is pretty certain men didn�t reach North or South America until almost the end of the Ice Age--had they done so earlier we would certainly have found some trace of them by now. The earliest tools we have yet found come from central and south -Africa. By the dating system I’m using, these tools must be over +Africa. By the dating system I�m using, these tools must be over 500,000 years old. There are now reports that a few such early tools have been found--at the Sterkfontein cave in South Africa--along with -the bones of small fossil men called “australopithecines.” +the bones of small fossil men called �australopithecines.� -Not all scientists would agree that the australopithecines were “men,” +Not all scientists would agree that the australopithecines were �men,� or would agree that the tools were made by the australopithecines themselves. For these sticklers, the earliest bones of men come from the island of Java. The date would be about 450,000 years ago. So far, @@ -727,12 +727,12 @@ Let me say it another way. How old are the earliest traces of men we now have? Over half a million years. This was a time when the first alpine glaciation was happening in the north. What has been found so far? The tools which the men of those times made, in different parts -of Africa. It is now fairly generally agreed that the “men” who made -the tools were the australopithecines. There is also a more “man-like” +of Africa. It is now fairly generally agreed that the �men� who made +the tools were the australopithecines. There is also a more �man-like� jawbone at Kanam in Kenya, but its find-spot has been questioned. The next earliest bones we have were found in Java, and they may be almost a hundred thousand years younger than the earliest African finds. We -haven’t yet found the tools of these early Javanese. Our knowledge of +haven�t yet found the tools of these early Javanese. Our knowledge of tool-using in Africa spreads quickly as time goes on: soon after the appearance of tools in the south we shall have them from as far north as Algeria. @@ -758,30 +758,30 @@ prove it. MEN AND APES Many people used to get extremely upset at the ill-formed notion -that “man descended from the apes.” Such words were much more likely -to start fights or “monkey trials” than the correct notion that all +that �man descended from the apes.� Such words were much more likely +to start fights or �monkey trials� than the correct notion that all living animals, including man, ascended or evolved from a single-celled organism which lived in the primeval seas hundreds of millions of years -ago. Men are mammals, of the order called Primates, and man’s living -relatives are the great apes. Men didn’t “descend” from the apes or +ago. Men are mammals, of the order called Primates, and man�s living +relatives are the great apes. Men didn�t �descend� from the apes or apes from men, and mankind must have had much closer relatives who have since become extinct. Men stand erect. They also walk and run on their two feet. Apes are happiest in trees, swinging with their arms from branch to branch. Few branches of trees will hold the mighty gorilla, although he still -manages to sleep in trees. Apes can’t stand really erect in our sense, +manages to sleep in trees. Apes can�t stand really erect in our sense, and when they have to run on the ground, they use the knuckles of their hands as well as their feet. A key group of fossil bones here are the south African australopithecines. These are called the _Australopithecinae_ or -“man-apes” or sometimes even “ape-men.” We do not _know_ that they were +�man-apes� or sometimes even �ape-men.� We do not _know_ that they were directly ancestral to men but they can hardly have been so to apes. -Presently I’ll describe them a bit more. The reason I mention them +Presently I�ll describe them a bit more. The reason I mention them here is that while they had brains no larger than those of apes, their hipbones were enough like ours so that they must have stood erect. -There is no good reason to think they couldn’t have walked as we do. +There is no good reason to think they couldn�t have walked as we do. BRAINS, HANDS, AND TOOLS @@ -801,12 +801,12 @@ Nobody knows which of these three is most important, or which came first. Most probably the growth of all three things was very much blended together. If you think about each of the things, you will see what I mean. Unless your hand is more flexible than a paw, and your -thumb will work against (or oppose) your fingers, you can’t hold a tool -very well. But you wouldn’t get the idea of using a tool unless you had +thumb will work against (or oppose) your fingers, you can�t hold a tool +very well. But you wouldn�t get the idea of using a tool unless you had enough brain to help you see cause and effect. And it is rather hard to see how your hand and brain would develop unless they had something to -practice on--like using tools. In Professor Krogman’s words, “the hand -must become the obedient servant of the eye and the brain.” It is the +practice on--like using tools. In Professor Krogman�s words, �the hand +must become the obedient servant of the eye and the brain.� It is the _co-ordination_ of these things that counts. Many other things must have been happening to the bodies of the @@ -820,17 +820,17 @@ little by little, all together. Men became men very slowly. WHEN SHALL WE CALL MEN MEN? -What do I mean when I say “men”? People who looked pretty much as we +What do I mean when I say �men�? People who looked pretty much as we do, and who used different tools to do different things, are men to me. -We’ll probably never know whether the earliest ones talked or not. They +We�ll probably never know whether the earliest ones talked or not. They probably had vocal cords, so they could make sounds, but did they know how to make sounds work as symbols to carry meanings? But if the fossil -bones look like our skeletons, and if we find tools which we’ll agree -couldn’t have been made by nature or by animals, then I’d say we had +bones look like our skeletons, and if we find tools which we�ll agree +couldn�t have been made by nature or by animals, then I�d say we had traces of _men_. The australopithecine finds of the Transvaal and Bechuanaland, in -south Africa, are bound to come into the discussion here. I’ve already +south Africa, are bound to come into the discussion here. I�ve already told you that the australopithecines could have stood upright and walked on their two hind legs. They come from the very base of the Pleistocene or Ice Age, and a few coarse stone tools have been found @@ -848,17 +848,17 @@ bones. The doubt as to whether the australopithecines used the tools themselves goes like this--just suppose some man-like creature (whose bones we have not yet found) made the tools and used them to kill and butcher australopithecines. Hence a few experts tend to let -australopithecines still hang in limbo as “man-apes.” +australopithecines still hang in limbo as �man-apes.� THE EARLIEST MEN WE KNOW -I’ll postpone talking about the tools of early men until the next +I�ll postpone talking about the tools of early men until the next chapter. The men whose bones were the earliest of the Java lot have been given the name _Meganthropus_. The bones are very fragmentary. We would not understand them very well unless we had the somewhat later -Javanese lot--the more commonly known _Pithecanthropus_ or “Java -man”--against which to refer them for study. One of the less well-known +Javanese lot--the more commonly known _Pithecanthropus_ or �Java +man�--against which to refer them for study. One of the less well-known and earliest fragments, a piece of lower jaw and some teeth, rather strongly resembles the lower jaws and teeth of the australopithecine type. Was _Meganthropus_ a sort of half-way point between the @@ -872,7 +872,7 @@ finds of Java man were made in 1891-92 by Dr. Eugene Dubois, a Dutch doctor in the colonial service. Finds have continued to be made. There are now bones enough to account for four skulls. There are also four jaws and some odd teeth and thigh bones. Java man, generally speaking, -was about five feet six inches tall, and didn’t hold his head very +was about five feet six inches tall, and didn�t hold his head very erect. His skull was very thick and heavy and had room for little more than two-thirds as large a brain as we have. He had big teeth and a big jaw and enormous eyebrow ridges. @@ -885,22 +885,22 @@ belonged to his near descendants. Remember that there are several varieties of men in the whole early Java lot, at least two of which are earlier than the _Pithecanthropus_, -“Java man.” Some of the earlier ones seem to have gone in for +�Java man.� Some of the earlier ones seem to have gone in for bigness, in tooth-size at least. _Meganthropus_ is one of these earlier varieties. As we said, he _may_ turn out to be a link to the australopithecines, who _may_ or _may not_ be ancestral to men. _Meganthropus_ is best understandable in terms of _Pithecanthropus_, who appeared later in the same general area. _Pithecanthropus_ is pretty well understandable from the bones he left us, and also because -of his strong resemblance to the fully tool-using cave-dwelling “Peking -man,” _Sinanthropus_, about whom we shall talk next. But you can see +of his strong resemblance to the fully tool-using cave-dwelling �Peking +man,� _Sinanthropus_, about whom we shall talk next. But you can see that the physical anthropologists and prehistoric archeologists still have a lot of work to do on the problem of earliest men. PEKING MEN AND SOME EARLY WESTERNERS -The earliest known Chinese are called _Sinanthropus_, or “Peking man,” +The earliest known Chinese are called _Sinanthropus_, or �Peking man,� because the finds were made near that city. In World War II, the United States Marine guard at our Embassy in Peking tried to help get the bones out of the city before the Japanese attack. Nobody knows where @@ -913,9 +913,9 @@ casts of the bones. Peking man lived in a cave in a limestone hill, made tools, cracked animal bones to get the marrow out, and used fire. Incidentally, the bones of Peking man were found because Chinese dig for what they call -“dragon bones” and “dragon teeth.” Uneducated Chinese buy these things +�dragon bones� and �dragon teeth.� Uneducated Chinese buy these things in their drug stores and grind them into powder for medicine. The -“dragon teeth” and “bones” are really fossils of ancient animals, and +�dragon teeth� and �bones� are really fossils of ancient animals, and sometimes of men. The people who supply the drug stores have learned where to dig for strange bones and teeth. Paleontologists who get to China go to the drug stores to buy fossils. In a roundabout way, this @@ -924,7 +924,7 @@ is how the fallen-in cave of Peking man at Choukoutien was discovered. Peking man was not quite as tall as Java man but he probably stood straighter. His skull looked very much like that of the Java skull except that it had room for a slightly larger brain. His face was less -brutish than was Java man’s face, but this isn’t saying much. +brutish than was Java man�s face, but this isn�t saying much. Peking man dates from early in the interglacial period following the second alpine glaciation. He probably lived close to 350,000 years @@ -946,9 +946,9 @@ big ridges over the eyes. The more fragmentary skull from Swanscombe in England (p. 11) has been much more carefully studied. Only the top and back of that skull have been found. Since the skull rounds up nicely, it has been assumed that the face and forehead must have been quite -“modern.” Careful comparison with Steinheim shows that this was not +�modern.� Careful comparison with Steinheim shows that this was not necessarily so. This is important because it bears on the question of -how early truly “modern” man appeared. +how early truly �modern� man appeared. Recently two fragmentary jaws were found at Ternafine in Algeria, northwest Africa. They look like the jaws of Peking man. Tools were @@ -971,22 +971,22 @@ modern Australian natives. During parts of the Ice Age there was a land bridge all the way from Java to Australia. -TWO ENGLISHMEN WHO WEREN’T OLD +TWO ENGLISHMEN WHO WEREN�T OLD The older textbooks contain descriptions of two English finds which were thought to be very old. These were called Piltdown (_Eoanthropus dawsoni_) and Galley Hill. The skulls were very modern in appearance. In 1948-49, British scientists began making chemical tests which proved that neither of these finds is very old. It is now known that both -“Piltdown man” and the tools which were said to have been found with +�Piltdown man� and the tools which were said to have been found with him were part of an elaborate fake! -TYPICAL “CAVE MEN” +TYPICAL �CAVE MEN� The next men we have to talk about are all members of a related group. -These are the Neanderthal group. “Neanderthal man” himself was found in -the Neander Valley, near Düsseldorf, Germany, in 1856. He was the first +These are the Neanderthal group. �Neanderthal man� himself was found in +the Neander Valley, near D�sseldorf, Germany, in 1856. He was the first human fossil to be recognized as such. [Illustration: PRINCIPAL KNOWN TYPES OF FOSSIL MEN @@ -999,7 +999,7 @@ human fossil to be recognized as such. PITHECANTHROPUS] Some of us think that the neanderthaloids proper are only those people -of western Europe who didn’t get out before the beginning of the last +of western Europe who didn�t get out before the beginning of the last great glaciation, and who found themselves hemmed in by the glaciers in the Alps and northern Europe. Being hemmed in, they intermarried a bit too much and developed into a special type. Professor F. Clark @@ -1010,7 +1010,7 @@ pre-neanderthaloids. There are traces of these pre-neanderthaloids pretty much throughout Europe during the third interglacial period--say 100,000 years ago. The pre-neanderthaloids are represented by such finds as the ones at Ehringsdorf in Germany and Saccopastore in Italy. -I won’t describe them for you, since they are simply less extreme than +I won�t describe them for you, since they are simply less extreme than the neanderthaloids proper--about half way between Steinheim and the classic Neanderthal people. @@ -1019,24 +1019,24 @@ get caught in the pocket of the southwest corner of Europe at the onset of the last great glaciation became the classic Neanderthalers. Out in the Near East, Howell thinks, it is possible to see traces of people evolving from the pre-neanderthaloid type toward that of fully modern -man. Certainly, we don’t see such extreme cases of “neanderthaloidism” +man. Certainly, we don�t see such extreme cases of �neanderthaloidism� outside of western Europe. There are at least a dozen good examples in the main or classic Neanderthal group in Europe. They date to just before and in the earlier part of the last great glaciation (85,000 to 40,000 years ago). -Many of the finds have been made in caves. The “cave men” the movies +Many of the finds have been made in caves. The �cave men� the movies and the cartoonists show you are probably meant to be Neanderthalers. -I’m not at all sure they dragged their women by the hair; the women +I�m not at all sure they dragged their women by the hair; the women were probably pretty tough, too! Neanderthal men had large bony heads, but plenty of room for brains. Some had brain cases even larger than the average for modern man. Their faces were heavy, and they had eyebrow ridges of bone, but the ridges were not as big as those of Java man. Their foreheads were very low, -and they didn’t have much chin. They were about five feet three inches -tall, but were heavy and barrel-chested. But the Neanderthalers didn’t -slouch as much as they’ve been blamed for, either. +and they didn�t have much chin. They were about five feet three inches +tall, but were heavy and barrel-chested. But the Neanderthalers didn�t +slouch as much as they�ve been blamed for, either. One important thing about the Neanderthal group is that there is a fair number of them to study. Just as important is the fact that we know @@ -1059,10 +1059,10 @@ different-looking people. EARLY MODERN MEN -How early is modern man (_Homo sapiens_), the “wise man”? Some people +How early is modern man (_Homo sapiens_), the �wise man�? Some people have thought that he was very early, a few still think so. Piltdown and Galley Hill, which were quite modern in anatomical appearance and -_supposedly_ very early in date, were the best “evidence” for very +_supposedly_ very early in date, were the best �evidence� for very early modern men. Now that Piltdown has been liquidated and Galley Hill is known to be very late, what is left of the idea? @@ -1073,13 +1073,13 @@ the Ternafine jaws, you might come to the conclusion that the crown of the Swanscombe head was that of a modern-like man. Two more skulls, again without faces, are available from a French -cave site, Fontéchevade. They come from the time of the last great +cave site, Font�chevade. They come from the time of the last great interglacial, as did the pre-neanderthaloids. The crowns of the -Fontéchevade skulls also look quite modern. There is a bit of the +Font�chevade skulls also look quite modern. There is a bit of the forehead preserved on one of these skulls and the brow-ridge is not heavy. Nevertheless, there is a suggestion that the bones belonged to an immature individual. In this case, his (or even more so, if _her_) -brow-ridges would have been weak anyway. The case for the Fontéchevade +brow-ridges would have been weak anyway. The case for the Font�chevade fossils, as modern type men, is little stronger than that for Swanscombe, although Professor Vallois believes it a good case. @@ -1101,8 +1101,8 @@ of the onset of colder weather, when the last glaciation was beginning in the north--say 75,000 years ago. The 70 per cent modern group came from only one cave, Mugharet es-Skhul -(“cave of the kids”). The other group, from several caves, had bones of -men of the type we’ve been calling pre-neanderthaloid which we noted +(�cave of the kids�). The other group, from several caves, had bones of +men of the type we�ve been calling pre-neanderthaloid which we noted were widespread in Europe and beyond. The tools which came with each of these finds were generally similar, and McCown and Keith, and other scholars since their study, have tended to assume that both the Skhul @@ -1131,26 +1131,26 @@ important fossil men of later Europe are shown in the chart on page DIFFERENCES IN THE EARLY MODERNS The main early European moderns have been divided into two groups, the -Cro-Magnon group and the Combe Capelle-Brünn group. Cro-Magnon people +Cro-Magnon group and the Combe Capelle-Br�nn group. Cro-Magnon people were tall and big-boned, with large, long, and rugged heads. They must have been built like many present-day Scandinavians. The Combe -Capelle-Brünn people were shorter; they had narrow heads and faces, and -big eyebrow-ridges. Of course we don’t find the skin or hair of these -people. But there is little doubt they were Caucasoids (“Whites”). +Capelle-Br�nn people were shorter; they had narrow heads and faces, and +big eyebrow-ridges. Of course we don�t find the skin or hair of these +people. But there is little doubt they were Caucasoids (�Whites�). Another important find came in the Italian Riviera, near Monte Carlo. Here, in a cave near Grimaldi, there was a grave containing a woman and a young boy, buried together. The two skeletons were first called -“Negroid” because some features of their bones were thought to resemble +�Negroid� because some features of their bones were thought to resemble certain features of modern African Negro bones. But more recently, Professor E. A. Hooton and other experts questioned the use of the word -“Negroid” in describing the Grimaldi skeletons. It is true that nothing +�Negroid� in describing the Grimaldi skeletons. It is true that nothing is known of the skin color, hair form, or any other fleshy feature of -the Grimaldi people, so that the word “Negroid” in its usual meaning is +the Grimaldi people, so that the word �Negroid� in its usual meaning is not proper here. It is also not clear whether the features of the bones -claimed to be “Negroid” are really so at all. +claimed to be �Negroid� are really so at all. -From a place called Wadjak, in Java, we have “proto-Australoid” skulls +From a place called Wadjak, in Java, we have �proto-Australoid� skulls which closely resemble those of modern Australian natives. Some of the skulls found in South Africa, especially the Boskop skull, look like those of modern Bushmen, but are much bigger. The ancestors of @@ -1159,12 +1159,12 @@ Desert. True African Negroes were forest people who apparently expanded out of the west central African area only in the last several thousand years. Although dark in skin color, neither the Australians nor the Bushmen are Negroes; neither the Wadjak nor the Boskop skulls are -“Negroid.” +�Negroid.� -As we’ve already mentioned, Professor Weidenreich believed that Peking +As we�ve already mentioned, Professor Weidenreich believed that Peking man was already on the way to becoming a Mongoloid. Anyway, the -Mongoloids would seem to have been present by the time of the “Upper -Cave” at Choukoutien, the _Sinanthropus_ find-spot. +Mongoloids would seem to have been present by the time of the �Upper +Cave� at Choukoutien, the _Sinanthropus_ find-spot. WHAT THE DIFFERENCES MEAN @@ -1175,14 +1175,14 @@ From area to area, men tended to look somewhat different, just as they do today. This is all quite natural. People _tended_ to mate near home; in the anthropological jargon, they made up geographically localized breeding populations. The simple continental division of -“stocks”--black = Africa, yellow = Asia, white = Europe--is too simple +�stocks�--black = Africa, yellow = Asia, white = Europe--is too simple a picture to fit the facts. People became accustomed to life in some -particular area within a continent (we might call it a “natural area”). +particular area within a continent (we might call it a �natural area�). As they went on living there, they evolved towards some particular physical variety. It would, of course, have been difficult to draw a clear boundary between two adjacent areas. There must always have been some mating across the boundaries in every case. One thing human -beings don’t do, and never have done, is to mate for “purity.” It is +beings don�t do, and never have done, is to mate for �purity.� It is self-righteous nonsense when we try to kid ourselves into thinking that they do. @@ -1195,28 +1195,28 @@ and they must do the writing about races. I shall, however, give two modern definitions of race, and then make one comment. Dr. William G. Boyd, professor of Immunochemistry, School of - Medicine, Boston University: “We may define a human race as a + Medicine, Boston University: �We may define a human race as a population which differs significantly from other human populations in regard to the frequency of one or more of the genes it - possesses.” + possesses.� Professor Sherwood L. Washburn, professor of Physical Anthropology, - Department of Anthropology, the University of California: “A ‘race’ + Department of Anthropology, the University of California: �A �race� is a group of genetically similar populations, and races intergrade - because there are always intermediate populations.” + because there are always intermediate populations.� My comment is that the ideas involved here are all biological: they concern groups, _not_ individuals. Boyd and Washburn may differ a bit -on what they want to consider a “population,” but a population is a +on what they want to consider a �population,� but a population is a group nevertheless, and genetics is biology to the hilt. Now a lot of people still think of race in terms of how people dress or fix their food or of other habits or customs they have. The next step is to talk -about racial “purity.” None of this has anything whatever to do with +about racial �purity.� None of this has anything whatever to do with race proper, which is a matter of the biology of groups. -Incidentally, I’m told that if man very carefully _controls_ +Incidentally, I�m told that if man very carefully _controls_ the breeding of certain animals over generations--dogs, cattle, -chickens--he might achieve a “pure” race of animals. But he doesn’t do +chickens--he might achieve a �pure� race of animals. But he doesn�t do it. Some unfortunate genetic trait soon turns up, so this has just as carefully to be bred out again, and so on. @@ -1240,20 +1240,20 @@ date to the second great interglacial period, about 350,000 years ago. Piltdown and Galley Hill are out, and with them, much of the starch in the old idea that there were two distinct lines of development -in human evolution: (1) a line of “paleoanthropic” development from +in human evolution: (1) a line of �paleoanthropic� development from Heidelberg to the Neanderthalers where it became extinct, and (2) a -very early “modern” line, through Piltdown, Galley Hill, Swanscombe, to +very early �modern� line, through Piltdown, Galley Hill, Swanscombe, to us. Swanscombe, Steinheim, and Ternafine are just as easily cases of very early pre-neanderthaloids. The pre-neanderthaloids were very widespread during the third interglacial: Ehringsdorf, Saccopastore, some of the Mount Carmel -people, and probably Fontéchevade are cases in point. A variety of +people, and probably Font�chevade are cases in point. A variety of their descendants can be seen, from Java (Solo), Africa (Rhodesian man), and about the Mediterranean and in western Europe. As the acute cold of the last glaciation set in, the western Europeans found themselves surrounded by water, ice, or bitter cold tundra. To vastly -over-simplify it, they “bred in” and became classic neanderthaloids. +over-simplify it, they �bred in� and became classic neanderthaloids. But on Mount Carmel, the Skhul cave-find with its 70 per cent modern features shows what could happen elsewhere at the same time. @@ -1263,12 +1263,12 @@ modern skeletons of men. The modern skeletons differ from place to place, just as different groups of men living in different places still look different. -What became of the Neanderthalers? Nobody can tell me for sure. I’ve a -hunch they were simply “bred out” again when the cold weather was over. +What became of the Neanderthalers? Nobody can tell me for sure. I�ve a +hunch they were simply �bred out� again when the cold weather was over. Many Americans, as the years go by, are no longer ashamed to claim they -have “Indian blood in their veins.” Give us a few more generations +have �Indian blood in their veins.� Give us a few more generations and there will not be very many other Americans left to whom we can -brag about it. It certainly isn’t inconceivable to me to imagine a +brag about it. It certainly isn�t inconceivable to me to imagine a little Cro-Magnon boy bragging to his friends about his tough, strong, Neanderthaler great-great-great-great-grandfather! @@ -1281,15 +1281,15 @@ Cultural BEGINNINGS Men, unlike the lower animals, are made up of much more than flesh and -blood and bones; for men have “culture.” +blood and bones; for men have �culture.� WHAT IS CULTURE? -“Culture” is a word with many meanings. The doctors speak of making a -“culture” of a certain kind of bacteria, and ants are said to have a -“culture.” Then there is the Emily Post kind of “culture”--you say a -person is “cultured,” or that he isn’t, depending on such things as +�Culture� is a word with many meanings. The doctors speak of making a +�culture� of a certain kind of bacteria, and ants are said to have a +�culture.� Then there is the Emily Post kind of �culture�--you say a +person is �cultured,� or that he isn�t, depending on such things as whether or not he eats peas with his knife. The anthropologists use the word too, and argue heatedly over its finer @@ -1300,7 +1300,7 @@ men from another. In this sense, a CULTURE means the way the members of a group of people think and believe and live, the tools they make, and the way they do things. Professor Robert Redfield says a culture is an organized or formalized body of conventional understandings. -“Conventional understandings” means the whole set of rules, beliefs, +�Conventional understandings� means the whole set of rules, beliefs, and standards which a group of people lives by. These understandings show themselves in art, and in the other things a people may make and do. The understandings continue to last, through tradition, from one @@ -1325,12 +1325,12 @@ Egyptians. I mean their beliefs as to why grain grew, as well as their ability to make tools with which to reap the grain. I mean their beliefs about life after death. What I am thinking about as culture is a thing which lasted in time. If any one Egyptian, even the Pharaoh, -died, it didn’t affect the Egyptian culture of that particular moment. +died, it didn�t affect the Egyptian culture of that particular moment. PREHISTORIC CULTURES -For that long period of man’s history that is all prehistory, we have +For that long period of man�s history that is all prehistory, we have no written descriptions of cultures. We find only the tools men made, the places where they lived, the graves in which they buried their dead. Fortunately for us, these tools and living places and graves all @@ -1345,15 +1345,15 @@ of the classic European Neanderthal group of men, we have found few cave-dwelling places of very early prehistoric men. First, there is the fallen-in cave where Peking man was found, near Peking. Then there are two or three other _early_, but not _very early_, possibilities. The -finds at the base of the French cave of Fontéchevade, those in one of +finds at the base of the French cave of Font�chevade, those in one of the Makapan caves in South Africa, and several open sites such as Dr. -L. S. B. Leakey’s Olorgesailie in Kenya doubtless all lie earlier than +L. S. B. Leakey�s Olorgesailie in Kenya doubtless all lie earlier than the time of the main European Neanderthal group, but none are so early as the Peking finds. You can see that we know very little about the home life of earlier prehistoric men. We find different kinds of early stone tools, but we -can’t even be really sure which tools may have been used together. +can�t even be really sure which tools may have been used together. WHY LITTLE HAS LASTED FROM EARLY TIMES @@ -1380,11 +1380,11 @@ there first! The front of this enormous sheet of ice moved down over the country, crushing and breaking and plowing up everything, like a gigantic bulldozer. You can see what happened to our camp site. -Everything the glacier couldn’t break, it pushed along in front of it +Everything the glacier couldn�t break, it pushed along in front of it or plowed beneath it. Rocks were ground to gravel, and soil was caught into the ice, which afterwards melted and ran off as muddy water. Hard -tools of flint sometimes remained whole. Human bones weren’t so hard; -it’s a wonder _any_ of them lasted. Gushing streams of melt water +tools of flint sometimes remained whole. Human bones weren�t so hard; +it�s a wonder _any_ of them lasted. Gushing streams of melt water flushed out the debris from underneath the glacier, and water flowed off the surface and through great crevasses. The hard materials these waters carried were even more rolled and ground up. Finally, such @@ -1407,26 +1407,26 @@ all up, and so we cannot say which particular sets of tools belonged together in the first place. -“EOLITHS” +�EOLITHS� But what sort of tools do we find earliest? For almost a century, people have been picking up odd bits of flint and other stone in the oldest Ice Age gravels in England and France. It is now thought these -odd bits of stone weren’t actually worked by prehistoric men. The -stones were given a name, _eoliths_, or “dawn stones.” You can see them +odd bits of stone weren�t actually worked by prehistoric men. The +stones were given a name, _eoliths_, or �dawn stones.� You can see them in many museums; but you can be pretty sure that very few of them were actually fashioned by men. -It is impossible to pick out “eoliths” that seem to be made in any -one _tradition_. By “tradition” I mean a set of habits for making one -kind of tool for some particular job. No two “eoliths” look very much +It is impossible to pick out �eoliths� that seem to be made in any +one _tradition_. By �tradition� I mean a set of habits for making one +kind of tool for some particular job. No two �eoliths� look very much alike: tools made as part of some one tradition all look much alike. -Now it’s easy to suppose that the very earliest prehistoric men picked -up and used almost any sort of stone. This wouldn’t be surprising; you -and I do it when we go camping. In other words, some of these “eoliths” +Now it�s easy to suppose that the very earliest prehistoric men picked +up and used almost any sort of stone. This wouldn�t be surprising; you +and I do it when we go camping. In other words, some of these �eoliths� may actually have been used by prehistoric men. They must have used anything that might be handy when they needed it. We could have figured -that out without the “eoliths.” +that out without the �eoliths.� THE ROAD TO STANDARDIZATION @@ -1434,7 +1434,7 @@ THE ROAD TO STANDARDIZATION Reasoning from what we know or can easily imagine, there should have been three major steps in the prehistory of tool-making. The first step would have been simple _utilization_ of what was at hand. This is the -step into which the “eoliths” would fall. The second step would have +step into which the �eoliths� would fall. The second step would have been _fashioning_--the haphazard preparation of a tool when there was a need for it. Probably many of the earlier pebble tools, which I shall describe next, fall into this group. The third step would have been @@ -1447,7 +1447,7 @@ tradition appears. PEBBLE TOOLS -At the beginning of the last chapter, you’ll remember that I said there +At the beginning of the last chapter, you�ll remember that I said there were tools from very early geological beds. The earliest bones of men have not yet been found in such early beds although the Sterkfontein australopithecine cave approaches this early date. The earliest tools @@ -1467,7 +1467,7 @@ Old World besides Africa; in fact, some prehistorians already claim to have identified a few. Since the forms and the distinct ways of making the earlier pebble tools had not yet sufficiently jelled into a set tradition, they are difficult for us to recognize. It is not -so difficult, however, if there are great numbers of “possibles” +so difficult, however, if there are great numbers of �possibles� available. A little later in time the tradition becomes more clearly set, and pebble tools are easier to recognize. So far, really large collections of pebble tools have only been found and examined in Africa. @@ -1475,9 +1475,9 @@ collections of pebble tools have only been found and examined in Africa. CORE-BIFACE TOOLS -The next tradition we’ll look at is the _core_ or biface one. The tools +The next tradition we�ll look at is the _core_ or biface one. The tools are large pear-shaped pieces of stone trimmed flat on the two opposite -sides or “faces.” Hence “biface” has been used to describe these tools. +sides or �faces.� Hence �biface� has been used to describe these tools. The front view is like that of a pear with a rather pointed top, and the back view looks almost exactly the same. Look at them side on, and you can see that the front and back faces are the same and have been @@ -1488,7 +1488,7 @@ illustration. [Illustration: ABBEVILLIAN BIFACE] We have very little idea of the way in which these core-bifaces were -used. They have been called “hand axes,” but this probably gives the +used. They have been called �hand axes,� but this probably gives the wrong idea, for an ax, to us, is not a pointed tool. All of these early tools must have been used for a number of jobs--chopping, scraping, cutting, hitting, picking, and prying. Since the core-bifaces tend to @@ -1505,7 +1505,7 @@ a big block of stone. You had to break off the flake in such a way that it was broad and thin, and also had a good sharp cutting edge. Once you really got on to the trick of doing it, this was probably a simpler way to make a good cutting tool than preparing a biface. You have to know -how, though; I’ve tried it and have mashed my fingers more than once. +how, though; I�ve tried it and have mashed my fingers more than once. The flake tools look as if they were meant mainly for chopping, scraping, and cutting jobs. When one made a flake tool, the idea seems @@ -1535,9 +1535,9 @@ tradition. It probably has its earliest roots in the pebble tool tradition of African type. There are several kinds of tools in this tradition, but all differ from the western core-bifaces and flakes. There are broad, heavy scrapers or cleavers, and tools with an -adze-like cutting edge. These last-named tools are called “hand adzes,” -just as the core-bifaces of the west have often been called “hand -axes.” The section of an adze cutting edge is ? shaped; the section of +adze-like cutting edge. These last-named tools are called �hand adzes,� +just as the core-bifaces of the west have often been called �hand +axes.� The section of an adze cutting edge is ? shaped; the section of an ax is < shaped. [Illustration: ANYATHIAN ADZE-LIKE TOOL] @@ -1581,17 +1581,17 @@ stratification.[3] Soan (India) Flake: - “Typical Mousterian” + �Typical Mousterian� Levalloiso-Mousterian Levalloisian Tayacian Clactonian (localized in England) Core-biface: - Some blended elements in “Mousterian” + Some blended elements in �Mousterian� Micoquian (= Acheulean 6 and 7) Acheulean - Abbevillian (once called “Chellean”) + Abbevillian (once called �Chellean�) Pebble tool: Oldowan @@ -1608,8 +1608,8 @@ out of glacial gravels the easiest thing to do first is to isolate individual types of tools into groups. First you put a bushel-basketful of tools on a table and begin matching up types. Then you give names to the groups of each type. The groups and the types are really matters of -the archeologists’ choice; in real life, they were probably less exact -than the archeologists’ lists of them. We now know pretty well in which +the archeologists� choice; in real life, they were probably less exact +than the archeologists� lists of them. We now know pretty well in which of the early traditions the various early groups belong. @@ -1635,9 +1635,9 @@ production must have been passed on from one generation to another. I could even guess that the notions of the ideal type of one or the other of these tools stood out in the minds of men of those times -somewhat like a symbol of “perfect tool for good job.” If this were -so--remember it’s only a wild guess of mine--then men were already -symbol users. Now let’s go on a further step to the fact that the words +somewhat like a symbol of �perfect tool for good job.� If this were +so--remember it�s only a wild guess of mine--then men were already +symbol users. Now let�s go on a further step to the fact that the words men speak are simply sounds, each different sound being a symbol for a different meaning. If standardized tool-making suggests symbol-making, is it also possible that crude word-symbols were also being made? I @@ -1650,7 +1650,7 @@ of our second step is more suggestive, although we may not yet feel sure that many of the earlier pebble tools were man-made products. But with the step to standardization and the appearance of the traditions, I believe we must surely be dealing with the traces of culture-bearing -_men_. The “conventional understandings” which Professor Redfield’s +_men_. The �conventional understandings� which Professor Redfield�s definition of culture suggests are now evidenced for us in the persistent habits for the preparation of stone tools. Were we able to see the other things these prehistoric men must have made--in materials @@ -1666,19 +1666,19 @@ In the last chapter, I told you that many of the older archeologists and human paleontologists used to think that modern man was very old. The supposed ages of Piltdown and Galley Hill were given as evidence of the great age of anatomically modern man, and some interpretations -of the Swanscombe and Fontéchevade fossils were taken to support +of the Swanscombe and Font�chevade fossils were taken to support this view. The conclusion was that there were two parallel lines or -“phyla” of men already present well back in the Pleistocene. The -first of these, the more primitive or “paleoanthropic” line, was +�phyla� of men already present well back in the Pleistocene. The +first of these, the more primitive or �paleoanthropic� line, was said to include Heidelberg, the proto-neanderthaloids and classic -Neanderthal. The more anatomically modern or “neanthropic” line was +Neanderthal. The more anatomically modern or �neanthropic� line was thought to consist of Piltdown and the others mentioned above. The Neanderthaler or paleoanthropic line was thought to have become extinct after the first phase of the last great glaciation. Of course, the modern or neanthropic line was believed to have persisted into the -present, as the basis for the world’s population today. But with +present, as the basis for the world�s population today. But with Piltdown liquidated, Galley Hill known to be very late, and Swanscombe -and Fontéchevade otherwise interpreted, there is little left of the +and Font�chevade otherwise interpreted, there is little left of the so-called parallel phyla theory. While the theory was in vogue, however, and as long as the European @@ -1695,9 +1695,9 @@ where they had actually been dropped by the men who made and used them. The tools came, rather, from the secondary hodge-podge of the glacial gravels. I tried to give you a picture of the bulldozing action of glaciers (p. 40) and of the erosion and weathering that were -side-effects of a glacially conditioned climate on the earth’s surface. +side-effects of a glacially conditioned climate on the earth�s surface. As we said above, if one simply plucks tools out of the redeposited -gravels, his natural tendency is to “type” the tools by groups, and to +gravels, his natural tendency is to �type� the tools by groups, and to think that the groups stand for something _on their own_. In 1906, M. Victor Commont actually made a rare find of what seems @@ -1705,15 +1705,15 @@ to have been a kind of workshop site, on a terrace above the Somme river in France. Here, Commont realized, flake tools appeared clearly in direct association with core-biface tools. Few prehistorians paid attention to Commont or his site, however. It was easier to believe -that flake tools represented a distinct “culture” and that this -“culture” was that of the Neanderthaler or paleoanthropic line, and -that the core-bifaces stood for another “culture” which was that of the +that flake tools represented a distinct �culture� and that this +�culture� was that of the Neanderthaler or paleoanthropic line, and +that the core-bifaces stood for another �culture� which was that of the supposed early modern or neanthropic line. Of course, I am obviously skipping many details here. Some later sites with Neanderthal fossils do seem to have only flake tools, but other such sites have both types of tools. The flake tools which appeared _with_ the core-bifaces in the Swanscombe gravels were never made much of, although it -was embarrassing for the parallel phyla people that Fontéchevade +was embarrassing for the parallel phyla people that Font�chevade ran heavily to flake tools. All in all, the parallel phyla theory flourished because it seemed so neat and easy to understand. @@ -1722,20 +1722,20 @@ TRADITIONS ARE TOOL-MAKING HABITS, NOT CULTURES In case you think I simply enjoy beating a dead horse, look in any standard book on prehistory written twenty (or even ten) years ago, or -in most encyclopedias. You’ll find that each of the individual tool -types, of the West, at least, was supposed to represent a “culture.” -The “cultures” were believed to correspond to parallel lines of human +in most encyclopedias. You�ll find that each of the individual tool +types, of the West, at least, was supposed to represent a �culture.� +The �cultures� were believed to correspond to parallel lines of human evolution. In 1937, Mr. Harper Kelley strongly re-emphasized the importance -of Commont’s workshop site and the presence of flake tools with -core-bifaces. Next followed Dr. Movius’ clear delineation of the +of Commont�s workshop site and the presence of flake tools with +core-bifaces. Next followed Dr. Movius� clear delineation of the chopper-chopping tool tradition of the Far East. This spoiled the nice symmetry of the flake-tool = paleoanthropic, core-biface = neanthropic equations. Then came increasing understanding of the importance of the pebble tools in Africa, and the location of several more workshop sites there, especially at Olorgesailie in Kenya. Finally came the -liquidation of Piltdown and the deflation of Galley Hill’s date. So it +liquidation of Piltdown and the deflation of Galley Hill�s date. So it is at last possible to picture an individual prehistoric man making a flake tool to do one job and a core-biface tool to do another. Commont showed us this picture in 1906, but few believed him. @@ -1751,7 +1751,7 @@ that of the cave on Mount Carmel in Palestine, where the blended pre-neanderthaloid, 70 per cent modern-type skulls were found. Here, in the same level with the skulls, were 9,784 flint tools. Of these, only three--doubtless strays--were core-bifaces; all the rest were flake -tools or flake chips. We noted above how the Fontéchevade cave ran to +tools or flake chips. We noted above how the Font�chevade cave ran to flake tools. The only conclusion I would draw from this is that times and circumstances did exist in which prehistoric men needed only flake tools. So they only made flake tools for those particular times and @@ -1773,13 +1773,13 @@ piece of bone. From the gravels which yield the Clactonian flakes of England comes the fire-hardened point of a wooden spear. There are also the chance finds of the fossil human bones themselves, of which we spoke in the last chapter. Aside from the cave of Peking man, none -of the earliest tools have been found in caves. Open air or “workshop” +of the earliest tools have been found in caves. Open air or �workshop� sites which do not seem to have been disturbed later by some geological agency are very rare. The chart on page 65 shows graphically what the situation in west-central Europe seems to have been. It is not yet certain whether -there were pebble tools there or not. The Fontéchevade cave comes +there were pebble tools there or not. The Font�chevade cave comes into the picture about 100,000 years ago or more. But for the earlier hundreds of thousands of years--below the red-dotted line on the chart--the tools we find come almost entirely from the haphazard @@ -1790,13 +1790,13 @@ kinds of all-purpose tools. Almost any one of them could be used for hacking, chopping, cutting, and scraping; so the men who used them must have been living in a rough and ready sort of way. They found or hunted their food wherever they could. In the anthropological jargon, they -were “food-gatherers,” pure and simple. +were �food-gatherers,� pure and simple. Because of the mixture in the gravels and in the materials they -carried, we can’t be sure which animals these men hunted. Bones of +carried, we can�t be sure which animals these men hunted. Bones of the larger animals turn up in the gravels, but they could just as well belong to the animals who hunted the men, rather than the other -way about. We don’t know. This is why camp sites like Commont’s and +way about. We don�t know. This is why camp sites like Commont�s and Olorgesailie in Kenya are so important when we do find them. The animal bones at Olorgesailie belonged to various mammals of extremely large size. Probably they were taken in pit-traps, but there are a number of @@ -1809,18 +1809,18 @@ animal. Professor F. Clark Howell recently returned from excavating another important open air site at Isimila in Tanganyika. The site yielded the bones of many fossil animals and also thousands of core-bifaces, -flakes, and choppers. But Howell’s reconstruction of the food-getting -habits of the Isimila people certainly suggests that the word “hunting” -is too dignified for what they did; “scavenging” would be much nearer +flakes, and choppers. But Howell�s reconstruction of the food-getting +habits of the Isimila people certainly suggests that the word �hunting� +is too dignified for what they did; �scavenging� would be much nearer the mark. During a great part of this time the climate was warm and pleasant. The second interglacial period (the time between the second and third great alpine glaciations) lasted a long time, and during much of this time -the climate may have been even better than ours is now. We don’t know +the climate may have been even better than ours is now. We don�t know that earlier prehistoric men in Europe or Africa lived in caves. They may not have needed to; much of the weather may have been so nice that -they lived in the open. Perhaps they didn’t wear clothes, either. +they lived in the open. Perhaps they didn�t wear clothes, either. WHAT THE PEKING CAVE-FINDS TELL US @@ -1832,7 +1832,7 @@ were bones of dangerous animals, members of the wolf, bear, and cat families. Some of the cat bones belonged to beasts larger than tigers. There were also bones of other wild animals: buffalo, camel, deer, elephants, horses, sheep, and even ostriches. Seventy per cent of the -animals Peking man killed were fallow deer. It’s much too cold and dry +animals Peking man killed were fallow deer. It�s much too cold and dry in north China for all these animals to live there today. So this list helps us know that the weather was reasonably warm, and that there was enough rain to grow grass for the grazing animals. The list also helps @@ -1840,7 +1840,7 @@ the paleontologists to date the find. Peking man also seems to have eaten plant food, for there are hackberry seeds in the debris of the cave. His tools were made of sandstone and -quartz and sometimes of a rather bad flint. As we’ve already seen, they +quartz and sometimes of a rather bad flint. As we�ve already seen, they belong in the chopper-tool tradition. It seems fairly clear that some of the edges were chipped by right-handed people. There are also many split pieces of heavy bone. Peking man probably split them so he could @@ -1850,10 +1850,10 @@ Many of these split bones were the bones of Peking men. Each one of the skulls had already had the base broken out of it. In no case were any of the bones resting together in their natural relation to one another. There is nothing like a burial; all of the bones are scattered. Now -it’s true that animals could have scattered bodies that were not cared +it�s true that animals could have scattered bodies that were not cared for or buried. But splitting bones lengthwise and carefully removing the base of a skull call for both the tools and the people to use them. -It’s pretty clear who the people were. Peking man was a cannibal. +It�s pretty clear who the people were. Peking man was a cannibal. * * * * * @@ -1862,8 +1862,8 @@ prehistoric men. In those days life was rough. You evidently had to watch out not only for dangerous animals but also for your fellow men. You ate whatever you could catch or find growing. But you had sense enough to build fires, and you had already formed certain habits for -making the kinds of stone tools you needed. That’s about all we know. -But I think we’ll have to admit that cultural beginnings had been made, +making the kinds of stone tools you needed. That�s about all we know. +But I think we�ll have to admit that cultural beginnings had been made, and that these early people were really _men_. @@ -1876,16 +1876,16 @@ MORE EVIDENCE of Culture While the dating is not yet sure, the material that we get from caves in Europe must go back to about 100,000 years ago; the time of the -classic Neanderthal group followed soon afterwards. We don’t know why +classic Neanderthal group followed soon afterwards. We don�t know why there is no earlier material in the caves; apparently they were not used before the last interglacial phase (the period just before the last great glaciation). We know that men of the classic Neanderthal group were living in caves from about 75,000 to 45,000 years ago. New radioactive carbon dates even suggest that some of the traces of -culture we’ll describe in this chapter may have lasted to about 35,000 +culture we�ll describe in this chapter may have lasted to about 35,000 years ago. Probably some of the pre-neanderthaloid types of men had also lived in caves. But we have so far found their bones in caves only -in Palestine and at Fontéchevade. +in Palestine and at Font�chevade. THE CAVE LAYERS @@ -1893,7 +1893,7 @@ THE CAVE LAYERS In parts of France, some peasants still live in caves. In prehistoric time, many generations of people lived in them. As a result, many caves have deep layers of debris. The first people moved in and lived -on the rock floor. They threw on the floor whatever they didn’t want, +on the rock floor. They threw on the floor whatever they didn�t want, and they tracked in mud; nobody bothered to clean house in those days. Their debris--junk and mud and garbage and what not--became packed into a layer. As time went on, and generations passed, the layer grew @@ -1910,20 +1910,20 @@ earliest to latest. This is the _stratification_ we talked about (p. [Illustration: SECTION OF SHELTER ON LOWER TERRACE, LE MOUSTIER] -While we may find a mix-up in caves, it’s not nearly as bad as the +While we may find a mix-up in caves, it�s not nearly as bad as the mixing up that was done by glaciers. The animal bones and shells, the fireplaces, the bones of men, and the tools the men made all belong -together, if they come from one layer. That’s the reason why the cave +together, if they come from one layer. That�s the reason why the cave of Peking man is so important. It is also the reason why the caves in Europe and the Near East are so important. We can get an idea of which things belong together and which lot came earliest and which latest. In most cases, prehistoric men lived only in the mouths of caves. -They didn’t like the dark inner chambers as places to live in. They +They didn�t like the dark inner chambers as places to live in. They preferred rock-shelters, at the bases of overhanging cliffs, if there was enough overhang to give shelter. When the weather was good, they no -doubt lived in the open air as well. I’ll go on using the term “cave” -since it’s more familiar, but remember that I really mean rock-shelter, +doubt lived in the open air as well. I�ll go on using the term �cave� +since it�s more familiar, but remember that I really mean rock-shelter, as a place in which people actually lived. The most important European cave sites are in Spain, France, and @@ -1933,29 +1933,29 @@ found when the out-of-the-way parts of Europe, Africa, and Asia are studied. -AN “INDUSTRY” DEFINED +AN �INDUSTRY� DEFINED We have already seen that the earliest European cave materials are -those from the cave of Fontéchevade. Movius feels certain that the +those from the cave of Font�chevade. Movius feels certain that the lowest materials here date back well into the third interglacial stage, -that which lay between the Riss (next to the last) and the Würm I +that which lay between the Riss (next to the last) and the W�rm I (first stage of the last) alpine glaciations. This material consists of an _industry_ of stone tools, apparently all made in the flake -tradition. This is the first time we have used the word “industry.” +tradition. This is the first time we have used the word �industry.� It is useful to call all of the different tools found together in one layer and made of _one kind of material_ an industry; that is, the tools must be found together as men left them. Tools taken from the glacial gravels (or from windswept desert surfaces or river gravels -or any geological deposit) are not “together” in this sense. We might -say the latter have only “geological,” not “archeological” context. +or any geological deposit) are not �together� in this sense. We might +say the latter have only �geological,� not �archeological� context. Archeological context means finding things just as men left them. We -can tell what tools go together in an “industrial” sense only if we +can tell what tools go together in an �industrial� sense only if we have archeological context. -Up to now, the only things we could have called “industries” were the +Up to now, the only things we could have called �industries� were the worked stone industry and perhaps the worked (?) bone industry of the Peking cave. We could add some of the very clear cases of open air -sites, like Olorgesailie. We couldn’t use the term for the stone tools +sites, like Olorgesailie. We couldn�t use the term for the stone tools from the glacial gravels, because we do not know which tools belonged together. But when the cave materials begin to appear in Europe, we can begin to speak of industries. Most of the European caves of this time @@ -1964,16 +1964,16 @@ contain industries of flint tools alone. THE EARLIEST EUROPEAN CAVE LAYERS -We’ve just mentioned the industry from what is said to be the oldest +We�ve just mentioned the industry from what is said to be the oldest inhabited cave in Europe; that is, the industry from the deepest layer -of the site at Fontéchevade. Apparently it doesn’t amount to much. The +of the site at Font�chevade. Apparently it doesn�t amount to much. The tools are made of stone, in the flake tradition, and are very poorly worked. This industry is called _Tayacian_. Its type tool seems to be a smallish flake tool, but there are also larger flakes which seem to have been fashioned for hacking. In fact, the type tool seems to be simply a smaller edition of the Clactonian tool (pictured on p. 45). -None of the Fontéchevade tools are really good. There are scrapers, +None of the Font�chevade tools are really good. There are scrapers, and more or less pointed tools, and tools that may have been used for hacking and chopping. Many of the tools from the earlier glacial gravels are better made than those of this first industry we see in @@ -2005,7 +2005,7 @@ core-biface and the flake traditions. The core-biface tools usually make up less than half of all the tools in the industry. However, the name of the biface type of tool is generally given to the whole industry. It is called the _Acheulean_, actually a late form of it, as -“Acheulean” is also used for earlier core-biface tools taken from the +�Acheulean� is also used for earlier core-biface tools taken from the glacial gravels. In western Europe, the name used is _Upper Acheulean_ or _Micoquian_. The same terms have been borrowed to name layers E and F in the Tabun cave, on Mount Carmel in Palestine. @@ -2029,7 +2029,7 @@ those used for at least one of the flake industries we shall mention presently. There is very little else in these early cave layers. We do not have -a proper “industry” of bone tools. There are traces of fire, and of +a proper �industry� of bone tools. There are traces of fire, and of animal bones, and a few shells. In Palestine, there are many more bones of deer than of gazelle in these layers; the deer lives in a wetter climate than does the gazelle. In the European cave layers, the @@ -2043,18 +2043,18 @@ bones of fossil men definitely in place with this industry. FLAKE INDUSTRIES FROM THE CAVES Two more stone industries--the _Levalloisian_ and the -“_Mousterian_”--turn up at approximately the same time in the European +�_Mousterian_�--turn up at approximately the same time in the European cave layers. Their tools seem to be mainly in the flake tradition, but according to some of the authorities their preparation also shows some combination with the habits by which the core-biface tools were prepared. -Now notice that I don’t tell you the Levalloisian and the “Mousterian” +Now notice that I don�t tell you the Levalloisian and the �Mousterian� layers are both above the late Acheulean layers. Look at the cave -section (p. 57) and you’ll find that some “Mousterian of Acheulean -tradition” appears above some “typical Mousterian.” This means that +section (p. 57) and you�ll find that some �Mousterian of Acheulean +tradition� appears above some �typical Mousterian.� This means that there may be some kinds of Acheulean industries that are later than -some kinds of “Mousterian.” The same is true of the Levalloisian. +some kinds of �Mousterian.� The same is true of the Levalloisian. There were now several different kinds of habits that men used in making stone tools. These habits were based on either one or the other @@ -2072,7 +2072,7 @@ were no patent laws in those days. The extremely complicated interrelationships of the different habits used by the tool-makers of this range of time are at last being -systematically studied. M. François Bordes has developed a statistical +systematically studied. M. Fran�ois Bordes has developed a statistical method of great importance for understanding these tool preparation habits. @@ -2081,22 +2081,22 @@ THE LEVALLOISIAN AND MOUSTERIAN The easiest Levalloisian tool to spot is a big flake tool. The trick in making it was to fashion carefully a big chunk of stone (called -the Levalloisian “tortoise core,” because it resembles the shape of +the Levalloisian �tortoise core,� because it resembles the shape of a turtle-shell) and then to whack this in such a way that a large flake flew off. This large thin flake, with sharp cutting edges, is the finished Levalloisian tool. There were various other tools in a Levalloisian industry, but this is the characteristic _Levalloisian_ tool. -There are several “typical Mousterian” stone tools. Different from -the tools of the Levalloisian type, these were made from “disc-like -cores.” There are medium-sized flake “side scrapers.” There are also -some small pointed tools and some small “hand axes.” The last of these +There are several �typical Mousterian� stone tools. Different from +the tools of the Levalloisian type, these were made from �disc-like +cores.� There are medium-sized flake �side scrapers.� There are also +some small pointed tools and some small �hand axes.� The last of these tool types is often a flake worked on both of the flat sides (that is, bifacially). There are also pieces of flint worked into the form of crude balls. The pointed tools may have been fixed on shafts to make short jabbing spears; the round flint balls may have been used as -bolas. Actually, we don’t _know_ what either tool was used for. The +bolas. Actually, we don�t _know_ what either tool was used for. The points and side scrapers are illustrated (pp. 64 and 66). [Illustration: LEVALLOIS FLAKE] @@ -2108,9 +2108,9 @@ Nowadays the archeologists are less and less sure of the importance of any one specific tool type and name. Twenty years ago, they used to speak simply of Acheulean or Levalloisian or Mousterian tools. Now, more and more, _all_ of the tools from some one layer in a -cave are called an “industry,” which is given a mixed name. Thus we -have “Levalloiso-Mousterian,” and “Acheuleo-Levalloisian,” and even -“Acheuleo-Mousterian” (or “Mousterian of Acheulean tradition”). Bordes’ +cave are called an �industry,� which is given a mixed name. Thus we +have �Levalloiso-Mousterian,� and �Acheuleo-Levalloisian,� and even +�Acheuleo-Mousterian� (or �Mousterian of Acheulean tradition�). Bordes� systematic work is beginning to clear up some of our confusion. The time of these late Acheuleo-Levalloiso-Mousterioid industries @@ -2120,16 +2120,16 @@ phase of the last great glaciation. It was also the time that the classic group of Neanderthal men was living in Europe. A number of the Neanderthal fossil finds come from these cave layers. Before the different habits of tool preparation were understood it used to be -popular to say Neanderthal man was “Mousterian man.” I think this is -wrong. What used to be called “Mousterian” is now known to be a variety +popular to say Neanderthal man was �Mousterian man.� I think this is +wrong. What used to be called �Mousterian� is now known to be a variety of industries with tools of both core-biface and flake habits, and -so mixed that the word “Mousterian” used alone really doesn’t mean +so mixed that the word �Mousterian� used alone really doesn�t mean anything. The Neanderthalers doubtless understood the tool preparation habits by means of which Acheulean, Levalloisian and Mousterian type tools were produced. We also have the more modern-like Mount Carmel people, found in a cave layer of Palestine with tools almost entirely -in the flake tradition, called “Levalloiso-Mousterian,” and the -Fontéchevade-Tayacian (p. 59). +in the flake tradition, called �Levalloiso-Mousterian,� and the +Font�chevade-Tayacian (p. 59). [Illustration: MOUSTERIAN POINT] @@ -2165,7 +2165,7 @@ which seem to have served as anvils or chopping blocks, are fairly common. Bits of mineral, used as coloring matter, have also been found. We -don’t know what the color was used for. +don�t know what the color was used for. [Illustration: MOUSTERIAN SIDE SCRAPER] @@ -2230,7 +2230,7 @@ might suggest some notion of hoarding up the spirits or the strength of bears killed in the hunt. Probably the people lived in small groups, as hunting and food-gathering seldom provide enough food for large groups of people. These groups probably had some kind of leader or -“chief.” Very likely the rude beginnings of rules for community life +�chief.� Very likely the rude beginnings of rules for community life and politics, and even law, were being made. But what these were, we do not know. We can only guess about such things, as we can only guess about many others; for example, how the idea of a family must have been @@ -2246,8 +2246,8 @@ small. The mixtures and blendings of the habits used in making stone tools must mean that there were also mixtures and blends in many of the other ideas and beliefs of these small groups. And what this probably means is that there was no one _culture_ of the time. It is -certainly unlikely that there were simply three cultures, “Acheulean,” -“Levalloisian,” and “Mousterian,” as has been thought in the past. +certainly unlikely that there were simply three cultures, �Acheulean,� +�Levalloisian,� and �Mousterian,� as has been thought in the past. Rather there must have been a great variety of loosely related cultures at about the same stage of advancement. We could say, too, that here we really begin to see, for the first time, that remarkable ability @@ -2272,7 +2272,7 @@ related habits for the making of tools. But the men who made them must have looked much like the men of the West. Their tools were different, but just as useful. -As to what the men of the West looked like, I’ve already hinted at all +As to what the men of the West looked like, I�ve already hinted at all we know so far (pp. 29 ff.). The Neanderthalers were present at the time. Some more modern-like men must have been about, too, since fossils of them have turned up at Mount Carmel in Palestine, and at @@ -2306,7 +2306,7 @@ A NEW TRADITION APPEARS Something new was probably beginning to happen in the European-Mediterranean area about 40,000 years ago, though all the rest of the Old World seems to have been going on as it had been. I -can’t be sure of this because the information we are using as a basis +can�t be sure of this because the information we are using as a basis for dates is very inaccurate for the areas outside of Europe and the Mediterranean. @@ -2325,7 +2325,7 @@ drawing shows. It has sharp cutting edges, and makes a very useful knife. The real trick is to be able to make one. It is almost impossible to make a blade out of any stone but flint or a natural volcanic glass called obsidian. And even if you have flint or obsidian, -you first have to work up a special cone-shaped “blade-core,” from +you first have to work up a special cone-shaped �blade-core,� from which to whack off blades. [Illustration: PLAIN BLADE] @@ -2351,8 +2351,8 @@ found in equally early cave levels in Syria; their popularity there seems to fluctuate a bit. Some more or less parallel-sided flakes are known in the Levalloisian industry in France, but they are probably no earlier than Tabun E. The Tabun blades are part of a local late -“Acheulean” industry, which is characterized by core-biface “hand -axes,” but which has many flake tools as well. Professor F. E. +�Acheulean� industry, which is characterized by core-biface �hand +axes,� but which has many flake tools as well. Professor F. E. Zeuner believes that this industry may be more than 120,000 years old; actually its date has not yet been fixed, but it is very old--older than the fossil finds of modern-like men in the same caves. @@ -2371,7 +2371,7 @@ We are not sure just where the earliest _persisting_ habits for the production of blade tools developed. Impressed by the very early momentary appearance of blades at Tabun on Mount Carmel, Professor Dorothy A. Garrod first favored the Near East as a center of origin. -She spoke of “some as yet unidentified Asiatic centre,” which she +She spoke of �some as yet unidentified Asiatic centre,� which she thought might be in the highlands of Iran or just beyond. But more recent work has been done in this area, especially by Professor Coon, and the blade tools do not seem to have an early appearance there. When @@ -2395,21 +2395,21 @@ core (and the striking of the Levalloisian flake from it) might have followed through to the conical core and punch technique for the production of blades. Professor Garrod is much impressed with the speed of change during the later phases of the last glaciation, and its -probable consequences. She speaks of “the greater number of industries +probable consequences. She speaks of �the greater number of industries having enough individual character to be classified as distinct ... -since evolution now starts to outstrip diffusion.” Her “evolution” here +since evolution now starts to outstrip diffusion.� Her �evolution� here is of course an industrial evolution rather than a biological one. Certainly the people of Europe had begun to make blade tools during the warm spell after the first phase of the last glaciation. By about 40,000 years ago blades were well established. The bones of the blade -tool makers we’ve found so far indicate that anatomically modern men +tool makers we�ve found so far indicate that anatomically modern men had now certainly appeared. Unfortunately, only a few fossil men have so far been found from the very beginning of the blade tool range in Europe (or elsewhere). What I certainly shall _not_ tell you is that conquering bands of fine, strong, anatomically modern men, armed with superior blade tools, came sweeping out of the East to exterminate the -lowly Neanderthalers. Even if we don’t know exactly what happened, I’d -lay a good bet it wasn’t that simple. +lowly Neanderthalers. Even if we don�t know exactly what happened, I�d +lay a good bet it wasn�t that simple. We do know a good deal about different blade industries in Europe. Almost all of them come from cave layers. There is a great deal of @@ -2418,7 +2418,7 @@ this complication; in fact, it doubtless simplifies it too much. But it may suggest all the complication of industries which is going on at this time. You will note that the upper portion of my much simpler chart (p. 65) covers the same material (in the section -marked “Various Blade-Tool Industries”). That chart is certainly too +marked �Various Blade-Tool Industries�). That chart is certainly too simplified. You will realize that all this complication comes not only from @@ -2429,7 +2429,7 @@ a good deal of climatic change at this time. The plants and animals that men used for food were changing, too. The great variety of tools and industries we now find reflect these changes and the ability of men to keep up with the times. Now, for example, is the first time we are -sure that there are tools to _make_ other tools. They also show men’s +sure that there are tools to _make_ other tools. They also show men�s increasing ability to adapt themselves. @@ -2437,15 +2437,15 @@ SPECIAL TYPES OF BLADE TOOLS The most useful tools that appear at this time were made from blades. - 1. The “backed” blade. This is a knife made of a flint blade, with - one edge purposely blunted, probably to save the user’s fingers + 1. The �backed� blade. This is a knife made of a flint blade, with + one edge purposely blunted, probably to save the user�s fingers from being cut. There are several shapes of backed blades (p. 73). [Illustration: TWO BURINS] - 2. The _burin_ or “graver.” The burin was the original chisel. Its - cutting edge is _transverse_, like a chisel’s. Some burins are + 2. The _burin_ or �graver.� The burin was the original chisel. Its + cutting edge is _transverse_, like a chisel�s. Some burins are made like a screw-driver, save that burins are sharp. Others have edges more like the blade of a chisel or a push plane, with only one bevel. Burins were probably used to make slots in wood @@ -2456,29 +2456,29 @@ The most useful tools that appear at this time were made from blades. [Illustration: TANGED POINT] - 3. The “tanged” point. These stone points were used to tip arrows or + 3. The �tanged� point. These stone points were used to tip arrows or light spears. They were made from blades, and they had a long tang at the bottom where they were fixed to the shaft. At the place where the tang met the main body of the stone point, there was - a marked “shoulder,” the beginnings of a barb. Such points had + a marked �shoulder,� the beginnings of a barb. Such points had either one or two shoulders. [Illustration: NOTCHED BLADE] - 4. The “notched” or “strangulated” blade. Along with the points for + 4. The �notched� or �strangulated� blade. Along with the points for arrows or light spears must go a tool to prepare the arrow or - spear shaft. Today, such a tool would be called a “draw-knife” or - a “spoke-shave,” and this is what the notched blades probably are. + spear shaft. Today, such a tool would be called a �draw-knife� or + a �spoke-shave,� and this is what the notched blades probably are. Our spoke-shaves have sharp straight cutting blades and really - “shave.” Notched blades of flint probably scraped rather than cut. + �shave.� Notched blades of flint probably scraped rather than cut. - 5. The “awl,” “drill,” or “borer.” These blade tools are worked out + 5. The �awl,� �drill,� or �borer.� These blade tools are worked out to a spike-like point. They must have been used for making holes in wood, bone, shell, skin, or other things. [Illustration: DRILL OR AWL] - 6. The “end-scraper on a blade” is a tool with one or both ends + 6. The �end-scraper on a blade� is a tool with one or both ends worked so as to give a good scraping edge. It could have been used to hollow out wood or bone, scrape hides, remove bark from trees, and a number of other things (p. 78). @@ -2489,11 +2489,11 @@ usually made of blades, but the best examples are so carefully worked on both sides (bifacially) that it is impossible to see the original blade. This tool is - 7. The “laurel leaf” point. Some of these tools were long and + 7. The �laurel leaf� point. Some of these tools were long and dagger-like, and must have been used as knives or daggers. Others - were small, called “willow leaf,” and must have been mounted on + were small, called �willow leaf,� and must have been mounted on spear or arrow shafts. Another typical Solutrean tool is the - “shouldered” point. Both the “laurel leaf” and “shouldered” point + �shouldered� point. Both the �laurel leaf� and �shouldered� point types are illustrated (see above and p. 79). [Illustration: END-SCRAPER ON A BLADE] @@ -2507,17 +2507,17 @@ second is a core tool. [Illustration: SHOULDERED POINT] - 8. The “keel-shaped round scraper” is usually small and quite round, + 8. The �keel-shaped round scraper� is usually small and quite round, and has had chips removed up to a peak in the center. It is called - “keel-shaped” because it is supposed to look (when upside down) + �keel-shaped� because it is supposed to look (when upside down) like a section through a boat. Actually, it looks more like a tent or an umbrella. Its outer edges are sharp all the way around, and it was probably a general purpose scraping tool (see illustration, p. 81). - 9. The “keel-shaped nosed scraper” is a much larger and heavier tool + 9. The �keel-shaped nosed scraper� is a much larger and heavier tool than the round scraper. It was made on a core with a flat bottom, - and has one nicely worked end or “nose.” Such tools are usually + and has one nicely worked end or �nose.� Such tools are usually large enough to be easily grasped, and probably were used like push planes (see illustration, p. 81). @@ -2530,7 +2530,7 @@ the most easily recognized blade tools, although they show differences in detail at different times. There are also many other kinds. Not all of these tools appear in any one industry at one time. Thus the different industries shown in the chart (p. 72) each have only some -of the blade tools we’ve just listed, and also a few flake tools. Some +of the blade tools we�ve just listed, and also a few flake tools. Some industries even have a few core tools. The particular types of blade tools appearing in one cave layer or another, and the frequency of appearance of the different types, tell which industry we have in each @@ -2545,15 +2545,15 @@ to appear. There are knives, pins, needles with eyes, and little double-pointed straight bars of bone that were probably fish-hooks. The fish-line would have been fastened in the center of the bar; when the fish swallowed the bait, the bar would have caught cross-wise in the -fish’s mouth. +fish�s mouth. One quite special kind of bone tool is a long flat point for a light spear. It has a deep notch cut up into the breadth of its base, and is -called a “split-based bone point” (p. 82). We know examples of bone +called a �split-based bone point� (p. 82). We know examples of bone beads from these times, and of bone handles for flint tools. Pierced teeth of some animals were worn as beads or pendants, but I am not sure -that elks’ teeth were worn this early. There are even spool-shaped -“buttons” or toggles. +that elks� teeth were worn this early. There are even spool-shaped +�buttons� or toggles. [Illustration: SPLIT-BASED BONE POINT] @@ -2595,12 +2595,12 @@ almost to have served as sketch blocks. The surfaces of these various objects may show animals, or rather abstract floral designs, or geometric designs. -[Illustration: “VENUS” FIGURINE FROM WILLENDORF] +[Illustration: �VENUS� FIGURINE FROM WILLENDORF] Some of the movable art is not done on tools. The most remarkable examples of this class are little figures of women. These women seem to be pregnant, and their most female characteristics are much emphasized. -It is thought that these “Venus” or “Mother-goddess” figurines may be +It is thought that these �Venus� or �Mother-goddess� figurines may be meant to show the great forces of nature--fertility and the birth of life. @@ -2616,21 +2616,21 @@ are different styles in the cave art. The really great cave art is pretty well restricted to southern France and Cantabrian (northwestern) Spain. -There are several interesting things about the “Franco-Cantabrian” cave +There are several interesting things about the �Franco-Cantabrian� cave art. It was done deep down in the darkest and most dangerous parts of the caves, although the men lived only in the openings of caves. If you think what they must have had for lights--crude lamps of hollowed stone have been found, which must have burned some kind of oil or grease, with a matted hair or fiber wick--and of the animals that may have -lurked in the caves, you’ll understand the part about danger. Then, -too, we’re sure the pictures these people painted were not simply to be +lurked in the caves, you�ll understand the part about danger. Then, +too, we�re sure the pictures these people painted were not simply to be looked at and admired, for they painted one picture right over other pictures which had been done earlier. Clearly, it was the _act_ of _painting_ that counted. The painter had to go way down into the most mysterious depths of the earth and create an animal in paint. Possibly he believed that by doing this he gained some sort of magic power over the same kind of animal when he hunted it in the open air. It certainly -doesn’t look as if he cared very much about the picture he painted--as +doesn�t look as if he cared very much about the picture he painted--as a finished product to be admired--for he or somebody else soon went down and painted another animal right over the one he had done. @@ -2683,10 +2683,10 @@ it. Their art is another example of the direction the human mind was taking. And when I say human, I mean it in the fullest sense, for this is the time in which fully modern man has appeared. On page 34, we -spoke of the Cro-Magnon group and of the Combe Capelle-Brünn group of -Caucasoids and of the Grimaldi “Negroids,” who are no longer believed +spoke of the Cro-Magnon group and of the Combe Capelle-Br�nn group of +Caucasoids and of the Grimaldi �Negroids,� who are no longer believed to be Negroid. I doubt that any one of these groups produced most of -the achievements of the times. It’s not yet absolutely sure which +the achievements of the times. It�s not yet absolutely sure which particular group produced the great cave art. The artists were almost certainly a blend of several (no doubt already mixed) groups. The pair of Grimaldians were buried in a grave with a sprinkling of red ochre, @@ -2705,9 +2705,9 @@ also found about the shore of the Mediterranean basin, and it moved into northern Europe as the last glaciation pulled northward. People began making blade tools of very small size. They learned how to chip very slender and tiny blades from a prepared core. Then they made these -little blades into tiny triangles, half-moons (“lunates”), trapezoids, +little blades into tiny triangles, half-moons (�lunates�), trapezoids, and several other geometric forms. These little tools are called -“microliths.” They are so small that most of them must have been fixed +�microliths.� They are so small that most of them must have been fixed in handles or shafts. [Illustration: MICROLITHS @@ -2726,7 +2726,7 @@ One corner of each little triangle stuck out, and the whole thing made a fine barbed harpoon. In historic times in Egypt, geometric trapezoidal microliths were still in use as arrowheads. They were fastened--broad end out--on the end of an arrow shaft. It seems queer -to give an arrow a point shaped like a “T.” Actually, the little points +to give an arrow a point shaped like a �T.� Actually, the little points were very sharp, and must have pierced the hides of animals very easily. We also think that the broader cutting edge of the point may have caused more bleeding than a pointed arrowhead would. In hunting @@ -2739,7 +2739,7 @@ is some evidence that they appear early in the Near East. Their use was very common in northwest Africa but this came later. The microlith makers who reached south Russia and central Europe possibly moved up out of the Near East. Or it may have been the other way around; we -simply don’t yet know. +simply don�t yet know. Remember that the microliths we are talking about here were made from carefully prepared little blades, and are often geometric in outline. @@ -2749,7 +2749,7 @@ even some flake scrapers, in most microlithic industries. I emphasize this bladelet and the geometric character of the microlithic industries of the western Old World, since there has sometimes been confusion in the matter. Sometimes small flake chips, utilized as minute pointed -tools, have been called “microliths.” They may be _microlithic_ in size +tools, have been called �microliths.� They may be _microlithic_ in size in terms of the general meaning of the word, but they do not seem to belong to the sub-tradition of the blade tool preparation habits which we have been discussing here. @@ -2763,10 +2763,10 @@ in western Asia too, and early, although Professor Garrod is no longer sure that the whole tradition originated in the Near East. If you look again at my chart (p. 72) you will note that in western Asia I list some of the names of the western European industries, but with the -qualification “-like” (for example, “Gravettian-like”). The western +qualification �-like� (for example, �Gravettian-like�). The western Asiatic blade-tool industries do vaguely recall some aspects of those of western Europe, but we would probably be better off if we used -completely local names for them. The “Emiran” of my chart is such an +completely local names for them. The �Emiran� of my chart is such an example; its industry includes a long spike-like blade point which has no western European counterpart. @@ -2774,13 +2774,13 @@ When we last spoke of Africa (p. 66), I told you that stone tools there were continuing in the Levalloisian flake tradition, and were becoming smaller. At some time during this process, two new tool types appeared in northern Africa: one was the Aterian point with -a tang (p. 67), and the other was a sort of “laurel leaf” point, -called the “Sbaikian.” These two tool types were both produced from +a tang (p. 67), and the other was a sort of �laurel leaf� point, +called the �Sbaikian.� These two tool types were both produced from flakes. The Sbaikian points, especially, are roughly similar to some of the Solutrean points of Europe. It has been suggested that both the Sbaikian and Aterian points may be seen on their way to France through their appearance in the Spanish cave deposits of Parpallo, but there is -also a rival “pre-Solutrean” in central Europe. We still do not know +also a rival �pre-Solutrean� in central Europe. We still do not know whether there was any contact between the makers of these north African tools and the Solutrean tool-makers. What does seem clear is that the blade-tool tradition itself arrived late in northern Africa. @@ -2788,11 +2788,11 @@ blade-tool tradition itself arrived late in northern Africa. NETHER AFRICA -Blade tools and “laurel leaf” points and some other probably late +Blade tools and �laurel leaf� points and some other probably late stone tool types also appear in central and southern Africa. There are geometric microliths on bladelets and even some coarse pottery in east Africa. There is as yet no good way of telling just where these -items belong in time; in broad geological terms they are “late.” +items belong in time; in broad geological terms they are �late.� Some people have guessed that they are as early as similar European and Near Eastern examples, but I doubt it. The makers of small-sized Levalloisian flake tools occupied much of Africa until very late in @@ -2823,18 +2823,18 @@ ancestors of the American Indians came from Asia. The stone-tool traditions of Europe, Africa, the Near and Middle East, and central Siberia, did _not_ move into the New World. With only a very few special or late exceptions, there are _no_ core-bifaces, -flakes, or blade tools of the Old World. Such things just haven’t been +flakes, or blade tools of the Old World. Such things just haven�t been found here. -This is why I say it’s a shame we don’t know more of the end of the +This is why I say it�s a shame we don�t know more of the end of the chopper-tool tradition in the Far East. According to Weidenreich, the Mongoloids were in the Far East long before the end of the last glaciation. If the genetics of the blood group types do demand a non-Mongoloid ancestry for the American Indians, who else may have been in the Far East 25,000 years ago? We know a little about the habits for making stone tools which these first people brought with them, -and these habits don’t conform with those of the western Old World. -We’d better keep our eyes open for whatever happened to the end of +and these habits don�t conform with those of the western Old World. +We�d better keep our eyes open for whatever happened to the end of the chopper-tool tradition in northern China; already there are hints that it lasted late there. Also we should watch future excavations in eastern Siberia. Perhaps we shall find the chopper-tool tradition @@ -2846,13 +2846,13 @@ THE NEW ERA Perhaps it comes in part from the way I read the evidence and perhaps in part it is only intuition, but I feel that the materials of this chapter suggest a new era in the ways of life. Before about 40,000 -years ago, people simply “gathered” their food, wandering over large +years ago, people simply �gathered� their food, wandering over large areas to scavenge or to hunt in a simple sort of way. But here we -have seen them “settling-in” more, perhaps restricting themselves in +have seen them �settling-in� more, perhaps restricting themselves in their wanderings and adapting themselves to a given locality in more intensive ways. This intensification might be suggested by the word -“collecting.” The ways of life we described in the earlier chapters -were “food-gathering” ways, but now an era of “food-collecting” has +�collecting.� The ways of life we described in the earlier chapters +were �food-gathering� ways, but now an era of �food-collecting� has begun. We shall see further intensifications of it in the next chapter. @@ -2883,8 +2883,8 @@ The last great glaciation of the Ice Age was a two-part affair, with a sub-phase at the end of the second part. In Europe the last sub-phase of this glaciation commenced somewhere around 15,000 years ago. Then the glaciers began to melt back, for the last time. Remember that -Professor Antevs (p. 19) isn’t sure the Ice Age is over yet! This -melting sometimes went by fits and starts, and the weather wasn’t +Professor Antevs (p. 19) isn�t sure the Ice Age is over yet! This +melting sometimes went by fits and starts, and the weather wasn�t always changing for the better; but there was at least one time when European weather was even better than it is now. @@ -2927,16 +2927,16 @@ Sweden. Much of this north European material comes from bogs and swamps where it had become water-logged and has kept very well. Thus we have much more complete _assemblages_[4] than for any time earlier. - [4] “Assemblage” is a useful word when there are different kinds of + [4] �Assemblage� is a useful word when there are different kinds of archeological materials belonging together, from one area and of - one time. An assemblage is made up of a number of “industries” + one time. An assemblage is made up of a number of �industries� (that is, all the tools in chipped stone, all the tools in bone, all the tools in wood, the traces of houses, etc.) and everything else that manages to survive, such as the art, the burials, the bones of the animals used as food, and the traces of plant foods; in fact, everything that has been left to us and can be used to help reconstruct the lives of the people to - whom it once belonged. Our own present-day “assemblage” would be + whom it once belonged. Our own present-day �assemblage� would be the sum total of all the objects in our mail-order catalogues, department stores and supply houses of every sort, our churches, our art galleries and other buildings, together with our roads, @@ -2976,7 +2976,7 @@ found. It seems likely that the Maglemosian bog finds are remains of summer camps, and that in winter the people moved to higher and drier regions. -Childe calls them the “Forest folk”; they probably lived much the +Childe calls them the �Forest folk�; they probably lived much the same sort of life as did our pre-agricultural Indians of the north central states. They hunted small game or deer; they did a great deal of fishing; they collected what plant food they could find. In fact, @@ -3010,7 +3010,7 @@ South of the north European belt the hunting-food-collecting peoples were living on as best they could during this time. One interesting group, which seems to have kept to the regions of sandy soil and scrub forest, made great quantities of geometric microliths. These are the -materials called _Tardenoisian_. The materials of the “Forest folk” of +materials called _Tardenoisian_. The materials of the �Forest folk� of France and central Europe generally are called _Azilian_; Dr. Movius believes the term might best be restricted to the area south of the Loire River. @@ -3032,24 +3032,24 @@ to it than this. Professor Mathiassen of Copenhagen, who knows the archeological remains of this time very well, poses a question. He speaks of the material -as being neither rich nor progressive, in fact “rather stagnant,” but -he goes on to add that the people had a certain “receptiveness” and +as being neither rich nor progressive, in fact �rather stagnant,� but +he goes on to add that the people had a certain �receptiveness� and were able to adapt themselves quickly when the next change did come. -My own understanding of the situation is that the “Forest folk” made +My own understanding of the situation is that the �Forest folk� made nothing as spectacular as had the producers of the earlier Magdalenian assemblage and the Franco-Cantabrian art. On the other hand, they _seem_ to have been making many more different kinds of tools for many more different kinds of tasks than had their Ice Age forerunners. I -emphasize “seem” because the preservation in the Maglemosian bogs +emphasize �seem� because the preservation in the Maglemosian bogs is very complete; certainly we cannot list anywhere near as many different things for earlier times as we did for the Maglemosians (p. 94). I believe this experimentation with all kinds of new tools and gadgets, this intensification of adaptiveness (p. 91), this -“receptiveness,” even if it is still only pointed toward hunting, +�receptiveness,� even if it is still only pointed toward hunting, fishing, and food-collecting, is an important thing. Remember that the only marker we have handy for the _beginning_ of -this tendency toward “receptiveness” and experimentation is the +this tendency toward �receptiveness� and experimentation is the little microlithic blade tools of various geometric forms. These, we saw, began before the last ice had melted away, and they lasted on in use for a very long time. I wish there were a better marker than @@ -3063,7 +3063,7 @@ CHANGES IN OTHER AREAS? All this last section was about Europe. How about the rest of the world when the last glaciers were melting away? -We simply don’t know much about this particular time in other parts +We simply don�t know much about this particular time in other parts of the world except in Europe, the Mediterranean basin and the Middle East. People were certainly continuing to move into the New World by way of Siberia and the Bering Strait about this time. But for the @@ -3075,10 +3075,10 @@ clear information. REAL CHANGE AND PRELUDE IN THE NEAR EAST The appearance of the microliths and the developments made by the -“Forest folk” of northwestern Europe also mark an end. They show us +�Forest folk� of northwestern Europe also mark an end. They show us the terminal phase of the old food-collecting way of life. It grows increasingly clear that at about the same time that the Maglemosian and -other “Forest folk” were adapting themselves to hunting, fishing, and +other �Forest folk� were adapting themselves to hunting, fishing, and collecting in new ways to fit the post-glacial environment, something completely new was being made ready in western Asia. @@ -3098,7 +3098,7 @@ simply gathering or collecting it. When their food-production became reasonably effective, people could and did settle down in village-farming communities. With the appearance of the little farming villages, a new way of life was actually under way. Professor Childe -has good reason to speak of the “food-producing revolution,” for it was +has good reason to speak of the �food-producing revolution,� for it was indeed a revolution. @@ -3117,8 +3117,8 @@ before the _how_ and _why_ answers begin to appear. Anthropologically trained archeologists are fascinated with the cultures of men in times of great change. About ten or twelve thousand years ago, the general level of culture in many parts of the world seems to have been ready -for change. In northwestern Europe, we saw that cultures “changed -just enough so that they would not have to change.” We linked this to +for change. In northwestern Europe, we saw that cultures �changed +just enough so that they would not have to change.� We linked this to environmental changes with the coming of post-glacial times. In western Asia, we archeologists can prove that the food-producing @@ -3155,7 +3155,7 @@ living as the Maglemosians did? These are the questions we still have to face. -CULTURAL “RECEPTIVENESS” AND PROMISING ENVIRONMENTS +CULTURAL �RECEPTIVENESS� AND PROMISING ENVIRONMENTS Until the archeologists and the natural scientists--botanists, geologists, zoologists, and general ecologists--have spent many more @@ -3163,15 +3163,15 @@ years on the problem, we shall not have full _how_ and _why_ answers. I do think, however, that we are beginning to understand what to look for. We shall have to learn much more of what makes the cultures of men -“receptive” and experimental. Did change in the environment alone -force it? Was it simply a case of Professor Toynbee’s “challenge and -response?” I cannot believe the answer is quite that simple. Were it -so simple, we should want to know why the change hadn’t come earlier, +�receptive� and experimental. Did change in the environment alone +force it? Was it simply a case of Professor Toynbee�s �challenge and +response?� I cannot believe the answer is quite that simple. Were it +so simple, we should want to know why the change hadn�t come earlier, along with earlier environmental changes. We shall not know the answer, however, until we have excavated the traces of many more cultures of the time in question. We shall doubtless also have to learn more about, and think imaginatively about, the simpler cultures still left today. -The “mechanics” of culture in general will be bound to interest us. +The �mechanics� of culture in general will be bound to interest us. It will also be necessary to learn much more of the environments of 10,000 to 12,000 years ago. In which regions of the world were the @@ -3228,7 +3228,7 @@ THE OLD THEORY TOO SIMPLE FOR THE FACTS This theory was set up before we really knew anything in detail about the later prehistory of the Near and Middle East. We now know that -the facts which have been found don’t fit the old theory at all well. +the facts which have been found don�t fit the old theory at all well. Also, I have yet to find an American meteorologist who feels that we know enough about the changes in the weather pattern to say that it can have been so simple and direct. And, of course, the glacial ice which @@ -3238,7 +3238,7 @@ of great alpine glaciers, and long periods of warm weather in between. If the rain belt moved north as the glaciers melted for the last time, it must have moved in the same direction in earlier times. Thus, the forced neighborliness of men, plants, and animals in river valleys and -oases must also have happened earlier. Why didn’t domestication happen +oases must also have happened earlier. Why didn�t domestication happen earlier, then? Furthermore, it does not seem to be in the oases and river valleys @@ -3275,20 +3275,20 @@ archeologists, probably through habit, favor an old scheme of Grecized names for the subdivisions: paleolithic, mesolithic, neolithic. I refuse to use these words myself. They have meant too many different things to too many different people and have tended to hide some pretty -fuzzy thinking. Probably you haven’t even noticed my own scheme of -subdivision up to now, but I’d better tell you in general what it is. +fuzzy thinking. Probably you haven�t even noticed my own scheme of +subdivision up to now, but I�d better tell you in general what it is. I think of the earliest great group of archeological materials, from which we can deduce only a food-gathering way of culture, as the -_food-gathering stage_. I say “stage” rather than “age,” because it +_food-gathering stage_. I say �stage� rather than �age,� because it is not quite over yet; there are still a few primitive people in out-of-the-way parts of the world who remain in the _food-gathering stage_. In fact, Professor Julian Steward would probably prefer to call it a food-gathering _level_ of existence, rather than a stage. This would be perfectly acceptable to me. I also tend to find myself using _collecting_, rather than _gathering_, for the more recent aspects or -era of the stage, as the word “collecting” appears to have more sense -of purposefulness and specialization than does “gathering” (see p. +era of the stage, as the word �collecting� appears to have more sense +of purposefulness and specialization than does �gathering� (see p. 91). Now, while I think we could make several possible subdivisions of the @@ -3297,22 +3297,22 @@ believe the only one which means much to us here is the last or _terminal sub-era of food-collecting_ of the whole food-gathering stage. The microliths seem to mark its approach in the northwestern part of the Old World. It is really shown best in the Old World by -the materials of the “Forest folk,” the cultural adaptation to the +the materials of the �Forest folk,� the cultural adaptation to the post-glacial environment in northwestern Europe. We talked about -the “Forest folk” at the beginning of this chapter, and I used the +the �Forest folk� at the beginning of this chapter, and I used the Maglemosian assemblage of Denmark as an example. [5] It is difficult to find words which have a sequence or gradation of meaning with respect to both development and a range of time in the past, or with a range of time from somewhere in the past which is perhaps not yet ended. One standard Webster definition - of _stage_ is: “One of the steps into which the material - development of man ... is divided.” I cannot find any dictionary + of _stage_ is: �One of the steps into which the material + development of man ... is divided.� I cannot find any dictionary definition that suggests which of the words, _stage_ or _era_, has the meaning of a longer span of time. Therefore, I have chosen to let my eras be shorter, and to subdivide my stages - into eras. Webster gives _era_ as: “A signal stage of history, - an epoch.” When I want to subdivide my eras, I find myself using + into eras. Webster gives _era_ as: �A signal stage of history, + an epoch.� When I want to subdivide my eras, I find myself using _sub-eras_. Thus I speak of the _eras_ within a _stage_ and of the _sub-eras_ within an _era_; that is, I do so when I feel that I really have to, and when the evidence is clear enough to @@ -3328,9 +3328,9 @@ realms of culture. It is rather that for most of prehistoric time the materials left to the archeologists tend to limit our deductions to technology and economics. -I’m so soon out of my competence, as conventional ancient history +I�m so soon out of my competence, as conventional ancient history begins, that I shall only suggest the earlier eras of the -food-producing stage to you. This book is about prehistory, and I’m not +food-producing stage to you. This book is about prehistory, and I�m not a universal historian. @@ -3339,28 +3339,28 @@ THE TWO EARLIEST ERAS OF THE FOOD-PRODUCING STAGE The food-producing stage seems to appear in western Asia with really revolutionary suddenness. It is seen by the relative speed with which the traces of new crafts appear in the earliest village-farming -community sites we’ve dug. It is seen by the spread and multiplication +community sites we�ve dug. It is seen by the spread and multiplication of these sites themselves, and the remarkable growth in human -population we deduce from this increase in sites. We’ll look at some +population we deduce from this increase in sites. We�ll look at some of these sites and the archeological traces they yield in the next chapter. When such village sites begin to appear, I believe we are in the _era of the primary village-farming community_. I also believe this is the second era of the food-producing stage. The first era of the food-producing stage, I believe, was an _era of -incipient cultivation and animal domestication_. I keep saying “I -believe” because the actual evidence for this earlier era is so slight +incipient cultivation and animal domestication_. I keep saying �I +believe� because the actual evidence for this earlier era is so slight that one has to set it up mainly by playing a hunch for it. The reason for playing the hunch goes about as follows. One thing we seem to be able to see, in the food-collecting era in general, is a tendency for people to begin to settle down. This settling down seemed to become further intensified in the terminal -era. How this is connected with Professor Mathiassen’s “receptiveness” +era. How this is connected with Professor Mathiassen�s �receptiveness� and the tendency to be experimental, we do not exactly know. The evidence from the New World comes into play here as well as that from the Old World. With this settling down in one place, the people of the -terminal era--especially the “Forest folk” whom we know best--began +terminal era--especially the �Forest folk� whom we know best--began making a great variety of new things. I remarked about this earlier in the chapter. Dr. Robert M. Adams is of the opinion that this atmosphere of experimentation with new tools--with new ways of collecting food--is @@ -3368,9 +3368,9 @@ the kind of atmosphere in which one might expect trials at planting and at animal domestication to have been made. We first begin to find traces of more permanent life in outdoor camp sites, although caves were still inhabited at the beginning of the terminal era. It is not -surprising at all that the “Forest folk” had already domesticated the +surprising at all that the �Forest folk� had already domesticated the dog. In this sense, the whole era of food-collecting was becoming ready -and almost “incipient” for cultivation and animal domestication. +and almost �incipient� for cultivation and animal domestication. Northwestern Europe was not the place for really effective beginnings in agriculture and animal domestication. These would have had to take @@ -3425,13 +3425,13 @@ zone which surrounds the drainage basin of the Tigris and Euphrates Rivers at elevations of from approximately 2,000 to 5,000 feet. The lower alluvial land of the Tigris-Euphrates basin itself has very little rainfall. Some years ago Professor James Henry Breasted called -the alluvial lands of the Tigris-Euphrates a part of the “fertile -crescent.” These alluvial lands are very fertile if irrigated. Breasted +the alluvial lands of the Tigris-Euphrates a part of the �fertile +crescent.� These alluvial lands are very fertile if irrigated. Breasted was most interested in the oriental civilizations of conventional ancient history, and irrigation had been discovered before they appeared. -The country of hilly flanks above Breasted’s crescent receives from +The country of hilly flanks above Breasted�s crescent receives from 10 to 20 or more inches of winter rainfall each year, which is about what Kansas has. Above the hilly-flanks zone tower the peaks and ridges of the Lebanon-Amanus chain bordering the coast-line from Palestine @@ -3440,7 +3440,7 @@ range of the Iraq-Iran borderland. This rugged mountain frame for our hilly-flanks zone rises to some magnificent alpine scenery, with peaks of from ten to fifteen thousand feet in elevation. There are several gaps in the Mediterranean coastal portion of the frame, through which -the winter’s rain-bearing winds from the sea may break so as to carry +the winter�s rain-bearing winds from the sea may break so as to carry rain to the foothills of the Taurus and the Zagros. The picture I hope you will have from this description is that of an @@ -3482,7 +3482,7 @@ hilly-flanks zone in their wild state. With a single exception--that of the dog--the earliest positive evidence of domestication includes the two forms of wheat, the barley, and the goat. The evidence comes from within the hilly-flanks zone. -However, it comes from a settled village proper, Jarmo (which I’ll +However, it comes from a settled village proper, Jarmo (which I�ll describe in the next chapter), and is thus from the era of the primary village-farming community. We are still without positive evidence of domesticated grain and animals in the first era of the food-producing @@ -3534,9 +3534,9 @@ and the spread of ideas of people who had passed on into one of the more developed eras. In many cases, the terminal era of food-collecting was ended by the incoming of the food-producing peoples themselves. For example, the practices of food-production were carried into Europe -by the actual movement of some numbers of peoples (we don’t know how +by the actual movement of some numbers of peoples (we don�t know how many) who had reached at least the level of the primary village-farming -community. The “Forest folk” learned food-production from them. There +community. The �Forest folk� learned food-production from them. There was never an era of incipient cultivation and domestication proper in Europe, if my hunch is right. @@ -3547,16 +3547,16 @@ The way I see it, two things were required in order that an era of incipient cultivation and domestication could begin. First, there had to be the natural environment of a nuclear area, with its whole group of plants and animals capable of domestication. This is the aspect of -the matter which we’ve said is directly given by nature. But it is +the matter which we�ve said is directly given by nature. But it is quite possible that such an environment with such a group of plants and animals in it may have existed well before ten thousand years ago in the Near East. It is also quite possible that the same promising condition may have existed in regions which never developed into nuclear areas proper. Here, again, we come back to the cultural factor. -I think it was that “atmosphere of experimentation” we’ve talked about -once or twice before. I can’t define it for you, other than to say that +I think it was that �atmosphere of experimentation� we�ve talked about +once or twice before. I can�t define it for you, other than to say that by the end of the Ice Age, the general level of many cultures was ready -for change. Ask me how and why this was so, and I’ll tell you we don’t +for change. Ask me how and why this was so, and I�ll tell you we don�t know yet, and that if we did understand this kind of question, there would be no need for me to go on being a prehistorian! @@ -3590,7 +3590,7 @@ such collections for the modern wild forms of animals and plants from some of our nuclear areas. In the nuclear area in the Near East, some of the wild animals, at least, have already become extinct. There are no longer wild cattle or wild horses in western Asia. We know they were -there from the finds we’ve made in caves of late Ice Age times, and +there from the finds we�ve made in caves of late Ice Age times, and from some slightly later sites. @@ -3601,7 +3601,7 @@ incipient era of cultivation and animal domestication. I am closing this chapter with descriptions of two of the best Near Eastern examples I know of. You may not be satisfied that what I am able to describe makes a full-bodied era of development at all. Remember, however, that -I’ve told you I’m largely playing a kind of a hunch, and also that the +I�ve told you I�m largely playing a kind of a hunch, and also that the archeological materials of this era will always be extremely difficult to interpret. At the beginning of any new way of life, there will be a great tendency for people to make-do, at first, with tools and habits @@ -3613,7 +3613,7 @@ THE NATUFIAN, AN ASSEMBLAGE OF THE INCIPIENT ERA The assemblage called the Natufian comes from the upper layers of a number of caves in Palestine. Traces of its flint industry have also -turned up in Syria and Lebanon. We don’t know just how old it is. I +turned up in Syria and Lebanon. We don�t know just how old it is. I guess that it probably falls within five hundred years either way of about 5000 B.C. @@ -3662,7 +3662,7 @@ pendants. There were also beads and pendants of pierced teeth and shell. A number of Natufian burials have been found in the caves; some burials were grouped together in one grave. The people who were buried within the Mount Carmel cave were laid on their backs in an extended position, -while those on the terrace seem to have been “flexed” (placed in their +while those on the terrace seem to have been �flexed� (placed in their graves in a curled-up position). This may mean no more than that it was easier to dig a long hole in cave dirt than in the hard-packed dirt of the terrace. The people often had some kind of object buried with them, @@ -3679,7 +3679,7 @@ beads. GROUND STONE BONE] -The animal bones of the Natufian layers show beasts of a “modern” type, +The animal bones of the Natufian layers show beasts of a �modern� type, but with some differences from those of present-day Palestine. The bones of the gazelle far outnumber those of the deer; since gazelles like a much drier climate than deer, Palestine must then have had much @@ -3692,9 +3692,9 @@ Maglemosian of northern Europe. More recently, it has been reported that a domesticated goat is also part of the Natufian finds. The study of the human bones from the Natufian burials is not yet -complete. Until Professor McCown’s study becomes available, we may note -Professor Coon’s assessment that these people were of a “basically -Mediterranean type.” +complete. Until Professor McCown�s study becomes available, we may note +Professor Coon�s assessment that these people were of a �basically +Mediterranean type.� THE KARIM SHAHIR ASSEMBLAGE @@ -3704,11 +3704,11 @@ of a temporary open site or encampment. It lies on the top of a bluff in the Kurdish hill-country of northeastern Iraq. It was dug by Dr. Bruce Howe of the expedition I directed in 1950-51 for the Oriental Institute and the American Schools of Oriental Research. In 1954-55, -our expedition located another site, M’lefaat, with general resemblance +our expedition located another site, M�lefaat, with general resemblance to Karim Shahir, but about a hundred miles north of it. In 1956, Dr. Ralph Solecki located still another Karim Shahir type of site called Zawi Chemi Shanidar. The Zawi Chemi site has a radiocarbon date of 8900 -± 300 B.C. +� 300 B.C. Karim Shahir has evidence of only one very shallow level of occupation. It was probably not lived on very long, although the people who lived @@ -3717,7 +3717,7 @@ layer yielded great numbers of fist-sized cracked pieces of limestone, which had been carried up from the bed of a stream at the bottom of the bluff. We think these cracked stones had something to do with a kind of architecture, but we were unable to find positive traces of hut plans. -At M’lefaat and Zawi Chemi, there were traces of rounded hut plans. +At M�lefaat and Zawi Chemi, there were traces of rounded hut plans. As in the Natufian, the great bulk of small objects of the Karim Shahir assemblage was in chipped flint. A large proportion of the flint tools @@ -3737,7 +3737,7 @@ clay figurines which seemed to be of animal form. UNBAKED CLAY SHELL BONE - “ARCHITECTURE”] + �ARCHITECTURE�] Karim Shahir did not yield direct evidence of the kind of vegetable food its people ate. The animal bones showed a considerable @@ -3746,7 +3746,7 @@ domestication--sheep, goat, cattle, horse, dog--as compared with animal bones from the earlier cave sites of the area, which have a high proportion of bones of wild forms like deer and gazelle. But we do not know that any of the Karim Shahir animals were actually domesticated. -Some of them may have been, in an “incipient” way, but we have no means +Some of them may have been, in an �incipient� way, but we have no means at the moment that will tell us from the bones alone. @@ -3761,7 +3761,7 @@ goat, and the general animal situation at Karim Shahir to hint at an incipient approach to food-production. At Karim Shahir, there was the tendency to settle down out in the open; this is echoed by the new reports of open air Natufian sites. The large number of cracked stones -certainly indicates that it was worth the peoples’ while to have some +certainly indicates that it was worth the peoples� while to have some kind of structure, even if the site as a whole was short-lived. It is a part of my hunch that these things all point toward @@ -3771,13 +3771,13 @@ which we shall look at next, are fully food-producing, the Natufian and Karim Shahir folk had not yet arrived. I think they were part of a general build-up to full scale food-production. They were possibly controlling a few animals of several kinds and perhaps one or two -plants, without realizing the full possibilities of this “control” as a +plants, without realizing the full possibilities of this �control� as a new way of life. This is why I think of the Karim Shahir and Natufian folk as being at a level, or in an era, of incipient cultivation and domestication. But we shall have to do a great deal more excavation in this range of time -before we’ll get the kind of positive information we need. +before we�ll get the kind of positive information we need. SUMMARY @@ -3798,7 +3798,7 @@ history. We know the earliest village-farming communities appeared in western Asia, in a nuclear area. We do not yet know why the Near Eastern -experiment came first, or why it didn’t happen earlier in some other +experiment came first, or why it didn�t happen earlier in some other nuclear area. Apparently, the level of culture and the promise of the natural environment were ready first in western Asia. The next sites we look at will show a simple but effective food-production already @@ -3835,7 +3835,7 @@ contrast between food-collecting and food-producing as ways of life. THE DIFFERENCE BETWEEN FOOD-COLLECTORS AND FOOD-PRODUCERS -Childe used the word “revolution” because of the radical change that +Childe used the word �revolution� because of the radical change that took place in the habits and customs of man. Food-collectors--that is, hunters, fishers, berry- and nut-gatherers--had to live in small groups or bands, for they had to be ready to move wherever their food supply @@ -3851,7 +3851,7 @@ for clothing beyond the tools that were probably used to dress the skins of animals; no time to think of much of anything but food and protection and disposal of the dead when death did come: an existence which takes nature as it finds it, which does little or nothing to -modify nature--all in all, a savage’s existence, and a very tough one. +modify nature--all in all, a savage�s existence, and a very tough one. A man who spends his whole life following animals just to kill them to eat, or moving from one berry patch to another, is really living just like an animal himself. @@ -3859,10 +3859,10 @@ like an animal himself. THE FOOD-PRODUCING ECONOMY -Against this picture let me try to draw another--that of man’s life -after food-production had begun. His meat was stored “on the hoof,” +Against this picture let me try to draw another--that of man�s life +after food-production had begun. His meat was stored �on the hoof,� his grain in silos or great pottery jars. He lived in a house: it was -worth his while to build one, because he couldn’t move far from his +worth his while to build one, because he couldn�t move far from his fields and flocks. In his neighborhood enough food could be grown and enough animals bred so that many people were kept busy. They all lived close to their flocks and fields, in a village. The village was @@ -3872,7 +3872,7 @@ Children and old men could shepherd the animals by day or help with the lighter work in the fields. After the crops had been harvested the younger men might go hunting and some of them would fish, but the food they brought in was only an addition to the food in the village; the -villagers wouldn’t starve, even if the hunters and fishermen came home +villagers wouldn�t starve, even if the hunters and fishermen came home empty-handed. There was more time to do different things, too. They began to modify @@ -3885,23 +3885,23 @@ people in the village who were becoming full-time craftsmen. Other things were changing, too. The villagers must have had to agree on new rules for living together. The head man of the village had problems different from those of the chief of the small -food-collectors’ band. If somebody’s flock of sheep spoiled a wheat +food-collectors� band. If somebody�s flock of sheep spoiled a wheat field, the owner wanted payment for the grain he lost. The chief of the hunters was never bothered with such questions. Even the gods had changed. The spirits and the magic that had been used by hunters -weren’t of any use to the villagers. They needed gods who would watch +weren�t of any use to the villagers. They needed gods who would watch over the fields and the flocks, and they eventually began to erect buildings where their gods might dwell, and where the men who knew most about the gods might live. -WAS FOOD-PRODUCTION A “REVOLUTION”? +WAS FOOD-PRODUCTION A �REVOLUTION�? If you can see the difference between these two pictures--between life in the food-collecting stage and life after food-production -had begun--you’ll see why Professor Childe speaks of a revolution. -By revolution, he doesn’t mean that it happened over night or that -it happened only once. We don’t know exactly how long it took. Some +had begun--you�ll see why Professor Childe speaks of a revolution. +By revolution, he doesn�t mean that it happened over night or that +it happened only once. We don�t know exactly how long it took. Some people think that all these changes may have occurred in less than 500 years, but I doubt that. The incipient era was probably an affair of some duration. Once the level of the village-farming community had @@ -3915,7 +3915,7 @@ been achieved with truly revolutionary suddenness. GAPS IN OUR KNOWLEDGE OF THE NEAR EAST -If you’ll look again at the chart (p. 111) you’ll see that I have +If you�ll look again at the chart (p. 111) you�ll see that I have very few sites and assemblages to name in the incipient era of cultivation and domestication, and not many in the earlier part of the primary village-farming level either. Thanks in no small part @@ -3926,20 +3926,20 @@ yard-stick here. But I am far from being able to show you a series of Sears Roebuck catalogues, even century by century, for any part of the nuclear area. There is still a great deal of earth to move, and a great mass of material to recover and interpret before we even begin to -understand “how” and “why.” +understand �how� and �why.� Perhaps here, because this kind of archeology is really my specialty, -you’ll excuse it if I become personal for a moment. I very much look +you�ll excuse it if I become personal for a moment. I very much look forward to having further part in closing some of the gaps in knowledge -of the Near East. This is not, as I’ve told you, the spectacular +of the Near East. This is not, as I�ve told you, the spectacular range of Near Eastern archeology. There are no royal tombs, no gold, no great buildings or sculpture, no writing, in fact nothing to excite the normal museum at all. Nevertheless it is a range which, idea-wise, gives the archeologist tremendous satisfaction. The country of the hilly flanks is an exciting combination of green grasslands and mountainous ridges. The Kurds, who inhabit the part of the area -in which I’ve worked most recently, are an extremely interesting and -hospitable people. Archeologists don’t become rich, but I’ll forego +in which I�ve worked most recently, are an extremely interesting and +hospitable people. Archeologists don�t become rich, but I�ll forego the Cadillac for any bright spring morning in the Kurdish hills, on a good site with a happy crew of workmen and an interested and efficient staff. It is probably impossible to convey the full feeling which life @@ -3965,15 +3965,15 @@ like the use of pottery borrowed from the more developed era of the same time in the nuclear area. The same general explanation doubtless holds true for certain materials in Egypt, along the upper Nile and in the Kharga oasis: these materials, called Sebilian III, the Khartoum -“neolithic,” and the Khargan microlithic, are from surface sites, +�neolithic,� and the Khargan microlithic, are from surface sites, not from caves. The chart (p. 111) shows where I would place these materials in era and time. [Illustration: THE HILLY FLANKS OF THE CRESCENT AND EARLY SITES OF THE NEAR EAST] -Both M’lefaat and Dr. Solecki’s Zawi Chemi Shanidar site appear to have -been slightly more “settled in” than was Karim Shahir itself. But I do +Both M�lefaat and Dr. Solecki�s Zawi Chemi Shanidar site appear to have +been slightly more �settled in� than was Karim Shahir itself. But I do not think they belong to the era of farming-villages proper. The first site of this era, in the hills of Iraqi Kurdistan, is Jarmo, on which we have spent three seasons of work. Following Jarmo comes a variety of @@ -3989,9 +3989,9 @@ times when their various cultures flourished, there must have been many little villages which shared the same general assemblage. We are only now beginning to locate them again. Thus, if I speak of Jarmo, or Jericho, or Sialk as single examples of their particular kinds of -assemblages, I don’t mean that they were unique at all. I think I could +assemblages, I don�t mean that they were unique at all. I think I could take you to the sites of at least three more Jarmos, within twenty -miles of the original one. They are there, but they simply haven’t yet +miles of the original one. They are there, but they simply haven�t yet been excavated. In 1956, a Danish expedition discovered material of Jarmo type at Shimshara, only two dozen miles northeast of Jarmo, and below an assemblage of Hassunan type (which I shall describe presently). @@ -4000,15 +4000,15 @@ below an assemblage of Hassunan type (which I shall describe presently). THE GAP BETWEEN KARIM SHAHIR AND JARMO As we see the matter now, there is probably still a gap in the -available archeological record between the Karim Shahir-M’lefaat-Zawi +available archeological record between the Karim Shahir-M�lefaat-Zawi Chemi group (of the incipient era) and that of Jarmo (of the village-farming era). Although some items of the Jarmo type materials do reflect the beginnings of traditions set in the Karim Shahir group (see p. 120), there is not a clear continuity. Moreover--to the degree that we may trust a few radiocarbon dates--there would appear to be around two thousand years of difference in time. The single -available Zawi Chemi “date” is 8900 ± 300 B.C.; the most reasonable -group of “dates” from Jarmo average to about 6750 ± 200 B.C. I am +available Zawi Chemi �date� is 8900 � 300 B.C.; the most reasonable +group of �dates� from Jarmo average to about 6750 � 200 B.C. I am uncertain about this two thousand years--I do not think it can have been so long. @@ -4021,7 +4021,7 @@ JARMO, IN THE KURDISH HILLS, IRAQ The site of Jarmo has a depth of deposit of about twenty-seven feet, and approximately a dozen layers of architectural renovation and -change. Nevertheless it is a “one period” site: its assemblage remains +change. Nevertheless it is a �one period� site: its assemblage remains essentially the same throughout, although one or two new items are added in later levels. It covers about four acres of the top of a bluff, below which runs a small stream. Jarmo lies in the hill country @@ -4078,7 +4078,7 @@ human beings in clay; one type of human figurine they favored was that of a markedly pregnant woman, probably the expression of some sort of fertility spirit. They provided their house floors with baked-in-place depressions, either as basins or hearths, and later with domed ovens of -clay. As we’ve noted, the houses themselves were of clay or mud; one +clay. As we�ve noted, the houses themselves were of clay or mud; one could almost say they were built up like a house-sized pot. Then, finally, the idea of making portable pottery itself appeared, although I very much doubt that the people of the Jarmo village discovered the @@ -4095,11 +4095,11 @@ over three hundred miles to the north. Already a bulk carrying trade had been established--the forerunner of commerce--and the routes were set by which, in later times, the metal trade was to move. -There are now twelve radioactive carbon “dates” from Jarmo. The most -reasonable cluster of determinations averages to about 6750 ± 200 -B.C., although there is a completely unreasonable range of “dates” +There are now twelve radioactive carbon �dates� from Jarmo. The most +reasonable cluster of determinations averages to about 6750 � 200 +B.C., although there is a completely unreasonable range of �dates� running from 3250 to 9250 B.C.! _If_ I am right in what I take to be -“reasonable,” the first flush of the food-producing revolution had been +�reasonable,� the first flush of the food-producing revolution had been achieved almost nine thousand years ago. @@ -4117,7 +4117,7 @@ it, but the Hassunan sites seem to cluster at slightly lower elevations than those we have been talking about so far. The catalogue of the Hassuna assemblage is of course more full and -elaborate than that of Jarmo. The Iraqi government’s archeologists +elaborate than that of Jarmo. The Iraqi government�s archeologists who dug Hassuna itself, exposed evidence of increasing architectural know-how. The walls of houses were still formed of puddled mud; sun-dried bricks appear only in later periods. There were now several @@ -4130,16 +4130,16 @@ largely disappeared by Hassunan times. The flint work of the Hassunan catalogue is, by and large, a wretched affair. We might guess that the kinaesthetic concentration of the Hassuna craftsmen now went into other categories; that is, they suddenly discovered they might have more fun -working with the newer materials. It’s a shame, for example, that none +working with the newer materials. It�s a shame, for example, that none of their weaving is preserved for us. The two available radiocarbon determinations from Hassunan contexts -stand at about 5100 and 5600 B.C. ± 250 years. +stand at about 5100 and 5600 B.C. � 250 years. OTHER EARLY VILLAGE SITES IN THE NUCLEAR AREA -I’ll now name and very briefly describe a few of the other early +I�ll now name and very briefly describe a few of the other early village assemblages either in or adjacent to the hilly flanks of the crescent. Unfortunately, we do not have radioactive carbon dates for many of these materials. We may guess that some particular assemblage, @@ -4177,7 +4177,7 @@ ecological niche, some seven hundred feet below sea level; it is geographically within the hilly-flanks zone but environmentally not part of it. -Several radiocarbon “dates” for Jericho fall within the range of those +Several radiocarbon �dates� for Jericho fall within the range of those I find reasonable for Jarmo, and their internal statistical consistency is far better than that for the Jarmo determinations. It is not yet clear exactly what this means. @@ -4226,7 +4226,7 @@ how things were made are different; the Sialk assemblage represents still another cultural pattern. I suspect it appeared a bit later in time than did that of Hassuna. There is an important new item in the Sialk catalogue. The Sialk people made small drills or pins of -hammered copper. Thus the metallurgist’s specialized craft had made its +hammered copper. Thus the metallurgist�s specialized craft had made its appearance. There is at least one very early Iranian site on the inward slopes @@ -4246,7 +4246,7 @@ shore of the Fayum lake. The Fayum materials come mainly from grain bins or silos. Another site, Merimde, in the western part of the Nile delta, shows the remains of a true village, but it may be slightly later than the settlement of the Fayum. There are radioactive carbon -“dates” for the Fayum materials at about 4275 B.C. ± 320 years, which +�dates� for the Fayum materials at about 4275 B.C. � 320 years, which is almost fifteen hundred years later than the determinations suggested for the Hassunan or Syro-Cilician assemblages. I suspect that this is a somewhat over-extended indication of the time it took for the @@ -4260,13 +4260,13 @@ the mound called Shaheinab. The Shaheinab catalogue roughly corresponds to that of the Fayum; the distance between the two places, as the Nile flows, is roughly 1,500 miles. Thus it took almost a thousand years for the new way of life to be carried as far south into Africa as Khartoum; -the two Shaheinab “dates” average about 3300 B.C. ± 400 years. +the two Shaheinab �dates� average about 3300 B.C. � 400 years. If the movement was up the Nile (southward), as these dates suggest, then I suspect that the earliest available village material of middle Egypt, the so-called Tasian, is also later than that of the Fayum. The Tasian materials come from a few graves near a village called Deir -Tasa, and I have an uncomfortable feeling that the Tasian “assemblage” +Tasa, and I have an uncomfortable feeling that the Tasian �assemblage� may be mainly an artificial selection of poor examples of objects which belong in the following range of time. @@ -4280,7 +4280,7 @@ spread outward in space from the nuclear area, as time went on. There is good archeological evidence that both these processes took place. For the hill country of northeastern Iraq, in the nuclear area, we have already noticed how the succession (still with gaps) from Karim -Shahir, through M’lefaat and Jarmo, to Hassuna can be charted (see +Shahir, through M�lefaat and Jarmo, to Hassuna can be charted (see chart, p. 111). In the next chapter, we shall continue this charting and description of what happened in Iraq upward through time. We also watched traces of the new way of life move through space up the Nile @@ -4299,7 +4299,7 @@ appearance of the village-farming community there--is still an open one. In the last chapter, we noted the probability of an independent nuclear area in southeastern Asia. Professor Carl Sauer strongly champions the great importance of this area as _the_ original center -of agricultural pursuits, as a kind of “cradle” of all incipient eras +of agricultural pursuits, as a kind of �cradle� of all incipient eras of the Old World at least. While there is certainly not the slightest archeological evidence to allow us to go that far, we may easily expect that an early southeast Asian development would have been felt in @@ -4311,13 +4311,13 @@ way of life moved well beyond Khartoum in Africa. THE SPREAD OF THE VILLAGE-FARMING COMMUNITY WAY OF LIFE INTO EUROPE -How about Europe? I won’t give you many details. You can easily imagine +How about Europe? I won�t give you many details. You can easily imagine that the late prehistoric prelude to European history is a complicated affair. We all know very well how complicated an area Europe is now, with its welter of different languages and cultures. Remember, however, that a great deal of archeology has been done on the late prehistory of Europe, and very little on that of further Asia and Africa. If we knew -as much about these areas as we do of Europe, I expect we’d find them +as much about these areas as we do of Europe, I expect we�d find them just as complicated. This much is clear for Europe, as far as the spread of the @@ -4329,21 +4329,21 @@ in western Asia. I do not, of course, mean that there were traveling salesmen who carried these ideas and things to Europe with a commercial gleam in their eyes. The process took time, and the ideas and things must have been passed on from one group of people to the next. There -was also some actual movement of peoples, but we don’t know the size of +was also some actual movement of peoples, but we don�t know the size of the groups that moved. -The story of the “colonization” of Europe by the first farmers is +The story of the �colonization� of Europe by the first farmers is thus one of (1) the movement from the eastern Mediterranean lands of some people who were farmers; (2) the spread of ideas and things beyond the Near East itself and beyond the paths along which the -“colonists” moved; and (3) the adaptations of the ideas and things -by the indigenous “Forest folk”, about whose “receptiveness” Professor +�colonists� moved; and (3) the adaptations of the ideas and things +by the indigenous �Forest folk�, about whose �receptiveness� Professor Mathiassen speaks (p. 97). It is important to note that the resulting cultures in the new European environment were European, not Near -Eastern. The late Professor Childe remarked that “the peoples of the +Eastern. The late Professor Childe remarked that �the peoples of the West were not slavish imitators; they adapted the gifts from the East ... into a new and organic whole capable of developing on its own -original lines.” +original lines.� THE WAYS TO EUROPE @@ -4389,19 +4389,19 @@ Hill, the earliest known trace of village-farming communities in England, is about 2500 B.C. I would expect about 5500 B.C. to be a safe date to give for the well-developed early village communities of Syro-Cilicia. We suspect that the spread throughout Europe did not -proceed at an even rate. Professor Piggott writes that “at a date +proceed at an even rate. Professor Piggott writes that �at a date probably about 2600 B.C., simple agricultural communities were being established in Spain and southern France, and from the latter region a spread northwards can be traced ... from points on the French seaboard of the [English] Channel ... there were emigrations of a certain number of these tribes by boat, across to the chalk lands of Wessex and Sussex [in England], probably not more than three or four generations later -than the formation of the south French colonies.” +than the formation of the south French colonies.� New radiocarbon determinations are becoming available all the time--already several suggest that the food-producing way of life had reached the lower Rhine and Holland by 4000 B.C. But not all -prehistorians accept these “dates,” so I do not show them on my map +prehistorians accept these �dates,� so I do not show them on my map (p. 139). @@ -4427,7 +4427,7 @@ concentric sets of banks and ditches. Traces of oblong timber houses have been found, but not within the enclosures. The second type of structure is mine-shafts, dug down into the chalk beds where good flint for the making of axes or hoes could be found. The third type -of structure is long simple mounds or “unchambered barrows,” in one +of structure is long simple mounds or �unchambered barrows,� in one end of which burials were made. It has been commonly believed that the Windmill Hill assemblage belonged entirely to the cultural tradition which moved up through France to the Channel. Professor Piggott is now @@ -4443,12 +4443,12 @@ consists mainly of tombs and the contents of tombs, with only very rare settlement sites. The tombs were of some size and received the bodies of many people. The tombs themselves were built of stone, heaped over with earth; the stones enclosed a passage to a central chamber -(“passage graves”), or to a simple long gallery, along the sides of -which the bodies were laid (“gallery graves”). The general type of -construction is called “megalithic” (= great stone), and the whole +(�passage graves�), or to a simple long gallery, along the sides of +which the bodies were laid (�gallery graves�). The general type of +construction is called �megalithic� (= great stone), and the whole earth-mounded structure is often called a _barrow_. Since many have -proper chambers, in one sense or another, we used the term “unchambered -barrow” above to distinguish those of the Windmill Hill type from these +proper chambers, in one sense or another, we used the term �unchambered +barrow� above to distinguish those of the Windmill Hill type from these megalithic structures. There is some evidence for sacrifice, libations, and ceremonial fires, and it is clear that some form of community ritual was focused on the megalithic tombs. @@ -4466,7 +4466,7 @@ The third early British group of antiquities of this general time It is not so certain that the people who made this assemblage, called Peterborough, were actually farmers. While they may on occasion have practiced a simple agriculture, many items of their assemblage link -them closely with that of the “Forest folk” of earlier times in +them closely with that of the �Forest folk� of earlier times in England and in the Baltic countries. Their pottery is decorated with impressions of cords and is quite different from that of Windmill Hill and the megalithic builders. In addition, the distribution of their @@ -4479,7 +4479,7 @@ to acquire the raw material for stone axes. A probably slightly later culture, whose traces are best known from Skara Brae on Orkney, also had its roots in those cultures of the -Baltic area which fused out of the meeting of the “Forest folk” and +Baltic area which fused out of the meeting of the �Forest folk� and the peoples who took the eastern way into Europe. Skara Brae is very well preserved, having been built of thin stone slabs about which dune-sand drifted after the village died. The individual houses, the @@ -4498,14 +4498,14 @@ details which I have omitted in order to shorten the story. I believe some of the difficulty we have in understanding the establishment of the first farming communities in Europe is with -the word “colonization.” We have a natural tendency to think of -“colonization” as it has happened within the last few centuries. In the +the word �colonization.� We have a natural tendency to think of +�colonization� as it has happened within the last few centuries. In the case of the colonization of the Americas, for example, the colonists came relatively quickly, and in increasingly vast numbers. They had vastly superior technical, political, and war-making skills, compared with those of the Indians. There was not much mixing with the Indians. The case in Europe five or six thousand years ago must have been very -different. I wonder if it is even proper to call people “colonists” +different. I wonder if it is even proper to call people �colonists� who move some miles to a new region, settle down and farm it for some years, then move on again, generation after generation? The ideas and the things which these new people carried were only _potentially_ @@ -4521,12 +4521,12 @@ migrants were moving by boat, long distances may have been covered in a short time. Remember, however, we seem to have about three thousand years between the early Syro-Cilician villages and Windmill Hill. -Let me repeat Professor Childe again. “The peoples of the West were +Let me repeat Professor Childe again. �The peoples of the West were not slavish imitators: they adapted the gifts from the East ... into a new and organic whole capable of developing on its own original -lines.” Childe is of course completely conscious of the fact that his -“peoples of the West” were in part the descendants of migrants who came -originally from the “East,” bringing their “gifts” with them. This +lines.� Childe is of course completely conscious of the fact that his +�peoples of the West� were in part the descendants of migrants who came +originally from the �East,� bringing their �gifts� with them. This was the late prehistoric achievement of Europe--to take new ideas and things and some migrant peoples and, by mixing them with the old in its own environments, to forge a new and unique series of cultures. @@ -4553,14 +4553,14 @@ things first happened there and also because I know it best. There is another interesting thing, too. We have seen that the first experiment in village-farming took place in the Near East. So did -the first experiment in civilization. Both experiments “took.” The +the first experiment in civilization. Both experiments �took.� The traditions we live by today are based, ultimately, on those ancient beginnings in food-production and civilization in the Near East. -WHAT “CIVILIZATION” MEANS +WHAT �CIVILIZATION� MEANS -I shall not try to define “civilization” for you; rather, I shall +I shall not try to define �civilization� for you; rather, I shall tell you what the word brings to my mind. To me civilization means urbanization: the fact that there are cities. It means a formal political set-up--that there are kings or governing bodies that the @@ -4606,7 +4606,7 @@ of Mexico, the Mayas of Yucatan and Guatemala, and the Incas of the Andes were civilized. -WHY DIDN’T CIVILIZATION COME TO ALL FOOD-PRODUCERS? +WHY DIDN�T CIVILIZATION COME TO ALL FOOD-PRODUCERS? Once you have food-production, even at the well-advanced level of the village-farming community, what else has to happen before you @@ -4625,13 +4625,13 @@ early civilization, is still an open and very interesting question. WHERE CIVILIZATION FIRST APPEARED IN THE NEAR EAST You remember that our earliest village-farming communities lay along -the hilly flanks of a great “crescent.” (See map on p. 125.) -Professor Breasted’s “fertile crescent” emphasized the rich river +the hilly flanks of a great �crescent.� (See map on p. 125.) +Professor Breasted�s �fertile crescent� emphasized the rich river valleys of the Nile and the Tigris-Euphrates Rivers. Our hilly-flanks area of the crescent zone arches up from Egypt through Palestine and Syria, along southern Turkey into northern Iraq, and down along the southwestern fringe of Iran. The earliest food-producing villages we -know already existed in this area by about 6750 B.C. (± 200 years). +know already existed in this area by about 6750 B.C. (� 200 years). Now notice that this hilly-flanks zone does not include southern Mesopotamia, the alluvial land of the lower Tigris and Euphrates in @@ -4639,7 +4639,7 @@ Iraq, or the Nile Valley proper. The earliest known villages of classic Mesopotamia and Egypt seem to appear fifteen hundred or more years after those of the hilly-flanks zone. For example, the early Fayum village which lies near a lake west of the Nile Valley proper (see p. -135) has a radiocarbon date of 4275 B.C. ± 320 years. It was in the +135) has a radiocarbon date of 4275 B.C. � 320 years. It was in the river lands, however, that the immediate beginnings of civilization were made. @@ -4657,8 +4657,8 @@ THE HILLY-FLANKS ZONE VERSUS THE RIVER LANDS Why did these two civilizations spring up in these two river lands which apparently were not even part of the area where the -village-farming community began? Why didn’t we have the first -civilizations in Palestine, Syria, north Iraq, or Iran, where we’re +village-farming community began? Why didn�t we have the first +civilizations in Palestine, Syria, north Iraq, or Iran, where we�re sure food-production had had a long time to develop? I think the probable answer gives a clue to the ways in which civilization began in Egypt and Mesopotamia. @@ -4669,7 +4669,7 @@ and Syria. There are pleasant mountain slopes, streams running out to the sea, and rain, at least in the winter months. The rain belt and the foothills of the Turkish mountains also extend to northern Iraq and on to the Iranian plateau. The Iranian plateau has its mountain valleys, -streams, and some rain. These hilly flanks of the “crescent,” through +streams, and some rain. These hilly flanks of the �crescent,� through most of its arc, are almost made-to-order for beginning farmers. The grassy slopes of the higher hills would be pasture for their herds and flocks. As soon as the earliest experiments with agriculture and @@ -4720,10 +4720,10 @@ Obviously, we can no longer find the first dikes or reservoirs of the Nile Valley, or the first canals or ditches of Mesopotamia. The same land has been lived on far too long for any traces of the first attempts to be left; or, especially in Egypt, it has been covered by -the yearly deposits of silt, dropped by the river floods. But we’re +the yearly deposits of silt, dropped by the river floods. But we�re pretty sure the first food-producers of Egypt and southern Mesopotamia must have made such dikes, canals, and ditches. In the first place, -there can’t have been enough rain for them to grow things otherwise. +there can�t have been enough rain for them to grow things otherwise. In the second place, the patterns for such projects seem to have been pretty well set by historic times. @@ -4733,10 +4733,10 @@ CONTROL OF THE RIVERS THE BUSINESS OF EVERYONE Here, then, is a _part_ of the reason why civilization grew in Egypt and Mesopotamia first--not in Palestine, Syria, or Iran. In the latter areas, people could manage to produce their food as individuals. It -wasn’t too hard; there were rain and some streams, and good pasturage +wasn�t too hard; there were rain and some streams, and good pasturage for the animals even if a crop or two went wrong. In Egypt and Mesopotamia, people had to put in a much greater amount of work, and -this work couldn’t be individual work. Whole villages or groups of +this work couldn�t be individual work. Whole villages or groups of people had to turn out to fix dikes or dig ditches. The dikes had to be repaired and the ditches carefully cleared of silt each year, or they would become useless. @@ -4745,7 +4745,7 @@ There also had to be hard and fast rules. The person who lived nearest the ditch or the reservoir must not be allowed to take all the water and leave none for his neighbors. It was not only a business of learning to control the rivers and of making their waters do the -farmer’s work. It also meant controlling men. But once these men had +farmer�s work. It also meant controlling men. But once these men had managed both kinds of controls, what a wonderful yield they had! The soil was already fertile, and the silt which came in the floods and ditches kept adding fertile soil. @@ -4756,7 +4756,7 @@ THE GERM OF CIVILIZATION IN EGYPT AND MESOPOTAMIA This learning to work together for the common good was the real germ of the Egyptian and the Mesopotamian civilizations. The bare elements of civilization were already there: the need for a governing hand and for -laws to see that the communities’ work was done and that the water was +laws to see that the communities� work was done and that the water was justly shared. You may object that there is a sort of chicken and egg paradox in this idea. How could the people set up the rules until they had managed to get a way to live, and how could they manage to get a @@ -4781,12 +4781,12 @@ My explanation has been pointed particularly at Egypt and Mesopotamia. I have already told you that the irrigation and water-control part of it does not apply to the development of the Aztecs or the Mayas, or perhaps anybody else. But I think that a fair part of the story of -Egypt and Mesopotamia must be as I’ve just told you. +Egypt and Mesopotamia must be as I�ve just told you. I am particularly anxious that you do _not_ understand me to mean that irrigation _caused_ civilization. I am sure it was not that simple at all. For, in fact, a complex and highly engineered irrigation system -proper did not come until later times. Let’s say rather that the simple +proper did not come until later times. Let�s say rather that the simple beginnings of irrigation allowed and in fact encouraged a great number of things in the technological, political, social, and moral realms of culture. We do not yet understand what all these things were or how @@ -4842,7 +4842,7 @@ the mound which later became the holy Sumerian city of Eridu, Iraqi archeologists uncovered a handsome painted pottery. Pottery of the same type had been noticed earlier by German archeologists on the surface of a small mound, awash in the spring floods, near the remains of the -Biblical city of Erich (Sumerian = Uruk; Arabic = Warka). This “Eridu” +Biblical city of Erich (Sumerian = Uruk; Arabic = Warka). This �Eridu� pottery, which is about all we have of the assemblage of the people who once produced it, may be seen as a blend of the Samarran and Halafian painted pottery styles. This may over-simplify the case, but as yet we @@ -4864,7 +4864,7 @@ seems to move into place before the Halaf manifestation is finished, and to blend with it. The Ubaidian assemblage in the south is by far the more spectacular. The development of the temple has been traced at Eridu from a simple little structure to a monumental building some -62 feet long, with a pilaster-decorated façade and an altar in its +62 feet long, with a pilaster-decorated fa�ade and an altar in its central chamber. There is painted Ubaidian pottery, but the style is hurried and somewhat careless and gives the _impression_ of having been a cheap mass-production means of decoration when compared with the @@ -4879,7 +4879,7 @@ turtle-like faces are another item in the southern Ubaidian assemblage. There is a large Ubaid cemetery at Eridu, much of it still awaiting excavation. The few skeletons so far tentatively studied reveal a -completely modern type of “Mediterraneanoid”; the individuals whom the +completely modern type of �Mediterraneanoid�; the individuals whom the skeletons represent would undoubtedly blend perfectly into the modern population of southern Iraq. What the Ubaidian assemblage says to us is that these people had already adapted themselves and their culture to @@ -4925,7 +4925,7 @@ woven stuffs must have been the mediums of exchange. Over what area did the trading net-work of Ubaid extend? We start with the idea that the Ubaidian assemblage is most richly developed in the south. We assume, I think, correctly, that it represents a cultural flowering of the south. -On the basis of the pottery of the still elusive “Eridu” immigrants +On the basis of the pottery of the still elusive �Eridu� immigrants who had first followed the rivers into alluvial Mesopotamia, we get the notion that the characteristic painted pottery style of Ubaid was developed in the southland. If this reconstruction is correct @@ -4935,7 +4935,7 @@ assemblage of (and from the southern point of view, _fairly_ pure) Ubaidian material in northern Iraq. The pottery appears all along the Iranian flanks, even well east of the head of the Persian Gulf, and ends in a later and spectacular flourish in an extremely handsome -painted style called the “Susa” style. Ubaidian pottery has been noted +painted style called the �Susa� style. Ubaidian pottery has been noted up the valleys of both of the great rivers, well north of the Iraqi and Syrian borders on the southern flanks of the Anatolian plateau. It reaches the Mediterranean Sea and the valley of the Orontes in @@ -4965,10 +4965,10 @@ Mesopotamia. Next, much to our annoyance, we have what is almost a temporary black-out. According to the system of terminology I favor, our next -“assemblage” after that of Ubaid is called the _Warka_ phase, from +�assemblage� after that of Ubaid is called the _Warka_ phase, from the Arabic name for the site of Uruk or Erich. We know it only from six or seven levels in a narrow test-pit at Warka, and from an even -smaller hole at another site. This “assemblage,” so far, is known only +smaller hole at another site. This �assemblage,� so far, is known only by its pottery, some of which still bears Ubaidian style painting. The characteristic Warkan pottery is unpainted, with smoothed red or gray surfaces and peculiar shapes. Unquestionably, there must be a great @@ -4979,7 +4979,7 @@ have to excavate it! THE DAWN OF CIVILIZATION After our exasperation with the almost unknown Warka interlude, -following the brilliant “false dawn” of Ubaid, we move next to an +following the brilliant �false dawn� of Ubaid, we move next to an assemblage which yields traces of a preponderance of those elements which we noted (p. 144) as meaning civilization. This assemblage is that called _Proto-Literate_; it already contains writing. On @@ -4988,8 +4988,8 @@ history--and no longer prehistory--the assemblage is named for the historical implications of its content, and no longer after the name of the site where it was first found. Since some of the older books used site-names for this assemblage, I will tell you that the Proto-Literate -includes the latter half of what used to be called the “Uruk period” -_plus_ all of what used to be called the “Jemdet Nasr period.” It shows +includes the latter half of what used to be called the �Uruk period� +_plus_ all of what used to be called the �Jemdet Nasr period.� It shows a consistent development from beginning to end. I shall, in fact, leave much of the description and the historic @@ -5033,18 +5033,18 @@ mental block seems to have been removed. Clay tablets bearing pictographic signs are the Proto-Literate forerunners of cuneiform writing. The earliest examples are not well -understood but they seem to be “devices for making accounts and -for remembering accounts.” Different from the later case in Egypt, +understood but they seem to be �devices for making accounts and +for remembering accounts.� Different from the later case in Egypt, where writing appears fully formed in the earliest examples, the development from simple pictographic signs to proper cuneiform writing may be traced, step by step, in Mesopotamia. It is most probable that the development of writing was connected with the temple and -the need for keeping account of the temple’s possessions. Professor +the need for keeping account of the temple�s possessions. Professor Jacobsen sees writing as a means for overcoming space, time, and the -increasing complications of human affairs: “Literacy, which began +increasing complications of human affairs: �Literacy, which began with ... civilization, enhanced mightily those very tendencies in its development which characterize it as a civilization and mark it off as -such from other types of culture.” +such from other types of culture.� [Illustration: RELIEF ON A PROTO-LITERATE STONE VASE, WARKA @@ -5098,7 +5098,7 @@ civilized way of life. I suppose you could say that the difference in the approach is that as a prehistorian I have been looking forward or upward in time, while the -historians look backward to glimpse what I’ve been describing here. My +historians look backward to glimpse what I�ve been describing here. My base-line was half a million years ago with a being who had little more than the capacity to make tools and fire to distinguish him from the animals about him. Thus my point of view and that of the conventional @@ -5114,17 +5114,17 @@ End of PREHISTORY [Illustration] -You’ll doubtless easily recall your general course in ancient history: +You�ll doubtless easily recall your general course in ancient history: how the Sumerian dynasties of Mesopotamia were supplanted by those of Babylonia, how the Hittite kingdom appeared in Anatolian Turkey, and about the three great phases of Egyptian history. The literate kingdom of Crete arose, and by 1500 B.C. there were splendid fortified Mycenean towns on the mainland of Greece. This was the time--about the whole eastern end of the Mediterranean--of what Professor Breasted called the -“first great internationalism,” with flourishing trade, international +�first great internationalism,� with flourishing trade, international treaties, and royal marriages between Egyptians, Babylonians, and -Hittites. By 1200 B.C., the whole thing had fragmented: “the peoples of -the sea were restless in their isles,” and the great ancient centers in +Hittites. By 1200 B.C., the whole thing had fragmented: �the peoples of +the sea were restless in their isles,� and the great ancient centers in Egypt, Mesopotamia, and Anatolia were eclipsed. Numerous smaller states arose--Assyria, Phoenicia, Israel--and the Trojan war was fought. Finally Assyria became the paramount power of all the Near East, @@ -5135,7 +5135,7 @@ but casting them with its own tradition into a new mould, arose in mainland Greece. I once shocked my Classical colleagues to the core by referring to -Greece as “a second degree derived civilization,” but there is much +Greece as �a second degree derived civilization,� but there is much truth in this. The principles of bronze- and then of iron-working, of the alphabet, and of many other elements in Greek culture were borrowed from western Asia. Our debt to the Greeks is too well known for me even @@ -5146,7 +5146,7 @@ Greece fell in its turn to Rome, and in 55 B.C. Caesar invaded Britain. I last spoke of Britain on page 142; I had chosen it as my single example for telling you something of how the earliest farming communities were established in Europe. Now I will continue with -Britain’s later prehistory, so you may sense something of the end of +Britain�s later prehistory, so you may sense something of the end of prehistory itself. Remember that Britain is simply a single example we select; the same thing could be done for all the other countries of Europe, and will be possible also, some day, for further Asia and @@ -5186,20 +5186,20 @@ few Battle-axe folk elements, including, in fact, stone battle-axes, reached England with the earliest Beaker folk,[6] coming from the Rhineland. - [6] The British authors use the term “Beaker folk” to mean both + [6] The British authors use the term �Beaker folk� to mean both archeological assemblage and human physical type. They speak - of a “... tall, heavy-boned, rugged, and round-headed” strain + of a �... tall, heavy-boned, rugged, and round-headed� strain which they take to have developed, apparently in the Rhineland, by a mixture of the original (Spanish?) beaker-makers and the northeast European battle-axe makers. However, since the science of physical anthropology is very much in flux at the moment, and since I am not able to assess the evidence for these - physical types, I _do not_ use the term “folk” in this book with + physical types, I _do not_ use the term �folk� in this book with its usual meaning of standardized physical type. When I use - “folk” here, I mean simply _the makers of a given archeological + �folk� here, I mean simply _the makers of a given archeological assemblage_. The difficulty only comes when assemblages are named for some item in them; it is too clumsy to make an - adjective of the item and refer to a “beakerian” assemblage. + adjective of the item and refer to a �beakerian� assemblage. The Beaker folk settled earliest in the agriculturally fertile south and east. There seem to have been several phases of Beaker folk @@ -5211,7 +5211,7 @@ folk are known. They buried their dead singly, sometimes in conspicuous individual barrows with the dead warrior in his full trappings. The spectacular element in the assemblage of the Beaker folk is a group of large circular monuments with ditches and with uprights of wood or -stone. These “henges” became truly monumental several hundred years +stone. These �henges� became truly monumental several hundred years later; while they were occasionally dedicated with a burial, they were not primarily tombs. The effect of the invasion of the Beaker folk seems to cut across the whole fabric of life in Britain. @@ -5221,7 +5221,7 @@ seems to cut across the whole fabric of life in Britain. There was, however, a second major element in British life at this time. It shows itself in the less well understood traces of a group again called after one of the items in their catalogue, the Food-vessel -folk. There are many burials in these “food-vessel” pots in northern +folk. There are many burials in these �food-vessel� pots in northern England, Scotland, and Ireland, and the pottery itself seems to link back to that of the Peterborough assemblage. Like the earlier Peterborough people in the highland zone before them, the makers of @@ -5238,8 +5238,8 @@ MORE INVASIONS About 1500 B.C., the situation became further complicated by the arrival of new people in the region of southern England anciently called Wessex. The traces suggest the Brittany coast of France as a -source, and the people seem at first to have been a small but “heroic” -group of aristocrats. Their “heroes” are buried with wealth and +source, and the people seem at first to have been a small but �heroic� +group of aristocrats. Their �heroes� are buried with wealth and ceremony, surrounded by their axes and daggers of bronze, their gold ornaments, and amber and jet beads. These rich finds show that the trade-linkage these warriors patronized spread from the Baltic sources @@ -5265,10 +5265,10 @@ which must have been necessary before such a great monument could have been built. -“THIS ENGLAND” +�THIS ENGLAND� The range from 1900 to about 1400 B.C. includes the time of development -of the archeological features usually called the “Early Bronze Age” +of the archeological features usually called the �Early Bronze Age� in Britain. In fact, traces of the Wessex warriors persisted down to about 1200 B.C. The main regions of the island were populated, and the adjustments to the highland and lowland zones were distinct and well @@ -5279,7 +5279,7 @@ trading role, separated from the European continent but conveniently adjacent to it. The tin of Cornwall--so important in the production of good bronze--as well as the copper of the west and of Ireland, taken with the gold of Ireland and the general excellence of Irish -metal work, assured Britain a trader’s place in the then known world. +metal work, assured Britain a trader�s place in the then known world. Contacts with the eastern Mediterranean may have been by sea, with Cornish tin as the attraction, or may have been made by the Food-vessel middlemen on their trips to the Baltic coast. There they would have @@ -5292,9 +5292,9 @@ relative isolation gave some peace and also gave time for a leveling and further fusion of culture. The separate cultural traditions began to have more in common. The growing of barley, the herding of sheep and cattle, and the production of woolen garments were already features -common to all Britain’s inhabitants save a few in the remote highlands, +common to all Britain�s inhabitants save a few in the remote highlands, the far north, and the distant islands not yet fully touched by -food-production. The “personality of Britain” was being formed. +food-production. The �personality of Britain� was being formed. CREMATION BURIALS BEGIN @@ -5325,9 +5325,9 @@ which we shall mention below. The British cremation-burial-in-urns folk survived a long time in the highland zone. In the general British scheme, they make up what is -called the “Middle Bronze Age,” but in the highland zone they last +called the �Middle Bronze Age,� but in the highland zone they last until after 900 B.C. and are considered to be a specialized highland -“Late Bronze Age.” In the highland zone, these later cremation-burial +�Late Bronze Age.� In the highland zone, these later cremation-burial folk seem to have continued the older Food-vessel tradition of being middlemen in the metal market. @@ -5379,12 +5379,12 @@ to get a picture of estate or tribal boundaries which included village communities; we find a variety of tools in bronze, and even whetstones which show that iron has been honed on them (although the scarce iron has not been found). Let me give you the picture in Professor S. -Piggott’s words: “The ... Late Bronze Age of southern England was but +Piggott�s words: �The ... Late Bronze Age of southern England was but the forerunner of the earliest Iron Age in the same region, not only in the techniques of agriculture, but almost certainly in terms of ethnic kinship ... we can with some assurance talk of the Celts ... the great early Celtic expansion of the Continent is recognized to be that of the -Urnfield people.” +Urnfield people.� Thus, certainly by 500 B.C., there were people in Britain, some of whose descendants we may recognize today in name or language in remote @@ -5399,11 +5399,11 @@ efficient set of tools than does bronze. Iron tools seem first to have been made in quantity in Hittite Anatolia about 1500 B.C. In continental Europe, the earliest, so-called Hallstatt, iron-using cultures appeared in Germany soon after 750 B.C. Somewhat later, -Greek and especially Etruscan exports of _objets d’art_--which moved +Greek and especially Etruscan exports of _objets d�art_--which moved with a flourishing trans-Alpine wine trade--influenced the Hallstatt iron-working tradition. Still later new classical motifs, together with older Hallstatt, oriental, and northern nomad motifs, gave rise to a -new style in metal decoration which characterizes the so-called La Tène +new style in metal decoration which characterizes the so-called La T�ne phase. A few iron users reached Britain a little before 400 B.C. Not long @@ -5422,7 +5422,7 @@ HILL-FORTS AND FARMS The earliest iron-users seem to have entrenched themselves temporarily within hill-top forts, mainly in the south. Gradually, they moved inland, establishing _individual_ farm sites with extensive systems -of rectangular fields. We recognize these fields by the “lynchets” or +of rectangular fields. We recognize these fields by the �lynchets� or lines of soil-creep which plowing left on the slopes of hills. New crops appeared; there were now bread wheat, oats, and rye, as well as barley. @@ -5434,7 +5434,7 @@ various outbuildings and pits for the storage of grain. Weaving was done on the farm, but not blacksmithing, which must have been a specialized trade. Save for the lack of firearms, the place might almost be taken for a farmstead on the American frontier in the early -1800’s. +1800�s. Toward 250 B.C. there seems to have been a hasty attempt to repair the hill-forts and to build new ones, evidently in response to signs of @@ -5446,9 +5446,9 @@ THE SECOND PHASE Perhaps the hill-forts were not entirely effective or perhaps a compromise was reached. In any case, the newcomers from the Marne district did establish themselves, first in the southeast and then to -the north and west. They brought iron with decoration of the La Tène +the north and west. They brought iron with decoration of the La T�ne type and also the two-wheeled chariot. Like the Wessex warriors of -over a thousand years earlier, they made “heroes’” graves, with their +over a thousand years earlier, they made �heroes�� graves, with their warriors buried in the war-chariots and dressed in full trappings. [Illustration: CELTIC BUCKLE] @@ -5457,7 +5457,7 @@ The metal work of these Marnian newcomers is excellent. The peculiar Celtic art style, based originally on the classic tendril motif, is colorful and virile, and fits with Greek and Roman descriptions of Celtic love of color in dress. There is a strong trace of these -newcomers northward in Yorkshire, linked by Ptolemy’s description to +newcomers northward in Yorkshire, linked by Ptolemy�s description to the Parisii, doubtless part of the Celtic tribe which originally gave its name to Paris on the Seine. Near Glastonbury, in Somerset, two villages in swamps have been excavated. They seem to date toward the @@ -5469,7 +5469,7 @@ villagers. In Scotland, which yields its first iron tools at a date of about 100 B.C., and in northern Ireland even slightly earlier, the effects of the -two phases of newcomers tend especially to blend. Hill-forts, “brochs” +two phases of newcomers tend especially to blend. Hill-forts, �brochs� (stone-built round towers) and a variety of other strange structures seem to appear as the new ideas develop in the comparative isolation of northern Britain. @@ -5493,27 +5493,27 @@ at last, we can even begin to speak of dynasties and individuals. Some time before 55 B.C., the Catuvellauni, originally from the Marne district in France, had possessed themselves of a large part of southeastern England. They evidently sailed up the Thames and built a -town of over a hundred acres in area. Here ruled Cassivellaunus, “the -first man in England whose name we know,” and whose town Caesar sacked. +town of over a hundred acres in area. Here ruled Cassivellaunus, �the +first man in England whose name we know,� and whose town Caesar sacked. The town sprang up elsewhere again, however. THE END OF PREHISTORY Prehistory, strictly speaking, is now over in southern Britain. -Claudius’ effective invasion took place in 43 A.D.; by 83 A.D., a raid +Claudius� effective invasion took place in 43 A.D.; by 83 A.D., a raid had been made as far north as Aberdeen in Scotland. But by 127 A.D., Hadrian had completed his wall from the Solway to the Tyne, and the Romans settled behind it. In Scotland, Romanization can have affected -the countryside very little. Professor Piggott adds that “... it is +the countryside very little. Professor Piggott adds that �... it is when the pressure of Romanization is relaxed by the break-up of the Dark Ages that we see again the Celtic metal-smiths handling their material with the same consummate skill as they had before the Roman Conquest, and with traditional styles that had not even then forgotten -their Marnian and Belgic heritage.” +their Marnian and Belgic heritage.� In fact, many centuries go by, in Britain as well as in the rest of -Europe, before the archeologist’s task is complete and the historian on +Europe, before the archeologist�s task is complete and the historian on his own is able to describe the ways of men in the past. @@ -5524,7 +5524,7 @@ you will have noticed how often I had to refer to the European continent itself. Britain, beyond the English Channel for all of her later prehistory, had a much simpler course of events than did most of the rest of Europe in later prehistoric times. This holds, in spite -of all the “invasions” and “reverberations” from the continent. Most +of all the �invasions� and �reverberations� from the continent. Most of Europe was the scene of an even more complicated ebb and flow of cultural change, save in some of its more remote mountain valleys and peninsulas. @@ -5536,7 +5536,7 @@ accounts and some good general accounts of part of the range from about 3000 B.C. to A.D. 1. I suspect that the difficulty of making a good book that covers all of its later prehistory is another aspect of what makes Europe so very complicated a continent today. The prehistoric -foundations for Europe’s very complicated set of civilizations, +foundations for Europe�s very complicated set of civilizations, cultures, and sub-cultures--which begin to appear as history proceeds--were in themselves very complicated. @@ -5552,8 +5552,8 @@ of their journeys. But by the same token, they had had time en route to take on their characteristic European aspects. Some time ago, Sir Cyril Fox wrote a famous book called _The -Personality of Britain_, sub-titled “Its Influence on Inhabitant and -Invader in Prehistoric and Early Historic Times.” We have not gone +Personality of Britain_, sub-titled �Its Influence on Inhabitant and +Invader in Prehistoric and Early Historic Times.� We have not gone into the post-Roman early historic period here; there are still the Anglo-Saxons and Normans to account for as well as the effects of the Romans. But what I have tried to do was to begin the story of @@ -5570,7 +5570,7 @@ Summary In the pages you have read so far, you have been brought through the -earliest 99 per cent of the story of man’s life on this planet. I have +earliest 99 per cent of the story of man�s life on this planet. I have left only 1 per cent of the story for the historians to tell. @@ -5601,7 +5601,7 @@ But I think there may have been a few. Certainly the pace of the first act accelerated with the swing from simple gathering to more intensified collecting. The great cave art of France and Spain was probably an expression of a climax. Even the ideas of burying the dead -and of the “Venus” figurines must also point to levels of human thought +and of the �Venus� figurines must also point to levels of human thought and activity that were over and above pure food-getting. @@ -5629,7 +5629,7 @@ five thousand years after the second act began. But it could never have happened in the first act at all. There is another curious thing about the first act. Many of the players -didn’t know it was over and they kept on with their roles long after +didn�t know it was over and they kept on with their roles long after the second act had begun. On the edges of the stage there are today some players who are still going on with the first act. The Eskimos, and the native Australians, and certain tribes in the Amazon jungle are @@ -5680,20 +5680,20 @@ act may have lessons for us and give depth to our thinking. I know there are at least _some_ lessons, even in the present incomplete state of our knowledge. The players who began the second act--that of food-production--separately, in different parts of the world, were not -all of one “pure race” nor did they have “pure” cultural traditions. +all of one �pure race� nor did they have �pure� cultural traditions. Some apparently quite mixed Mediterraneans got off to the first start on the second act and brought it to its first two climaxes as well. Peoples of quite different physical type achieved the first climaxes in China and in the New World. In our British example of how the late prehistory of Europe worked, we -listed a continuous series of “invasions” and “reverberations.” After +listed a continuous series of �invasions� and �reverberations.� After each of these came fusion. Even though the Channel protected Britain from some of the extreme complications of the mixture and fusion of continental Europe, you can see how silly it would be to refer to a -“pure” British race or a “pure” British culture. We speak of the United -States as a “melting pot.” But this is nothing new. Actually, Britain -and all the rest of the world have been “melting pots” at one time or +�pure� British race or a �pure� British culture. We speak of the United +States as a �melting pot.� But this is nothing new. Actually, Britain +and all the rest of the world have been �melting pots� at one time or another. By the time the written records of Mesopotamia and Egypt begin to turn @@ -5703,12 +5703,12 @@ itself, we are thrown back on prehistoric archeology. And this is as true for China, India, Middle America, and the Andes, as it is for the Near East. -There are lessons to be learned from all of man’s past, not simply +There are lessons to be learned from all of man�s past, not simply lessons of how to fight battles or win peace conferences, but of how human society evolves from one stage to another. Many of these lessons can only be looked for in the prehistoric past. So far, we have only made a beginning. There is much still to do, and many gaps in the story -are yet to be filled. The prehistorian’s job is to find the evidence, +are yet to be filled. The prehistorian�s job is to find the evidence, to fill the gaps, and to discover the lessons men have learned in the past. As I see it, this is not only an exciting but a very practical goal for which to strive. @@ -5745,7 +5745,7 @@ paperbound books.) GEOCHRONOLOGY AND THE ICE AGE -(Two general books. Some Pleistocene geologists disagree with Zeuner’s +(Two general books. Some Pleistocene geologists disagree with Zeuner�s interpretation of the dating evidence, but their points of view appear in professional journals, in articles too cumbersome to list here.) @@ -5815,7 +5815,7 @@ GENERAL PREHISTORY Press. Movius, Hallam L., Jr. - “Old World Prehistory: Paleolithic” in _Anthropology Today_. + �Old World Prehistory: Paleolithic� in _Anthropology Today_. Kroeber, A. L., ed. 1953. University of Chicago Press. Oakley, Kenneth P. @@ -5826,7 +5826,7 @@ GENERAL PREHISTORY _British Prehistory._ 1949. Oxford University Press. Pittioni, Richard - _Die Urgeschichtlichen Grundlagen der Europäischen Kultur._ + _Die Urgeschichtlichen Grundlagen der Europ�ischen Kultur._ 1949. Deuticke. (A single book which does attempt to cover the whole range of European prehistory to ca. 1 A.D.) @@ -5834,7 +5834,7 @@ GENERAL PREHISTORY THE NEAR EAST Adams, Robert M. - “Developmental Stages in Ancient Mesopotamia,” _in_ Steward, + �Developmental Stages in Ancient Mesopotamia,� _in_ Steward, Julian, _et al_, _Irrigation Civilizations: A Comparative Study_. 1955. Pan American Union. @@ -6000,7 +6000,7 @@ Index Bolas, 54 - Bordes, François, 62 + Bordes, Fran�ois, 62 Borer, 77 @@ -6028,7 +6028,7 @@ Index killed by stampede, 86 Burials, 66, 86; - in “henges,” 164; + in �henges,� 164; in urns, 168 Burins, 75 @@ -6085,7 +6085,7 @@ Index Combe Capelle, 30 - Combe Capelle-Brünn group, 34 + Combe Capelle-Br�nn group, 34 Commont, Victor, 51 @@ -6097,7 +6097,7 @@ Index Corrals for cattle, 140 - “Cradle of mankind,” 136 + �Cradle of mankind,� 136 Cremation, 167 @@ -6123,7 +6123,7 @@ Index Domestication, of animals, 100, 105, 107; of plants, 100 - “Dragon teeth” fossils in China, 28 + �Dragon teeth� fossils in China, 28 Drill, 77 @@ -6176,9 +6176,9 @@ Index Fayum, 135; radiocarbon date, 146 - “Fertile Crescent,” 107, 146 + �Fertile Crescent,� 107, 146 - Figurines, “Venus,” 84; + Figurines, �Venus,� 84; at Jarmo, 128; at Ubaid, 153 @@ -6197,7 +6197,7 @@ Index Flint industry, 127 - Fontéchevade, 32, 56, 58 + Font�chevade, 32, 56, 58 Food-collecting, 104, 121; end of, 104 @@ -6223,7 +6223,7 @@ Index Food-vessel folk, 164 - “Forest folk,” 97, 98, 104, 110 + �Forest folk,� 97, 98, 104, 110 Fox, Sir Cyril, 174 @@ -6379,7 +6379,7 @@ Index Land bridges in Mediterranean, 19 - La Tène phase, 170 + La T�ne phase, 170 Laurel leaf point, 78, 89 @@ -6404,7 +6404,7 @@ Index Mammoth, 93; in cave art, 85 - “Man-apes,” 26 + �Man-apes,� 26 Mango, 107 @@ -6435,7 +6435,7 @@ Index Microliths, 87; at Jarmo, 130; - “lunates,” 87; + �lunates,� 87; trapezoids, 87; triangles, 87 @@ -6443,7 +6443,7 @@ Index Mine-shafts, 140 - M’lefaat, 126, 127 + M�lefaat, 126, 127 Mongoloids, 29, 90 @@ -6453,9 +6453,9 @@ Index Mount Carmel, 11, 33, 52, 59, 64, 69, 113, 114 - “Mousterian man,” 64 + �Mousterian man,� 64 - “Mousterian” tools, 61, 62; + �Mousterian� tools, 61, 62; of Acheulean tradition, 62 Movius, H. L., 47 @@ -6471,7 +6471,7 @@ Index Near East, beginnings of civilization in, 20, 144; cave sites, 58; climate in Ice Age, 99; - “Fertile Crescent,” 107, 146; + �Fertile Crescent,� 107, 146; food-production in, 99; Natufian assemblage in, 113-115; stone tools, 114 @@ -6539,7 +6539,7 @@ Index Pig, wild, 108 - “Piltdown man,” 29 + �Piltdown man,� 29 Pins, 80 @@ -6578,7 +6578,7 @@ Index Race, 35; biological, 36; - “pure,” 16 + �pure,� 16 Radioactivity, 9, 10 @@ -6795,7 +6795,7 @@ Index Writing, 158; cuneiform, 158 - Würm I glaciation, 58 + W�rm I glaciation, 58 Zebu cattle, domestication of, 107 @@ -6810,7 +6810,7 @@ Index -Transcriber’s note: +Transcriber�s note: Punctuation, hyphenation, and spelling were made consistent when a predominant preference was found in this book; otherwise they were not @@ -7109,9 +7109,9 @@ and permanent future for Project Gutenberg-tm and future generations. To learn more about the Project Gutenberg Literary Archive Foundation and how your efforts and donations can help, see Sections 3 and 4 and the Foundation information page at -www.gutenberg.org +www.gutenberg.org -Section 3. Information about the Project Gutenberg Literary +Section 3. Information about the Project Gutenberg Literary Archive Foundation The Project Gutenberg Literary Archive Foundation is a non profit diff --git a/ciphers/rabin_miller.py b/ciphers/rabin_miller.py index d6e5cbe8f19f..410d559d4315 100644 --- a/ciphers/rabin_miller.py +++ b/ciphers/rabin_miller.py @@ -2,7 +2,8 @@ import random -def rabinMiller(num): + +def rabin_miller(num: int) -> bool: s = num - 1 t = 0 @@ -10,7 +11,7 @@ def rabinMiller(num): s = s // 2 t += 1 - for trials in range(5): + for _ in range(5): a = random.randrange(2, num - 1) v = pow(a, s, num) if v != 1: @@ -20,44 +21,203 @@ def rabinMiller(num): return False else: i = i + 1 - v = (v ** 2) % num + v = (v**2) % num return True -def isPrime(num): - if (num < 2): + +def is_prime_low_num(num: int) -> bool: + if num < 2: return False - lowPrimes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, - 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, - 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, - 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, - 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, - 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, - 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, - 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, - 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, - 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, - 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, - 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, - 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, - 971, 977, 983, 991, 997] - - if num in lowPrimes: + low_primes = [ + 2, + 3, + 5, + 7, + 11, + 13, + 17, + 19, + 23, + 29, + 31, + 37, + 41, + 43, + 47, + 53, + 59, + 61, + 67, + 71, + 73, + 79, + 83, + 89, + 97, + 101, + 103, + 107, + 109, + 113, + 127, + 131, + 137, + 139, + 149, + 151, + 157, + 163, + 167, + 173, + 179, + 181, + 191, + 193, + 197, + 199, + 211, + 223, + 227, + 229, + 233, + 239, + 241, + 251, + 257, + 263, + 269, + 271, + 277, + 281, + 283, + 293, + 307, + 311, + 313, + 317, + 331, + 337, + 347, + 349, + 353, + 359, + 367, + 373, + 379, + 383, + 389, + 397, + 401, + 409, + 419, + 421, + 431, + 433, + 439, + 443, + 449, + 457, + 461, + 463, + 467, + 479, + 487, + 491, + 499, + 503, + 509, + 521, + 523, + 541, + 547, + 557, + 563, + 569, + 571, + 577, + 587, + 593, + 599, + 601, + 607, + 613, + 617, + 619, + 631, + 641, + 643, + 647, + 653, + 659, + 661, + 673, + 677, + 683, + 691, + 701, + 709, + 719, + 727, + 733, + 739, + 743, + 751, + 757, + 761, + 769, + 773, + 787, + 797, + 809, + 811, + 821, + 823, + 827, + 829, + 839, + 853, + 857, + 859, + 863, + 877, + 881, + 883, + 887, + 907, + 911, + 919, + 929, + 937, + 941, + 947, + 953, + 967, + 971, + 977, + 983, + 991, + 997, + ] + + if num in low_primes: return True - for prime in lowPrimes: + for prime in low_primes: if (num % prime) == 0: return False - return rabinMiller(num) + return rabin_miller(num) -def generateLargePrime(keysize = 1024): + +def generate_large_prime(keysize: int = 1024) -> int: while True: num = random.randrange(2 ** (keysize - 1), 2 ** (keysize)) - if isPrime(num): + if is_prime_low_num(num): return num -if __name__ == '__main__': - num = generateLargePrime() - print('Prime number:', num) - print('isPrime:', isPrime(num)) + +if __name__ == "__main__": + num = generate_large_prime() + print(("Prime number:", num)) + print(("is_prime_low_num:", is_prime_low_num(num))) diff --git a/ciphers/rail_fence_cipher.py b/ciphers/rail_fence_cipher.py new file mode 100644 index 000000000000..5b2311a115e4 --- /dev/null +++ b/ciphers/rail_fence_cipher.py @@ -0,0 +1,102 @@ +"""https://en.wikipedia.org/wiki/Rail_fence_cipher""" + + +def encrypt(input_string: str, key: int) -> str: + """ + Shuffles the character of a string by placing each of them + in a grid (the height is dependent on the key) in a zigzag + formation and reading it left to right. + + >>> encrypt("Hello World", 4) + 'HWe olordll' + + >>> encrypt("This is a message", 0) + Traceback (most recent call last): + ... + ValueError: Height of grid can't be 0 or negative + + >>> encrypt(b"This is a byte string", 5) + Traceback (most recent call last): + ... + TypeError: sequence item 0: expected str instance, int found + """ + temp_grid: list[list[str]] = [[] for _ in range(key)] + lowest = key - 1 + + if key <= 0: + raise ValueError("Height of grid can't be 0 or negative") + if key == 1 or len(input_string) <= key: + return input_string + + for position, character in enumerate(input_string): + num = position % (lowest * 2) # puts it in bounds + num = min(num, lowest * 2 - num) # creates zigzag pattern + temp_grid[num].append(character) + grid = ["".join(row) for row in temp_grid] + output_string = "".join(grid) + + return output_string + + +def decrypt(input_string: str, key: int) -> str: + """ + Generates a template based on the key and fills it in with + the characters of the input string and then reading it in + a zigzag formation. + + >>> decrypt("HWe olordll", 4) + 'Hello World' + + >>> decrypt("This is a message", -10) + Traceback (most recent call last): + ... + ValueError: Height of grid can't be 0 or negative + + >>> decrypt("My key is very big", 100) + 'My key is very big' + """ + grid = [] + lowest = key - 1 + + if key <= 0: + raise ValueError("Height of grid can't be 0 or negative") + if key == 1: + return input_string + + temp_grid: list[list[str]] = [[] for _ in range(key)] # generates template + for position in range(len(input_string)): + num = position % (lowest * 2) # puts it in bounds + num = min(num, lowest * 2 - num) # creates zigzag pattern + temp_grid[num].append("*") + + counter = 0 + for row in temp_grid: # fills in the characters + splice = input_string[counter : counter + len(row)] + grid.append(list(splice)) + counter += len(row) + + output_string = "" # reads as zigzag + for position in range(len(input_string)): + num = position % (lowest * 2) # puts it in bounds + num = min(num, lowest * 2 - num) # creates zigzag pattern + output_string += grid[num][0] + grid[num].pop(0) + return output_string + + +def bruteforce(input_string: str) -> dict[int, str]: + """Uses decrypt function by guessing every key + + >>> bruteforce("HWe olordll")[4] + 'Hello World' + """ + results = {} + for key_guess in range(1, len(input_string)): # tries every key + results[key_guess] = decrypt(input_string, key_guess) + return results + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/ciphers/rc4.py b/ciphers/rc4.py new file mode 100644 index 000000000000..30cd4a7f16a7 --- /dev/null +++ b/ciphers/rc4.py @@ -0,0 +1,217 @@ +""" +RC4 (Rivest Cipher 4) Stream Cipher Algorithm +============================================= + +RC4 is a symmetric stream cipher designed by Ron Rivest in 1987 for RSA Security. +It is famous for its simplicity and speed in software. It operates on bytes, +encrypting and decrypting data one byte at a time by XORing the plaintext with +a pseudorandom keystream. + +How it works: +------------- +1. Key Scheduling Algorithm (KSA): + Initializes and permutes a 256-byte state array (the S-box) based on the secret key. +2. Pseudo-Random Generation Algorithm (PRGA): + Generates a continuous sequence of pseudorandom bytes (the keystream) + from the S-box. + With each byte generated, the state array is mutated to ensure unpredictability. +3. Encryption/Decryption: + The plaintext is XORed byte-by-byte with the keystream to produce the ciphertext. + Since XOR is its own inverse, decryption uses the exact same process (XORing + the ciphertext with the same keystream). + +Security Status: +---------------- +WARNING: RC4 is cryptographically broken and insecure. +It suffers from significant keystream biases, particularly in the initial bytes. +If the same key is reused, or if an attacker captures enough ciphertext, they +can reconstruct the plaintext or the key. The use of RC4 is prohibited in modern +protocols (such as TLS via RFC 7465). It is implemented here strictly for +educational purposes. + +Further reading: +---------------- +* https://en.wikipedia.org/wiki/RC4 +""" + +from collections.abc import Generator + + +def ksa(key: bytes) -> list[int]: + """ + Key Scheduling Algorithm (KSA) + ============================== + + The KSA initializes the permutation in the array S (S-box) of size 256 + with values from 0 to 255. Then, it shuffles the array using the secret key. + + Parameters: + ----------- + * `key`: The secret key used for encryption/decryption as a bytes object. + + Returns: + -------- + * A list of 256 integers representing the permuted S-box. + + Doctests: + ========= + >>> ksa(b"Key")[:5] + [75, 51, 132, 157, 192] + """ + s_box = list(range(256)) + j = 0 + key_length = len(key) + for i in range(256): + j = (j + s_box[i] + key[i % key_length]) % 256 + s_box[i], s_box[j] = s_box[j], s_box[i] + return s_box + + +def prga(s_box: list[int]) -> Generator[int]: + """ + Pseudo-Random Generation Algorithm (PRGA) + ========================================= + + The PRGA generates keystream bytes from the permuted S-box S. + For each iteration, it modifies the S-box and outputs one byte of the keystream. + + Parameters: + ----------- + * `s_box`: The permuted state array S-box. + + Yields: + ------- + * An integer representing the next byte of the pseudo-random keystream. + + Doctests: + ========= + >>> box = ksa(b"Key") + >>> stream = prga(box) + >>> [next(stream) for _ in range(5)] + [235, 159, 119, 129, 183] + """ + s = s_box.copy() + i = 0 + j = 0 + while True: + i = (i + 1) % 256 + j = (j + s[i]) % 256 + s[i], s[j] = s[j], s[i] + yield s[(s[i] + s[j]) % 256] + + +def encrypt(plaintext: bytes, key: bytes) -> bytes: + """ + Encrypts/Decrypts the plaintext bytes with a key using the RC4 stream cipher. + + Parameters: + ----------- + * `plaintext`: The input message to encrypt/decrypt (bytes). + * `key`: The secret key (bytes). + + Returns: + -------- + * The encrypted/decrypted result (bytes). + + More on RC4: + ============ + RC4 (Rivest Cipher 4) is a symmetric stream cipher. Because it is symmetric, + the encryption and decryption operations are identical. The cipher + generates a pseudorandom stream of bytes (keystream) which is combined with + the plaintext using bitwise exclusive-or (XOR). + + Warning: + -------- + RC4 is cryptographically insecure and vulnerable to several attacks (such + as keystream biases). It should not be used in secure systems today. It is + implemented here purely for educational purposes. + + Further reading: + ================ + * https://en.wikipedia.org/wiki/RC4 + + Doctests: + ========= + >>> encrypt(b"Plaintext", b"Key") + b'\\xbb\\xf3\\x16\\xe8\\xd9@\\xaf\\n\\xd3' + >>> encrypt(b"pedia", b"Wiki") + b'\\x10!\\xbf\\x04 ' + >>> encrypt(b"\\x10!\\xbf\\x04 ", b"Wiki") + b'pedia' + """ + if not key: + raise ValueError("Key must not be empty.") + + s_box = ksa(key) + keystream = prga(s_box) + return bytes(p ^ next(keystream) for p in plaintext) + + +def decrypt(ciphertext: bytes, key: bytes) -> bytes: + """ + Decrypts the ciphertext bytes with a key using the RC4 stream cipher. + + Since RC4 is symmetric, decryption is identical to encryption. + + Parameters: + ----------- + * `ciphertext`: The input cipher text to decrypt (bytes). + * `key`: The secret key (bytes). + + Returns: + -------- + * The decrypted plaintext (bytes). + + Doctests: + ========= + >>> decrypt(b'\\x10!\\xbf\\x04 ', b"Wiki") + b'pedia' + """ + return encrypt(ciphertext, key) + + +if __name__ == "__main__": + import sys + + # Check for doctests + if len(sys.argv) > 1 and sys.argv[1] == "--test": + import doctest + + doctest.testmod() + sys.exit(0) + + print(f"\n{'-' * 10}\n RC4 Cipher Menu\n{'-' * 10}") + print("1. Encrypt String") + print("2. Decrypt Hex String") + print("3. Quit") + + while True: + choice = input("\nWhat would you like to do?: ").strip() + if choice == "3" or not choice: + print("Goodbye.") + break + elif choice == "1": + plain_str = input("Enter plain text to encrypt: ") + key_str = input("Enter key: ") + if not key_str: + print("Key cannot be empty!") + continue + encrypted_bytes = encrypt( + plain_str.encode("utf-8"), key_str.encode("utf-8") + ) + print(f"Ciphertext (Hex): {encrypted_bytes.hex()}") + elif choice == "2": + hex_str = input("Enter hex ciphertext to decrypt: ") + key_str = input("Enter key: ") + if not key_str: + print("Key cannot be empty!") + continue + try: + cipher_bytes = bytes.fromhex(hex_str) + decrypted_bytes = decrypt(cipher_bytes, key_str.encode("utf-8")) + decrypted_text = decrypted_bytes.decode("utf-8", errors="replace") + print(f"Decrypted text: {decrypted_text}") + except ValueError as e: + print(f"Invalid input: {e}") + else: + print("Invalid choice, please enter 1, 2, or 3.") diff --git a/ciphers/rot13.py b/ciphers/rot13.py new file mode 100644 index 000000000000..b367c3215127 --- /dev/null +++ b/ciphers/rot13.py @@ -0,0 +1,37 @@ +def dencrypt(s: str, n: int = 13) -> str: + """ + https://en.wikipedia.org/wiki/ROT13 + + >>> msg = "My secret bank account number is 173-52946 so don't tell anyone!!" + >>> s = dencrypt(msg) + >>> s + "Zl frperg onax nppbhag ahzore vf 173-52946 fb qba'g gryy nalbar!!" + >>> dencrypt(s) == msg + True + """ + out = "" + for c in s: + if "A" <= c <= "Z": + out += chr(ord("A") + (ord(c) - ord("A") + n) % 26) + elif "a" <= c <= "z": + out += chr(ord("a") + (ord(c) - ord("a") + n) % 26) + else: + out += c + return out + + +def main() -> None: + s0 = input("Enter message: ") + + s1 = dencrypt(s0, 13) + print("Encryption:", s1) + + s2 = dencrypt(s1, 13) + print("Decryption: ", s2) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/ciphers/rsa_cipher.py b/ciphers/rsa_cipher.py index 7e2dc5fd1228..d974e0c8a176 100644 --- a/ciphers/rsa_cipher.py +++ b/ciphers/rsa_cipher.py @@ -1,124 +1,152 @@ -import sys, rsa_key_generator as rkg, os +import os +import sys + +from . import rsa_key_generator as rkg DEFAULT_BLOCK_SIZE = 128 BYTE_SIZE = 256 -def main(): - filename = 'encrypted_file.txt' - response = input('Encrypte\Decrypt [e\d]: ') - - if response.lower().startswith('e'): - mode = 'encrypt' - elif response.lower().startswith('d'): - mode = 'decrypt' - - if mode == 'encrypt': - if not os.path.exists('rsa_pubkey.txt'): - rkg.makeKeyFiles('rsa', 1024) - - message = input('\nEnter message: ') - pubKeyFilename = 'rsa_pubkey.txt' - print('Encrypting and writing to %s...' % (filename)) - encryptedText = encryptAndWriteToFile(filename, pubKeyFilename, message) - - print('\nEncrypted text:') - print(encryptedText) - - elif mode == 'decrypt': - privKeyFilename = 'rsa_privkey.txt' - print('Reading from %s and decrypting...' % (filename)) - decryptedText = readFromFileAndDecrypt(filename, privKeyFilename) - print('writing decryption to rsa_decryption.txt...') - with open('rsa_decryption.txt', 'w') as dec: - dec.write(decryptedText) - - print('\nDecryption:') - print(decryptedText) - - -def getBlocksFromText(message, blockSize=DEFAULT_BLOCK_SIZE): - messageBytes = message.encode('ascii') - - blockInts = [] - for blockStart in range(0, len(messageBytes), blockSize): - blockInt = 0 - for i in range(blockStart, min(blockStart + blockSize, len(messageBytes))): - blockInt += messageBytes[i] * (BYTE_SIZE ** (i % blockSize)) - blockInts.append(blockInt) - return blockInts - - -def getTextFromBlocks(blockInts, messageLength, blockSize=DEFAULT_BLOCK_SIZE): - message = [] - for blockInt in blockInts: - blockMessage = [] - for i in range(blockSize - 1, -1, -1): - if len(message) + i < messageLength: - asciiNumber = blockInt // (BYTE_SIZE ** i) - blockInt = blockInt % (BYTE_SIZE ** i) - blockMessage.insert(0, chr(asciiNumber)) - message.extend(blockMessage) - return ''.join(message) - - -def encryptMessage(message, key, blockSize=DEFAULT_BLOCK_SIZE): - encryptedBlocks = [] + +def get_blocks_from_text( + message: str, block_size: int = DEFAULT_BLOCK_SIZE +) -> list[int]: + message_bytes = message.encode("ascii") + + block_ints = [] + for block_start in range(0, len(message_bytes), block_size): + block_int = 0 + for i in range(block_start, min(block_start + block_size, len(message_bytes))): + block_int += message_bytes[i] * (BYTE_SIZE ** (i % block_size)) + block_ints.append(block_int) + return block_ints + + +def get_text_from_blocks( + block_ints: list[int], message_length: int, block_size: int = DEFAULT_BLOCK_SIZE +) -> str: + message: list[str] = [] + for block_int in block_ints: + block_message: list[str] = [] + for i in range(block_size - 1, -1, -1): + if len(message) + i < message_length: + ascii_number = block_int // (BYTE_SIZE**i) + block_int = block_int % (BYTE_SIZE**i) + block_message.insert(0, chr(ascii_number)) + message.extend(block_message) + return "".join(message) + + +def encrypt_message( + message: str, key: tuple[int, int], block_size: int = DEFAULT_BLOCK_SIZE +) -> list[int]: + encrypted_blocks = [] n, e = key - for block in getBlocksFromText(message, blockSize): - encryptedBlocks.append(pow(block, e, n)) - return encryptedBlocks + for block in get_blocks_from_text(message, block_size): + encrypted_blocks.append(pow(block, e, n)) + return encrypted_blocks -def decryptMessage(encryptedBlocks, messageLength, key, blockSize=DEFAULT_BLOCK_SIZE): - decryptedBlocks = [] +def decrypt_message( + encrypted_blocks: list[int], + message_length: int, + key: tuple[int, int], + block_size: int = DEFAULT_BLOCK_SIZE, +) -> str: + decrypted_blocks = [] n, d = key - for block in encryptedBlocks: - decryptedBlocks.append(pow(block, d, n)) - return getTextFromBlocks(decryptedBlocks, messageLength, blockSize) - - -def readKeyFile(keyFilename): - fo = open(keyFilename) - content = fo.read() - fo.close() - keySize, n, EorD = content.split(',') - return (int(keySize), int(n), int(EorD)) - - -def encryptAndWriteToFile(messageFilename, keyFilename, message, blockSize=DEFAULT_BLOCK_SIZE): - keySize, n, e = readKeyFile(keyFilename) - if keySize < blockSize * 8: - sys.exit('ERROR: Block size is %s bits and key size is %s bits. The RSA cipher requires the block size to be equal to or greater than the key size. Either decrease the block size or use different keys.' % (blockSize * 8, keySize)) - - encryptedBlocks = encryptMessage(message, (n, e), blockSize) - - for i in range(len(encryptedBlocks)): - encryptedBlocks[i] = str(encryptedBlocks[i]) - encryptedContent = ','.join(encryptedBlocks) - encryptedContent = '%s_%s_%s' % (len(message), blockSize, encryptedContent) - fo = open(messageFilename, 'w') - fo.write(encryptedContent) - fo.close() - return encryptedContent - - -def readFromFileAndDecrypt(messageFilename, keyFilename): - keySize, n, d = readKeyFile(keyFilename) - fo = open(messageFilename) - content = fo.read() - messageLength, blockSize, encryptedMessage = content.split('_') - messageLength = int(messageLength) - blockSize = int(blockSize) - - if keySize < blockSize * 8: - sys.exit('ERROR: Block size is %s bits and key size is %s bits. The RSA cipher requires the block size to be equal to or greater than the key size. Did you specify the correct key file and encrypted file?' % (blockSize * 8, keySize)) - - encryptedBlocks = [] - for block in encryptedMessage.split(','): - encryptedBlocks.append(int(block)) - - return decryptMessage(encryptedBlocks, messageLength, (n, d), blockSize) - -if __name__ == '__main__': + for block in encrypted_blocks: + decrypted_blocks.append(pow(block, d, n)) + return get_text_from_blocks(decrypted_blocks, message_length, block_size) + + +def read_key_file(key_filename: str) -> tuple[int, int, int]: + with open(key_filename) as fo: + content = fo.read() + key_size, n, eor_d = content.split(",") + return (int(key_size), int(n), int(eor_d)) + + +def encrypt_and_write_to_file( + message_filename: str, + key_filename: str, + message: str, + block_size: int = DEFAULT_BLOCK_SIZE, +) -> str: + key_size, n, e = read_key_file(key_filename) + if key_size < block_size * 8: + sys.exit( + f"ERROR: Block size is {block_size * 8} bits and key size is {key_size} " + "bits. The RSA cipher requires the block size to be equal to or greater " + "than the key size. Either decrease the block size or use different keys." + ) + + encrypted_blocks = [str(i) for i in encrypt_message(message, (n, e), block_size)] + + encrypted_content = ",".join(encrypted_blocks) + encrypted_content = f"{len(message)}_{block_size}_{encrypted_content}" + with open(message_filename, "w") as fo: + fo.write(encrypted_content) + return encrypted_content + + +def read_from_file_and_decrypt(message_filename: str, key_filename: str) -> str: + key_size, n, d = read_key_file(key_filename) + with open(message_filename) as fo: + content = fo.read() + message_length_str, block_size_str, encrypted_message = content.split("_") + message_length = int(message_length_str) + block_size = int(block_size_str) + + if key_size < block_size * 8: + sys.exit( + f"ERROR: Block size is {block_size * 8} bits and key size is {key_size} " + "bits. The RSA cipher requires the block size to be equal to or greater " + "than the key size. Were the correct key file and encrypted file specified?" + ) + + encrypted_blocks = [] + for block in encrypted_message.split(","): + encrypted_blocks.append(int(block)) + + return decrypt_message(encrypted_blocks, message_length, (n, d), block_size) + + +def main() -> None: + filename = "encrypted_file.txt" + response = input(r"Encrypt\Decrypt [e\d]: ") + + if response.lower().startswith("e"): + mode = "encrypt" + elif response.lower().startswith("d"): + mode = "decrypt" + + match mode: + case "encrypt": + if not os.path.exists("rsa_pubkey.txt"): + rkg.make_key_files("rsa", 1024) + + message = input("\nEnter message: ") + pubkey_filename = "rsa_pubkey.txt" + print(f"Encrypting and writing to {filename}...") + encrypted_text = encrypt_and_write_to_file( + filename, pubkey_filename, message + ) + + print("\nEncrypted text:") + print(encrypted_text) + + case "decrypt": + privkey_filename = "rsa_privkey.txt" + print(f"Reading from {filename} and decrypting...") + decrypted_text = read_from_file_and_decrypt(filename, privkey_filename) + print("writing decryption to rsa_decryption.txt...") + with open("rsa_decryption.txt", "w") as dec: + dec.write(decrypted_text) + + print("\nDecryption:") + print(decrypted_text) + + +if __name__ == "__main__": main() diff --git a/ciphers/rsa_factorization.py b/ciphers/rsa_factorization.py new file mode 100644 index 000000000000..585b21fac856 --- /dev/null +++ b/ciphers/rsa_factorization.py @@ -0,0 +1,61 @@ +""" +An RSA prime factor algorithm. + +The program can efficiently factor RSA prime number given the private key d and +public key e. + +| Source: on page ``3`` of https://crypto.stanford.edu/~dabo/papers/RSA-survey.pdf +| More readable source: https://www.di-mgt.com.au/rsa_factorize_n.html + +large number can take minutes to factor, therefore are not included in doctest. +""" + +from __future__ import annotations + +import math +import random + + +def rsafactor(d: int, e: int, n: int) -> list[int]: + """ + This function returns the factors of N, where p*q=N + + Return: [p, q] + + We call N the RSA modulus, e the encryption exponent, and d the decryption exponent. + The pair (N, e) is the public key. As its name suggests, it is public and is used to + encrypt messages. + The pair (N, d) is the secret key or private key and is known only to the recipient + of encrypted messages. + + >>> rsafactor(3, 16971, 25777) + [149, 173] + >>> rsafactor(7331, 11, 27233) + [113, 241] + >>> rsafactor(4021, 13, 17711) + [89, 199] + """ + k = d * e - 1 + p = 0 + q = 0 + while p == 0: + g = random.randint(2, n - 1) + t = k + while True: + if t % 2 == 0: + t = t // 2 + x = (g**t) % n + y = math.gcd(x - 1, n) + if x > 1 and y > 1: + p = y + q = n // y + break # find the correct factors + else: + break # t is not divisible by 2, break and choose another g + return sorted([p, q]) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/ciphers/rsa_key_generator.py b/ciphers/rsa_key_generator.py index 7cd7163b68d5..44970e8cbc15 100644 --- a/ciphers/rsa_key_generator.py +++ b/ciphers/rsa_key_generator.py @@ -1,45 +1,63 @@ -import random, sys, os -import rabin_miller as rabinMiller, cryptomath_module as cryptoMath - -def main(): - print('Making key files...') - makeKeyFiles('rsa', 1024) - print('Key files generation successful.') - -def generateKey(keySize): - print('Generating prime p...') - p = rabinMiller.generateLargePrime(keySize) - print('Generating prime q...') - q = rabinMiller.generateLargePrime(keySize) +import os +import random +import sys + +from maths.greatest_common_divisor import gcd_by_iterative + +from . import cryptomath_module, rabin_miller + + +def main() -> None: + print("Making key files...") + make_key_files("rsa", 1024) + print("Key files generation successful.") + + +def generate_key(key_size: int) -> tuple[tuple[int, int], tuple[int, int]]: + """ + >>> random.seed(0) # for repeatability + >>> public_key, private_key = generate_key(8) + >>> public_key + (26569, 239) + >>> private_key + (26569, 2855) + """ + p = rabin_miller.generate_large_prime(key_size) + q = rabin_miller.generate_large_prime(key_size) n = p * q - print('Generating e that is relatively prime to (p - 1) * (q - 1)...') + # Generate e that is relatively prime to (p - 1) * (q - 1) while True: - e = random.randrange(2 ** (keySize - 1), 2 ** (keySize)) - if cryptoMath.gcd(e, (p - 1) * (q - 1)) == 1: + e = random.randrange(2 ** (key_size - 1), 2 ** (key_size)) + if gcd_by_iterative(e, (p - 1) * (q - 1)) == 1: break - print('Calculating d that is mod inverse of e...') - d = cryptoMath.findModInverse(e, (p - 1) * (q - 1)) + # Calculate d that is mod inverse of e + d = cryptomath_module.find_mod_inverse(e, (p - 1) * (q - 1)) - publicKey = (n, e) - privateKey = (n, d) - return (publicKey, privateKey) + public_key = (n, e) + private_key = (n, d) + return (public_key, private_key) -def makeKeyFiles(name, keySize): - if os.path.exists('%s_pubkey.txt' % (name)) or os.path.exists('%s_privkey.txt' % (name)): - print('\nWARNING:') - print('"%s_pubkey.txt" or "%s_privkey.txt" already exists. \nUse a different name or delete these files and re-run this program.' % (name, name)) + +def make_key_files(name: str, key_size: int) -> None: + if os.path.exists(f"{name}_pubkey.txt") or os.path.exists(f"{name}_privkey.txt"): + print("\nWARNING:") + print( + f'"{name}_pubkey.txt" or "{name}_privkey.txt" already exists. \n' + "Use a different name or delete these files and re-run this program." + ) sys.exit() - publicKey, privateKey = generateKey(keySize) - print('\nWriting public key to file %s_pubkey.txt...' % name) - with open('%s_pubkey.txt' % name, 'w') as fo: - fo.write('%s,%s,%s' % (keySize, publicKey[0], publicKey[1])) + public_key, private_key = generate_key(key_size) + print(f"\nWriting public key to file {name}_pubkey.txt...") + with open(f"{name}_pubkey.txt", "w") as out_file: + out_file.write(f"{key_size},{public_key[0]},{public_key[1]}") + + print(f"Writing private key to file {name}_privkey.txt...") + with open(f"{name}_privkey.txt", "w") as out_file: + out_file.write(f"{key_size},{private_key[0]},{private_key[1]}") - print('Writing private key to file %s_privkey.txt...' % name) - with open('%s_privkey.txt' % name, 'w') as fo: - fo.write('%s,%s,%s' % (keySize, privateKey[0], privateKey[1])) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/ciphers/running_key_cipher.py b/ciphers/running_key_cipher.py new file mode 100644 index 000000000000..6bda417be898 --- /dev/null +++ b/ciphers/running_key_cipher.py @@ -0,0 +1,75 @@ +""" +https://en.wikipedia.org/wiki/Running_key_cipher +""" + + +def running_key_encrypt(key: str, plaintext: str) -> str: + """ + Encrypts the plaintext using the Running Key Cipher. + + :param key: The running key (long piece of text). + :param plaintext: The plaintext to be encrypted. + :return: The ciphertext. + """ + plaintext = plaintext.replace(" ", "").upper() + key = key.replace(" ", "").upper() + key_length = len(key) + ciphertext = [] + ord_a = ord("A") + + for i, char in enumerate(plaintext): + p = ord(char) - ord_a + k = ord(key[i % key_length]) - ord_a + c = (p + k) % 26 + ciphertext.append(chr(c + ord_a)) + + return "".join(ciphertext) + + +def running_key_decrypt(key: str, ciphertext: str) -> str: + """ + Decrypts the ciphertext using the Running Key Cipher. + + :param key: The running key (long piece of text). + :param ciphertext: The ciphertext to be decrypted. + :return: The plaintext. + """ + ciphertext = ciphertext.replace(" ", "").upper() + key = key.replace(" ", "").upper() + key_length = len(key) + plaintext = [] + ord_a = ord("A") + + for i, char in enumerate(ciphertext): + c = ord(char) - ord_a + k = ord(key[i % key_length]) - ord_a + p = (c - k) % 26 + plaintext.append(chr(p + ord_a)) + + return "".join(plaintext) + + +def test_running_key_encrypt() -> None: + """ + >>> key = "How does the duck know that? said Victor" + >>> ciphertext = running_key_encrypt(key, "DEFEND THIS") + >>> running_key_decrypt(key, ciphertext) == "DEFENDTHIS" + True + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + test_running_key_encrypt() + + plaintext = input("Enter the plaintext: ").upper() + print(f"\n{plaintext = }") + + key = "How does the duck know that? said Victor" + encrypted_text = running_key_encrypt(key, plaintext) + print(f"{encrypted_text = }") + + decrypted_text = running_key_decrypt(key, encrypted_text) + print(f"{decrypted_text = }") diff --git a/ciphers/shuffled_shift_cipher.py b/ciphers/shuffled_shift_cipher.py new file mode 100644 index 000000000000..08b2cab97c69 --- /dev/null +++ b/ciphers/shuffled_shift_cipher.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +import random +import string + + +class ShuffledShiftCipher: + """ + This algorithm uses the Caesar Cipher algorithm but removes the option to + use brute force to decrypt the message. + + The passcode is a random password from the selection buffer of + 1. uppercase letters of the English alphabet + 2. lowercase letters of the English alphabet + 3. digits from 0 to 9 + + Using unique characters from the passcode, the normal list of characters, + that can be allowed in the plaintext, is pivoted and shuffled. Refer to docstring + of __make_key_list() to learn more about the shuffling. + + Then, using the passcode, a number is calculated which is used to encrypt the + plaintext message with the normal shift cipher method, only in this case, the + reference, to look back at while decrypting, is shuffled. + + Each cipher object can possess an optional argument as passcode, without which a + new passcode is generated for that object automatically. + cip1 = ShuffledShiftCipher('d4usr9TWxw9wMD') + cip2 = ShuffledShiftCipher() + """ + + def __init__(self, passcode: str | None = None) -> None: + """ + Initializes a cipher object with a passcode as it's entity + Note: No new passcode is generated if user provides a passcode + while creating the object + """ + self.__passcode = passcode or self.__passcode_creator() + self.__key_list = self.__make_key_list() + self.__shift_key = self.__make_shift_key() + + def __str__(self) -> str: + """ + :return: passcode of the cipher object + """ + return "".join(self.__passcode) + + def __neg_pos(self, iterlist: list[int]) -> list[int]: + """ + Mutates the list by changing the sign of each alternate element + + :param iterlist: takes a list iterable + :return: the mutated list + + """ + for i in range(1, len(iterlist), 2): + iterlist[i] *= -1 + return iterlist + + def __passcode_creator(self) -> list[str]: + """ + Creates a random password from the selection buffer of + 1. uppercase letters of the English alphabet + 2. lowercase letters of the English alphabet + 3. digits from 0 to 9 + + :rtype: list + :return: a password of a random length between 10 to 20 + """ + choices = string.ascii_letters + string.digits + password = [random.choice(choices) for _ in range(random.randint(10, 20))] + return password + + def __make_key_list(self) -> list[str]: + """ + Shuffles the ordered character choices by pivoting at breakpoints + Breakpoints are the set of characters in the passcode + + eg: + if, ABCDEFGHIJKLMNOPQRSTUVWXYZ are the possible characters + and CAMERA is the passcode + then, breakpoints = [A,C,E,M,R] # sorted set of characters from passcode + shuffled parts: [A,CB,ED,MLKJIHGF,RQPON,ZYXWVUTS] + shuffled __key_list : ACBEDMLKJIHGFRQPONZYXWVUTS + + Shuffling only 26 letters of the english alphabet can generate 26! + combinations for the shuffled list. In the program we consider, a set of + 97 characters (including letters, digits, punctuation and whitespaces), + thereby creating a possibility of 97! combinations (which is a 152 digit number + in itself), thus diminishing the possibility of a brute force approach. + Moreover, shift keys even introduce a multiple of 26 for a brute force approach + for each of the already 97! combinations. + """ + # key_list_options contain nearly all printable except few elements from + # string.whitespace + key_list_options = ( + string.ascii_letters + string.digits + string.punctuation + " \t\n" + ) + + keys_l = [] + + # creates points known as breakpoints to break the key_list_options at those + # points and pivot each substring + breakpoints = sorted(set(self.__passcode)) + temp_list: list[str] = [] + + # algorithm for creating a new shuffled list, keys_l, out of key_list_options + for i in key_list_options: + temp_list.extend(i) + + # checking breakpoints at which to pivot temporary sublist and add it into + # keys_l + if i in breakpoints or i == key_list_options[-1]: + keys_l.extend(temp_list[::-1]) + temp_list.clear() + + # returning a shuffled keys_l to prevent brute force guessing of shift key + return keys_l + + def __make_shift_key(self) -> int: + """ + sum() of the mutated list of ascii values of all characters where the + mutated list is the one returned by __neg_pos() + """ + num = sum(self.__neg_pos([ord(x) for x in self.__passcode])) + return num if num > 0 else len(self.__passcode) + + def decrypt(self, encoded_message: str) -> str: + """ + Performs shifting of the encoded_message w.r.t. the shuffled __key_list + to create the decoded_message + + >>> ssc = ShuffledShiftCipher('4PYIXyqeQZr44') + >>> ssc.decrypt("d>**-1z6&'5z'5z:z+-='$'>=zp:>5:#z<'.&>#") + 'Hello, this is a modified Caesar cipher' + + """ + decoded_message = "" + + # decoding shift like Caesar cipher algorithm implementing negative shift or + # reverse shift or left shift + for i in encoded_message: + position = self.__key_list.index(i) + decoded_message += self.__key_list[ + (position - self.__shift_key) % -len(self.__key_list) + ] + + return decoded_message + + def encrypt(self, plaintext: str) -> str: + """ + Performs shifting of the plaintext w.r.t. the shuffled __key_list + to create the encoded_message + + >>> ssc = ShuffledShiftCipher('4PYIXyqeQZr44') + >>> ssc.encrypt('Hello, this is a modified Caesar cipher') + "d>**-1z6&'5z'5z:z+-='$'>=zp:>5:#z<'.&>#" + + """ + encoded_message = "" + + # encoding shift like Caesar cipher algorithm implementing positive shift or + # forward shift or right shift + for i in plaintext: + position = self.__key_list.index(i) + encoded_message += self.__key_list[ + (position + self.__shift_key) % len(self.__key_list) + ] + + return encoded_message + + +def test_end_to_end(msg: str = "Hello, this is a modified Caesar cipher") -> str: + """ + >>> test_end_to_end() + 'Hello, this is a modified Caesar cipher' + """ + cip1 = ShuffledShiftCipher() + return cip1.decrypt(cip1.encrypt(msg)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/ciphers/simple_keyword_cypher.py b/ciphers/simple_keyword_cypher.py new file mode 100644 index 000000000000..bde137d826c3 --- /dev/null +++ b/ciphers/simple_keyword_cypher.py @@ -0,0 +1,98 @@ +def remove_duplicates(key: str) -> str: + """ + Removes duplicate alphabetic characters in a keyword (letter is ignored after its + first appearance). + + :param key: Keyword to use + :return: String with duplicates removed + + >>> remove_duplicates('Hello World!!') + 'Helo Wrd' + """ + + key_no_dups = "" + for ch in key: + if ch == " " or (ch not in key_no_dups and ch.isalpha()): + key_no_dups += ch + return key_no_dups + + +def create_cipher_map(key: str) -> dict[str, str]: + """ + Returns a cipher map given a keyword. + + :param key: keyword to use + :return: dictionary cipher map + """ + # Create a list of the letters in the alphabet + alphabet = [chr(i + 65) for i in range(26)] + # Remove duplicate characters from key + key = remove_duplicates(key.upper()) + offset = len(key) + # First fill cipher with key characters + cipher_alphabet = {alphabet[i]: char for i, char in enumerate(key)} + # Then map remaining characters in alphabet to + # the alphabet from the beginning + for i in range(len(cipher_alphabet), 26): + char = alphabet[i - offset] + # Ensure we are not mapping letters to letters previously mapped + while char in key: + offset -= 1 + char = alphabet[i - offset] + cipher_alphabet[alphabet[i]] = char + return cipher_alphabet + + +def encipher(message: str, cipher_map: dict[str, str]) -> str: + """ + Enciphers a message given a cipher map. + + :param message: Message to encipher + :param cipher_map: Cipher map + :return: enciphered string + + >>> encipher('Hello World!!', create_cipher_map('Goodbye!!')) + 'CYJJM VMQJB!!' + """ + return "".join(cipher_map.get(ch, ch) for ch in message.upper()) + + +def decipher(message: str, cipher_map: dict[str, str]) -> str: + """ + Deciphers a message given a cipher map + + :param message: Message to decipher + :param cipher_map: Dictionary mapping to use + :return: Deciphered string + + >>> cipher_map = create_cipher_map('Goodbye!!') + >>> decipher(encipher('Hello World!!', cipher_map), cipher_map) + 'HELLO WORLD!!' + """ + # Reverse our cipher mappings + rev_cipher_map = {v: k for k, v in cipher_map.items()} + return "".join(rev_cipher_map.get(ch, ch) for ch in message.upper()) + + +def main() -> None: + """ + Handles I/O + + :return: void + """ + message = input("Enter message to encode or decode: ").strip() + key = input("Enter keyword: ").strip() + option = input("Encipher or decipher? E/D:").strip()[0].lower() + try: + func = {"e": encipher, "d": decipher}[option] + except KeyError: + raise KeyError("invalid input option") + cipher_map = create_cipher_map(key) + print(func(message, cipher_map)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/ciphers/simple_substitution_cipher.py b/ciphers/simple_substitution_cipher.py index 41ac4a6a7b98..3c6898c7b1ee 100644 --- a/ciphers/simple_substitution_cipher.py +++ b/ciphers/simple_substitution_cipher.py @@ -1,70 +1,95 @@ -import sys, random +import random +import sys -LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" -def main(): - message = input('Enter message: ') - key = 'LFWOAYUISVKMNXPBDCRJTQEGHZ' - resp = input('Encrypt/Decrypt [e/d]: ') - checkValidKey(key) +def main() -> None: + message = input("Enter message: ") + key = "LFWOAYUISVKMNXPBDCRJTQEGHZ" + resp = input("Encrypt/Decrypt [e/d]: ") - if resp.lower().startswith('e'): - mode = 'encrypt' - translated = encryptMessage(key, message) - elif resp.lower().startswith('d'): - mode = 'decrypt' - translated = decryptMessage(key, message) + check_valid_key(key) - print('\n%sion: \n%s' % (mode.title(), translated)) - -def checkValidKey(key): - keyList = list(key) - lettersList = list(LETTERS) - keyList.sort() - lettersList.sort() + if resp.lower().startswith("e"): + mode = "encrypt" + translated = encrypt_message(key, message) + elif resp.lower().startswith("d"): + mode = "decrypt" + translated = decrypt_message(key, message) - if keyList != lettersList: - sys.exit('Error in the key or symbol set.') + print(f"\n{mode.title()}ion: \n{translated}") -def encryptMessage(key, message): + +def check_valid_key(key: str) -> None: + """ + Check if the key is valid (contains all 26 letters of the alphabet exactly once). + Exits the program if the key is invalid. + + >>> check_valid_key('LFWOAYUISVKMNXPBDCRJTQEGHZ') + >>> check_valid_key('INVALIDKEY') + Traceback (most recent call last): + ... + SystemExit: Error in the key or symbol set. + """ + key_list = list(key) + letters_list = list(LETTERS) + key_list.sort() + letters_list.sort() + + if key_list != letters_list: + sys.exit("Error in the key or symbol set.") + + +def encrypt_message(key: str, message: str) -> str: """ - >>> encryptMessage('LFWOAYUISVKMNXPBDCRJTQEGHZ', 'Harshil Darji') + >>> encrypt_message('LFWOAYUISVKMNXPBDCRJTQEGHZ', 'Harshil Darji') 'Ilcrism Olcvs' """ - return translateMessage(key, message, 'encrypt') + return translate_message(key, message, "encrypt") + -def decryptMessage(key, message): +def decrypt_message(key: str, message: str) -> str: """ - >>> decryptMessage('LFWOAYUISVKMNXPBDCRJTQEGHZ', 'Ilcrism Olcvs') + >>> decrypt_message('LFWOAYUISVKMNXPBDCRJTQEGHZ', 'Ilcrism Olcvs') 'Harshil Darji' """ - return translateMessage(key, message, 'decrypt') + return translate_message(key, message, "decrypt") -def translateMessage(key, message, mode): - translated = '' - charsA = LETTERS - charsB = key - if mode == 'decrypt': - charsA, charsB = charsB, charsA - +def translate_message(key: str, message: str, mode: str) -> str: + translated = "" + chars_a = LETTERS + chars_b = key + + if mode == "decrypt": + chars_a, chars_b = chars_b, chars_a + for symbol in message: - if symbol.upper() in charsA: - symIndex = charsA.find(symbol.upper()) + if symbol.upper() in chars_a: + sym_index = chars_a.find(symbol.upper()) if symbol.isupper(): - translated += charsB[symIndex].upper() + translated += chars_b[sym_index].upper() else: - translated += charsB[symIndex].lower() + translated += chars_b[sym_index].lower() else: translated += symbol return translated -def getRandomKey(): + +def get_random_key() -> str: + """ + Generate a random substitution cipher key. + + >>> random.seed(0) + >>> get_random_key() + 'OAXSGFHKWUECVDRLTJZPQIBNYM' + """ key = list(LETTERS) random.shuffle(key) - return ''.join(key) + return "".join(key) + -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/ciphers/skytale_cipher.py b/ciphers/skytale_cipher.py new file mode 100644 index 000000000000..11fe83e5c45c --- /dev/null +++ b/ciphers/skytale_cipher.py @@ -0,0 +1,103 @@ +"""Scytale (Skytale) transposition cipher. + +A classical transposition cipher used in ancient Greece. The sender wraps a +strip of parchment around a rod (scytale) and writes the message along the rod. +The recipient with a rod of the same diameter can read the message. + +Reference: https://en.wikipedia.org/wiki/Scytale + +Functions here keep characters as-is (including spaces). The key is a positive +integer representing the circumference count (number of rows). + +>>> encrypt("WE ARE DISCOVERED FLEE AT ONCE", 3) +'WA SVEFETNERDCEDL C EIOR EAOE' +>>> decrypt('WA SVEFETNERDCEDL C EIOR EAOE', 3) +'WE ARE DISCOVERED FLEE AT ONCE' + +Edge cases: +>>> encrypt("HELLO", 1) +'HELLO' +>>> decrypt("HELLO", 1) +'HELLO' +>>> encrypt("HELLO", 5) # key equals length +'HELLO' +>>> decrypt("HELLO", 5) +'HELLO' +>>> encrypt("HELLO", 0) +Traceback (most recent call last): + ... +ValueError: Key must be a positive integer +>>> decrypt("HELLO", -2) +Traceback (most recent call last): + ... +ValueError: Key must be a positive integer +""" + +from __future__ import annotations + + +def encrypt(plaintext: str, key: int) -> str: + """Encrypt plaintext using Scytale transposition. + + Write characters around a rod with `key` rows, then read off by rows. + + :param plaintext: Input message to encrypt + :param key: Positive integer number of rows + :return: Ciphertext string + :raises ValueError: if key <= 0 + """ + if key <= 0: + raise ValueError("Key must be a positive integer") + if key == 1 or len(plaintext) <= key: + return plaintext + + # Read every key-th character starting from each row offset + return "".join(plaintext[row::key] for row in range(key)) + + +def decrypt(ciphertext: str, key: int) -> str: + """Decrypt Scytale ciphertext. + + Reconstruct rows by their lengths and interleave by columns. + + :param ciphertext: Encrypted string + :param key: Positive integer number of rows + :return: Decrypted plaintext + :raises ValueError: if key <= 0 + """ + if key <= 0: + raise ValueError("Key must be a positive integer") + if key == 1 or len(ciphertext) <= key: + return ciphertext + + length = len(ciphertext) + base = length // key + extra = length % key + + # Determine each row length + row_lengths: list[int] = [base + (1 if r < extra else 0) for r in range(key)] + + # Slice ciphertext into rows + rows: list[str] = [] + idx = 0 + for r_len in row_lengths: + rows.append(ciphertext[idx : idx + r_len]) + idx += r_len + + # Pointers to current index in each row + pointers = [0] * key + + # Reconstruct by taking characters column-wise across rows + result_chars: list[str] = [] + for i in range(length): + r = i % key + if pointers[r] < len(rows[r]): + result_chars.append(rows[r][pointers[r]]) + pointers[r] += 1 + return "".join(result_chars) + + +if __name__ == "__main__": # pragma: no cover + import doctest + + doctest.testmod() diff --git a/ciphers/transposition_cipher.py b/ciphers/transposition_cipher.py index 1c2ed0aa0452..76178cb6a1bc 100644 --- a/ciphers/transposition_cipher.py +++ b/ciphers/transposition_cipher.py @@ -1,53 +1,68 @@ import math -def main(): - message = input('Enter message: ') - key = int(input('Enter key [2-%s]: ' % (len(message) - 1))) - mode = input('Encryption/Decryption [e/d]: ') +""" +In cryptography, the TRANSPOSITION cipher is a method of encryption where the +positions of plaintext are shifted a certain number(determined by the key) that +follows a regular system that results in the permuted text, known as the encrypted +text. The type of transposition cipher demonstrated under is the ROUTE cipher. +""" - if mode.lower().startswith('e'): - text = encryptMessage(key, message) - elif mode.lower().startswith('d'): - text = decryptMessage(key, message) + +def main() -> None: + message = input("Enter message: ") + key = int(input(f"Enter key [2-{len(message) - 1}]: ")) + mode = input("Encryption/Decryption [e/d]: ") + + if mode.lower().startswith("e"): + text = encrypt_message(key, message) + elif mode.lower().startswith("d"): + text = decrypt_message(key, message) # Append pipe symbol (vertical bar) to identify spaces at the end. - print('Output:\n%s' %(text + '|')) + print(f"Output:\n{text + '|'}") + -def encryptMessage(key, message): +def encrypt_message(key: int, message: str) -> str: """ - >>> encryptMessage(6, 'Harshil Darji') + >>> encrypt_message(6, 'Harshil Darji') 'Hlia rDsahrij' """ - cipherText = [''] * key + cipher_text = [""] * key for col in range(key): pointer = col while pointer < len(message): - cipherText[col] += message[pointer] + cipher_text[col] += message[pointer] pointer += key - return ''.join(cipherText) + return "".join(cipher_text) -def decryptMessage(key, message): + +def decrypt_message(key: int, message: str) -> str: """ - >>> decryptMessage(6, 'Hlia rDsahrij') + >>> decrypt_message(6, 'Hlia rDsahrij') 'Harshil Darji' """ - numCols = math.ceil(len(message) / key) - numRows = key - numShadedBoxes = (numCols * numRows) - len(message) - plainText = [""] * numCols - col = 0; row = 0; + num_cols = math.ceil(len(message) / key) + num_rows = key + num_shaded_boxes = (num_cols * num_rows) - len(message) + plain_text = [""] * num_cols + col = 0 + row = 0 for symbol in message: - plainText[col] += symbol + plain_text[col] += symbol col += 1 - if (col == numCols) or (col == numCols - 1) and (row >= numRows - numShadedBoxes): + if (col == num_cols) or ( + (col == num_cols - 1) and (row >= num_rows - num_shaded_boxes) + ): col = 0 row += 1 - return "".join(plainText) + return "".join(plain_text) + -if __name__ == '__main__': +if __name__ == "__main__": import doctest + doctest.testmod() main() diff --git a/ciphers/transposition_cipher_encrypt-decrypt_file.py b/ciphers/transposition_cipher_encrypt-decrypt_file.py deleted file mode 100644 index f2252de8a2e2..000000000000 --- a/ciphers/transposition_cipher_encrypt-decrypt_file.py +++ /dev/null @@ -1,35 +0,0 @@ -import time, os, sys -import transposition_cipher as transCipher - -def main(): - inputFile = 'Prehistoric Men.txt' - outputFile = 'Output.txt' - key = int(input('Enter key: ')) - mode = input('Encrypt/Decrypt [e/d]: ') - - if not os.path.exists(inputFile): - print('File %s does not exist. Quitting...' % inputFile) - sys.exit() - if os.path.exists(outputFile): - print('Overwrite %s? [y/n]' % outputFile) - response = input('> ') - if not response.lower().startswith('y'): - sys.exit() - - startTime = time.time() - if mode.lower().startswith('e'): - content = open(inputFile).read() - translated = transCipher.encryptMessage(key, content) - elif mode.lower().startswith('d'): - content = open(outputFile).read() - translated =transCipher .decryptMessage(key, content) - - outputObj = open(outputFile, 'w') - outputObj.write(translated) - outputObj.close() - - totalTime = round(time.time() - startTime, 2) - print('Done (', totalTime, 'seconds )') - -if __name__ == '__main__': - main() diff --git a/ciphers/transposition_cipher_encrypt_decrypt_file.py b/ciphers/transposition_cipher_encrypt_decrypt_file.py new file mode 100644 index 000000000000..b9630243d7f3 --- /dev/null +++ b/ciphers/transposition_cipher_encrypt_decrypt_file.py @@ -0,0 +1,41 @@ +import os +import sys +import time + +from . import transposition_cipher as trans_cipher + + +def main() -> None: + input_file = "./prehistoric_men.txt" + output_file = "./Output.txt" + key = int(input("Enter key: ")) + mode = input("Encrypt/Decrypt [e/d]: ") + + if not os.path.exists(input_file): + print(f"File {input_file} does not exist. Quitting...") + sys.exit() + if os.path.exists(output_file): + print(f"Overwrite {output_file}? [y/n]") + response = input("> ") + if not response.lower().startswith("y"): + sys.exit() + + start_time = time.time() + if mode.lower().startswith("e"): + with open(input_file) as f: + content = f.read() + translated = trans_cipher.encrypt_message(key, content) + elif mode.lower().startswith("d"): + with open(output_file) as f: + content = f.read() + translated = trans_cipher.decrypt_message(key, content) + + with open(output_file, "w") as output_obj: + output_obj.write(translated) + + total_time = round(time.time() - start_time, 2) + print(("Done (", total_time, "seconds )")) + + +if __name__ == "__main__": + main() diff --git a/ciphers/trifid_cipher.py b/ciphers/trifid_cipher.py new file mode 100644 index 000000000000..13a47e9dd03b --- /dev/null +++ b/ciphers/trifid_cipher.py @@ -0,0 +1,215 @@ +""" +The trifid cipher uses a table to fractionate each plaintext letter into a trigram, +mixes the constituents of the trigrams, and then applies the table in reverse to turn +these mixed trigrams into ciphertext letters. + +https://en.wikipedia.org/wiki/Trifid_cipher +""" + +from __future__ import annotations + +# fmt: off +TEST_CHARACTER_TO_NUMBER = { + "A": "111", "B": "112", "C": "113", "D": "121", "E": "122", "F": "123", "G": "131", + "H": "132", "I": "133", "J": "211", "K": "212", "L": "213", "M": "221", "N": "222", + "O": "223", "P": "231", "Q": "232", "R": "233", "S": "311", "T": "312", "U": "313", + "V": "321", "W": "322", "X": "323", "Y": "331", "Z": "332", "+": "333", +} +# fmt: off + +TEST_NUMBER_TO_CHARACTER = {val: key for key, val in TEST_CHARACTER_TO_NUMBER.items()} + + +def __encrypt_part(message_part: str, character_to_number: dict[str, str]) -> str: + """ + Arrange the triagram value of each letter of `message_part` vertically and join + them horizontally. + + >>> __encrypt_part('ASK', TEST_CHARACTER_TO_NUMBER) + '132111112' + """ + one, two, three = "", "", "" + for each in (character_to_number[character] for character in message_part): + one += each[0] + two += each[1] + three += each[2] + + return one + two + three + + +def __decrypt_part( + message_part: str, character_to_number: dict[str, str] +) -> tuple[str, str, str]: + """ + Convert each letter of the input string into their respective trigram values, join + them and split them into three equal groups of strings which are returned. + + >>> __decrypt_part('ABCDE', TEST_CHARACTER_TO_NUMBER) + ('11111', '21131', '21122') + """ + this_part = "".join(character_to_number[character] for character in message_part) + result = [] + tmp = "" + for digit in this_part: + tmp += digit + if len(tmp) == len(message_part): + result.append(tmp) + tmp = "" + + return result[0], result[1], result[2] + + +def __prepare( + message: str, alphabet: str +) -> tuple[str, str, dict[str, str], dict[str, str]]: + """ + A helper function that generates the triagrams and assigns each letter of the + alphabet to its corresponding triagram and stores this in a dictionary + (`character_to_number` and `number_to_character`) after confirming if the + alphabet's length is ``27``. + + >>> test = __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVwxYZ+') + >>> expected = ('IAMABOY','ABCDEFGHIJKLMNOPQRSTUVWXYZ+', + ... TEST_CHARACTER_TO_NUMBER, TEST_NUMBER_TO_CHARACTER) + >>> test == expected + True + + Testing with incomplete alphabet + + >>> __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVw') + Traceback (most recent call last): + ... + KeyError: 'Length of alphabet has to be 27.' + + Testing with extra long alphabets + + >>> __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVwxyzzwwtyyujjgfd') + Traceback (most recent call last): + ... + KeyError: 'Length of alphabet has to be 27.' + + Testing with punctuation not in the given alphabet + + >>> __prepare('am i a boy?','abCdeFghijkLmnopqrStuVwxYZ+') + Traceback (most recent call last): + ... + ValueError: Each message character has to be included in alphabet! + + Testing with numbers + + >>> __prepare(500,'abCdeFghijkLmnopqrStuVwxYZ+') + Traceback (most recent call last): + ... + AttributeError: 'int' object has no attribute 'replace' + """ + # Validate message and alphabet, set to upper and remove spaces + alphabet = alphabet.replace(" ", "").upper() + message = message.replace(" ", "").upper() + + # Check length and characters + if len(alphabet) != 27: + raise KeyError("Length of alphabet has to be 27.") + if any(char not in alphabet for char in message): + raise ValueError("Each message character has to be included in alphabet!") + + # Generate dictionares + character_to_number = dict(zip(alphabet, TEST_CHARACTER_TO_NUMBER.values())) + number_to_character = { + number: letter for letter, number in character_to_number.items() + } + + return message, alphabet, character_to_number, number_to_character + + +def encrypt_message( + message: str, alphabet: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ.", period: int = 5 +) -> str: + """ + encrypt_message + =============== + + Encrypts a message using the trifid_cipher. Any punctuatuion chars that + would be used should be added to the alphabet. + + PARAMETERS + ---------- + + * `message`: The message you want to encrypt. + * `alphabet` (optional): The characters to be used for the cipher . + * `period` (optional): The number of characters you want in a group whilst + encrypting. + + >>> encrypt_message('I am a boy') + 'BCDGBQY' + + >>> encrypt_message(' ') + '' + + >>> encrypt_message(' aide toi le c iel ta id era ', + ... 'FELIXMARDSTBCGHJKNOPQUVWYZ+',5) + 'FMJFVOISSUFTFPUFEQQC' + + """ + message, alphabet, character_to_number, number_to_character = __prepare( + message, alphabet + ) + + encrypted_numeric = "" + for i in range(0, len(message) + 1, period): + encrypted_numeric += __encrypt_part( + message[i : i + period], character_to_number + ) + + encrypted = "" + for i in range(0, len(encrypted_numeric), 3): + encrypted += number_to_character[encrypted_numeric[i : i + 3]] + return encrypted + + +def decrypt_message( + message: str, alphabet: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ.", period: int = 5 +) -> str: + """ + decrypt_message + =============== + + Decrypts a trifid_cipher encrypted message. + + PARAMETERS + ---------- + + * `message`: The message you want to decrypt. + * `alphabet` (optional): The characters used for the cipher. + * `period` (optional): The number of characters used in grouping when it + was encrypted. + + >>> decrypt_message('BCDGBQY') + 'IAMABOY' + + Decrypting with your own alphabet and period + + >>> decrypt_message('FMJFVOISSUFTFPUFEQQC','FELIXMARDSTBCGHJKNOPQUVWYZ+',5) + 'AIDETOILECIELTAIDERA' + """ + message, alphabet, character_to_number, number_to_character = __prepare( + message, alphabet + ) + + decrypted_numeric = [] + for i in range(0, len(message), period): + a, b, c = __decrypt_part(message[i : i + period], character_to_number) + + for j in range(len(a)): + decrypted_numeric.append(a[j] + b[j] + c[j]) + + return "".join(number_to_character[each] for each in decrypted_numeric) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + msg = "DEFEND THE EAST WALL OF THE CASTLE." + encrypted = encrypt_message(msg, "EPSDUCVWYM.ZLKXNBTFGORIJHAQ") + decrypted = decrypt_message(encrypted, "EPSDUCVWYM.ZLKXNBTFGORIJHAQ") + print(f"Encrypted: {encrypted}\nDecrypted: {decrypted}") diff --git a/ciphers/vernam_cipher.py b/ciphers/vernam_cipher.py new file mode 100644 index 000000000000..197f28635a1c --- /dev/null +++ b/ciphers/vernam_cipher.py @@ -0,0 +1,42 @@ +def vernam_encrypt(plaintext: str, key: str) -> str: + """ + >>> vernam_encrypt("HELLO","KEY") + 'RIJVS' + """ + ciphertext = "" + for i in range(len(plaintext)): + ct = ord(key[i % len(key)]) - 65 + ord(plaintext[i]) - 65 + while ct > 25: + ct = ct - 26 + ciphertext += chr(65 + ct) + return ciphertext + + +def vernam_decrypt(ciphertext: str, key: str) -> str: + """ + >>> vernam_decrypt("RIJVS","KEY") + 'HELLO' + """ + decrypted_text = "" + for i in range(len(ciphertext)): + ct = ord(ciphertext[i]) - ord(key[i % len(key)]) + while ct < 0: + ct = 26 + ct + decrypted_text += chr(65 + ct) + return decrypted_text + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + + # Example usage + plaintext = "HELLO" + key = "KEY" + encrypted_text = vernam_encrypt(plaintext, key) + decrypted_text = vernam_decrypt(encrypted_text, key) + print("\n\n") + print("Plaintext:", plaintext) + print("Encrypted:", encrypted_text) + print("Decrypted:", decrypted_text) diff --git a/ciphers/vigenere_cipher.py b/ciphers/vigenere_cipher.py index 95eeb431109f..37b96ea14a46 100644 --- a/ciphers/vigenere_cipher.py +++ b/ciphers/vigenere_cipher.py @@ -1,46 +1,51 @@ -LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" -def main(): - message = input('Enter message: ') - key = input('Enter key [alphanumeric]: ') - mode = input('Encrypt/Decrypt [e/d]: ') - if mode.lower().startswith('e'): - mode = 'encrypt' - translated = encryptMessage(key, message) - elif mode.lower().startswith('d'): - mode = 'decrypt' - translated = decryptMessage(key, message) +def main() -> None: + message = input("Enter message: ") + key = input("Enter key [alphanumeric]: ") + mode = input("Encrypt/Decrypt [e/d]: ") - print('\n%sed message:' % mode.title()) + if mode.lower().startswith("e"): + mode = "encrypt" + translated = encrypt_message(key, message) + elif mode.lower().startswith("d"): + mode = "decrypt" + translated = decrypt_message(key, message) + + print(f"\n{mode.title()}ed message:") print(translated) -def encryptMessage(key, message): - ''' - >>> encryptMessage('HDarji', 'This is Harshil Darji from Dharmaj.') + +def encrypt_message(key: str, message: str) -> str: + """ + >>> encrypt_message('HDarji', 'This is Harshil Darji from Dharmaj.') 'Akij ra Odrjqqs Gaisq muod Mphumrs.' - ''' - return translateMessage(key, message, 'encrypt') + """ + return translate_message(key, message, "encrypt") + -def decryptMessage(key, message): - ''' - >>> decryptMessage('HDarji', 'Akij ra Odrjqqs Gaisq muod Mphumrs.') +def decrypt_message(key: str, message: str) -> str: + """ + >>> decrypt_message('HDarji', 'Akij ra Odrjqqs Gaisq muod Mphumrs.') 'This is Harshil Darji from Dharmaj.' - ''' - return translateMessage(key, message, 'decrypt') + """ + return translate_message(key, message, "decrypt") -def translateMessage(key, message, mode): + +def translate_message(key: str, message: str, mode: str) -> str: translated = [] - keyIndex = 0 + key_index = 0 key = key.upper() for symbol in message: num = LETTERS.find(symbol.upper()) if num != -1: - if mode == 'encrypt': - num += LETTERS.find(key[keyIndex]) - elif mode == 'decrypt': - num -= LETTERS.find(key[keyIndex]) + match mode: + case "encrypt": + num += LETTERS.find(key[key_index]) + case "decrypt": + num -= LETTERS.find(key[key_index]) num %= len(LETTERS) @@ -49,12 +54,13 @@ def translateMessage(key, message, mode): elif symbol.islower(): translated.append(LETTERS[num].lower()) - keyIndex += 1 - if keyIndex == len(key): - keyIndex = 0 + key_index += 1 + if key_index == len(key): + key_index = 0 else: translated.append(symbol) - return ''.join(translated) + return "".join(translated) + -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/ciphers/xor_cipher.py b/ciphers/xor_cipher.py new file mode 100644 index 000000000000..2196775cf575 --- /dev/null +++ b/ciphers/xor_cipher.py @@ -0,0 +1,268 @@ +""" +author: Christian Bender +date: 21.12.2017 +class: XORCipher + +This class implements the XOR-cipher algorithm and provides +some useful methods for encrypting and decrypting strings and +files. + +Overview about methods + +- encrypt : list of char +- decrypt : list of char +- encrypt_string : str +- decrypt_string : str +- encrypt_file : boolean +- decrypt_file : boolean +""" + +from __future__ import annotations + + +class XORCipher: + def __init__(self, key: int = 0) -> None: + """ + simple constructor that receives a key or uses + default key = 0 + """ + + # private field + self.__key = key + + def encrypt(self, content: str, key: int) -> list[str]: + """ + input: 'content' of type string and 'key' of type int + output: encrypted string 'content' as a list of chars + if key not passed the method uses the key by the constructor. + otherwise key = 1 + + Empty list + >>> XORCipher().encrypt("", 5) + [] + + One key + >>> XORCipher().encrypt("hallo welt", 1) + ['i', '`', 'm', 'm', 'n', '!', 'v', 'd', 'm', 'u'] + + Normal key + >>> XORCipher().encrypt("HALLO WELT", 32) + ['h', 'a', 'l', 'l', 'o', '\\x00', 'w', 'e', 'l', 't'] + + Key greater than 255 + >>> XORCipher().encrypt("hallo welt", 256) + ['h', 'a', 'l', 'l', 'o', ' ', 'w', 'e', 'l', 't'] + """ + + # precondition + assert isinstance(key, int) + assert isinstance(content, str) + + key = key or self.__key or 1 + + # make sure key is an appropriate size + key %= 256 + + return [chr(ord(ch) ^ key) for ch in content] + + def decrypt(self, content: str, key: int) -> list[str]: + """ + input: 'content' of type list and 'key' of type int + output: decrypted string 'content' as a list of chars + if key not passed the method uses the key by the constructor. + otherwise key = 1 + + Empty list + >>> XORCipher().decrypt("", 5) + [] + + One key + >>> XORCipher().decrypt("hallo welt", 1) + ['i', '`', 'm', 'm', 'n', '!', 'v', 'd', 'm', 'u'] + + Normal key + >>> XORCipher().decrypt("HALLO WELT", 32) + ['h', 'a', 'l', 'l', 'o', '\\x00', 'w', 'e', 'l', 't'] + + Key greater than 255 + >>> XORCipher().decrypt("hallo welt", 256) + ['h', 'a', 'l', 'l', 'o', ' ', 'w', 'e', 'l', 't'] + """ + + # precondition + assert isinstance(key, int) + assert isinstance(content, str) + + key = key or self.__key or 1 + + # make sure key is an appropriate size + key %= 256 + + return [chr(ord(ch) ^ key) for ch in content] + + def encrypt_string(self, content: str, key: int = 0) -> str: + """ + input: 'content' of type string and 'key' of type int + output: encrypted string 'content' + if key not passed the method uses the key by the constructor. + otherwise key = 1 + + Empty list + >>> XORCipher().encrypt_string("", 5) + '' + + One key + >>> XORCipher().encrypt_string("hallo welt", 1) + 'i`mmn!vdmu' + + Normal key + >>> XORCipher().encrypt_string("HALLO WELT", 32) + 'hallo\\x00welt' + + Key greater than 255 + >>> XORCipher().encrypt_string("hallo welt", 256) + 'hallo welt' + """ + + # precondition + assert isinstance(key, int) + assert isinstance(content, str) + + key = key or self.__key or 1 + + # make sure key is an appropriate size + key %= 256 + + # This will be returned + ans = "" + + for ch in content: + ans += chr(ord(ch) ^ key) + + return ans + + def decrypt_string(self, content: str, key: int = 0) -> str: + """ + input: 'content' of type string and 'key' of type int + output: decrypted string 'content' + if key not passed the method uses the key by the constructor. + otherwise key = 1 + + Empty list + >>> XORCipher().decrypt_string("", 5) + '' + + One key + >>> XORCipher().decrypt_string("hallo welt", 1) + 'i`mmn!vdmu' + + Normal key + >>> XORCipher().decrypt_string("HALLO WELT", 32) + 'hallo\\x00welt' + + Key greater than 255 + >>> XORCipher().decrypt_string("hallo welt", 256) + 'hallo welt' + """ + + # precondition + assert isinstance(key, int) + assert isinstance(content, str) + + key = key or self.__key or 1 + + # make sure key is an appropriate size + key %= 256 + + # This will be returned + ans = "" + + for ch in content: + ans += chr(ord(ch) ^ key) + + return ans + + def encrypt_file(self, file: str, key: int = 0) -> bool: + """ + input: filename (str) and a key (int) + output: returns true if encrypt process was + successful otherwise false + if key not passed the method uses the key by the constructor. + otherwise key = 1 + """ + + # precondition + assert isinstance(file, str) + assert isinstance(key, int) + + # make sure key is an appropriate size + key %= 256 + + try: + with open(file) as fin, open("encrypt.out", "w+") as fout: + # actual encrypt-process + for line in fin: + fout.write(self.encrypt_string(line, key)) + + except OSError: + return False + + return True + + def decrypt_file(self, file: str, key: int) -> bool: + """ + input: filename (str) and a key (int) + output: returns true if decrypt process was + successful otherwise false + if key not passed the method uses the key by the constructor. + otherwise key = 1 + """ + + # precondition + assert isinstance(file, str) + assert isinstance(key, int) + + # make sure key is an appropriate size + key %= 256 + + try: + with open(file) as fin, open("decrypt.out", "w+") as fout: + # actual encrypt-process + for line in fin: + fout.write(self.decrypt_string(line, key)) + + except OSError: + return False + + return True + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + +# Tests +# crypt = XORCipher() +# key = 67 + +# # test encrypt +# print(crypt.encrypt("hallo welt",key)) +# # test decrypt +# print(crypt.decrypt(crypt.encrypt("hallo welt",key), key)) + +# # test encrypt_string +# print(crypt.encrypt_string("hallo welt",key)) + +# # test decrypt_string +# print(crypt.decrypt_string(crypt.encrypt_string("hallo welt",key),key)) + +# if (crypt.encrypt_file("test.txt",key)): +# print("encrypt successful") +# else: +# print("encrypt unsuccessful") + +# if (crypt.decrypt_file("encrypt.out",key)): +# print("decrypt successful") +# else: +# print("decrypt unsuccessful") diff --git a/ciphers/xtea.py b/ciphers/xtea.py new file mode 100644 index 000000000000..4d2aedde6c31 --- /dev/null +++ b/ciphers/xtea.py @@ -0,0 +1,118 @@ +""" +XTEA (eXtended Tiny Encryption Algorithm) is a block cipher designed to +correct weaknesses in TEA. It was published by David Wheeler and Roger +Needham in 1997. XTEA operates on 64-bit blocks with a 128-bit key and +uses a Feistel network with a recommended 64 rounds. + +It's still found in embedded systems and game networking protocols due +to its simplicity and small code footprint. + +Reference: https://en.wikipedia.org/wiki/XTEA +""" + +import struct + +DELTA = 0x9E3779B9 +MASK = 0xFFFFFFFF + + +def xtea_encrypt(block: bytes, key: bytes, num_rounds: int = 64) -> bytes: + """ + Encrypt a single 64-bit block using XTEA. + + :param block: 8 bytes of plaintext + :param key: 16 bytes (128-bit key) + :param num_rounds: number of Feistel rounds (default 64) + :return: 8 bytes of ciphertext + + >>> key = b'\\x00' * 16 + >>> plaintext = b'\\x00' * 8 + >>> ciphertext = xtea_encrypt(plaintext, key) + >>> ciphertext.hex() + 'fc924d124ad0ed50' + + >>> xtea_encrypt(b'hello!!!', b'sixteenbyteskey!') + b'u\\x8d\\x00\\x17c\\xb8\\xf0*' + + >>> xtea_encrypt(b'short', key) + Traceback (most recent call last): + ... + ValueError: block must be 8 bytes + + >>> xtea_encrypt(plaintext, b'short') + Traceback (most recent call last): + ... + ValueError: key must be 16 bytes + """ + if len(block) != 8: + raise ValueError("block must be 8 bytes") + if len(key) != 16: + raise ValueError("key must be 16 bytes") + + v0, v1 = struct.unpack("!II", block) + k = struct.unpack("!4I", key) + + total = 0 + for _ in range(num_rounds): + v0 = (v0 + ((((v1 << 4) ^ (v1 >> 5)) + v1) ^ (total + k[total & 3]))) & MASK + total = (total + DELTA) & MASK + v1 = ( + v1 + ((((v0 << 4) ^ (v0 >> 5)) + v0) ^ (total + k[(total >> 11) & 3])) + ) & MASK + + return struct.pack("!II", v0, v1) + + +def xtea_decrypt(block: bytes, key: bytes, num_rounds: int = 64) -> bytes: + """ + Decrypt a single 64-bit block using XTEA. + + :param block: 8 bytes of ciphertext + :param key: 16 bytes (128-bit key) + :param num_rounds: number of Feistel rounds (default 64) + :return: 8 bytes of plaintext + + Roundtrip test -- encrypt then decrypt returns original plaintext: + >>> key = b'\\x00' * 16 + >>> plaintext = b'\\x00' * 8 + >>> xtea_decrypt(xtea_encrypt(plaintext, key), key) == plaintext + True + + >>> msg = b'hello!!!' + >>> k = b'sixteenbyteskey!' + >>> xtea_decrypt(xtea_encrypt(msg, k), k) == msg + True + + >>> xtea_decrypt(b'short', key) + Traceback (most recent call last): + ... + ValueError: block must be 8 bytes + + >>> xtea_decrypt(b'\\x00' * 8, b'short') + Traceback (most recent call last): + ... + ValueError: key must be 16 bytes + """ + if len(block) != 8: + raise ValueError("block must be 8 bytes") + if len(key) != 16: + raise ValueError("key must be 16 bytes") + + v0, v1 = struct.unpack("!II", block) + k = struct.unpack("!4I", key) + + total = (DELTA * num_rounds) & MASK + for _ in range(num_rounds): + v1 = ( + v1 - ((((v0 << 4) ^ (v0 >> 5)) + v0) ^ (total + k[(total >> 11) & 3])) + ) & MASK + total = (total - DELTA) & MASK + v0 = (v0 - ((((v1 << 4) ^ (v1 >> 5)) + v1) ^ (total + k[total & 3]))) & MASK + + return struct.pack("!II", v0, v1) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/computer_vision/README.md b/computer_vision/README.md new file mode 100644 index 000000000000..ba7ccf457097 --- /dev/null +++ b/computer_vision/README.md @@ -0,0 +1,11 @@ +# Computer Vision + +Computer vision is an interdisciplinary field focused on enabling computers to gain high-level understanding from images and video—automatically extracting, analyzing, and interpreting visual information to produce outputs such as labels, measurements, 3D structure, or decisions. + +In practice, computer vision methods combine geometry, physics, statistics, and machine learning to connect pixel data to semantic concepts like objects, actions, and scenes. + +## Image processing vs. computer vision + +Image processing primarily transforms images (e.g., denoising, contrast enhancement, geometric warping) where the output is another image. + +Computer vision uses images/video as input but often outputs information about the scene (e.g., detections, segmentation masks, pose estimates, tracking results, or a decision), which may then drive downstream behavior in a larger system. diff --git a/computer_vision/__init__.py b/computer_vision/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/computer_vision/flip_augmentation.py b/computer_vision/flip_augmentation.py new file mode 100644 index 000000000000..1c4fad847471 --- /dev/null +++ b/computer_vision/flip_augmentation.py @@ -0,0 +1,129 @@ +import glob +import os +import random +from string import ascii_lowercase, digits + +import cv2 + +""" +Flip image and bounding box for computer vision task +https://paperswithcode.com/method/randomhorizontalflip +""" + +# Params +LABEL_DIR = "" +IMAGE_DIR = "" +OUTPUT_DIR = "" +FLIP_TYPE = 1 # (0 is vertical, 1 is horizontal) + + +def main() -> None: + """ + Get images list and annotations list from input dir. + Update new images and annotations. + Save images and annotations in output dir. + """ + img_paths, annos = get_dataset(LABEL_DIR, IMAGE_DIR) + print("Processing...") + new_images, new_annos, paths = update_image_and_anno(img_paths, annos, FLIP_TYPE) + + for index, image in enumerate(new_images): + # Get random string code: '7b7ad245cdff75241935e4dd860f3bad' + letter_code = random_chars(32) + file_name = paths[index].split(os.sep)[-1].rsplit(".", 1)[0] + file_root = f"{OUTPUT_DIR}/{file_name}_FLIP_{letter_code}" + cv2.imwrite(f"{file_root}.jpg", image, [cv2.IMWRITE_JPEG_QUALITY, 85]) + print(f"Success {index + 1}/{len(new_images)} with {file_name}") + annos_list = [] + for anno in new_annos[index]: + obj = f"{anno[0]} {anno[1]} {anno[2]} {anno[3]} {anno[4]}" + annos_list.append(obj) + with open(f"{file_root}.txt", "w") as outfile: + outfile.write("\n".join(line for line in annos_list)) + + +def get_dataset(label_dir: str, img_dir: str) -> tuple[list, list]: + """ + - label_dir : Path to label include annotation of images + - img_dir : Path to folder contain images + Return : List of images path and labels + """ + img_paths = [] + labels = [] + for label_file in glob.glob(os.path.join(label_dir, "*.txt")): + label_name = label_file.split(os.sep)[-1].rsplit(".", 1)[0] + with open(label_file) as in_file: + obj_lists = in_file.readlines() + img_path = os.path.join(img_dir, f"{label_name}.jpg") + + boxes = [] + for obj_list in obj_lists: + obj = obj_list.rstrip("\n").split(" ") + boxes.append( + [ + int(obj[0]), + float(obj[1]), + float(obj[2]), + float(obj[3]), + float(obj[4]), + ] + ) + if not boxes: + continue + img_paths.append(img_path) + labels.append(boxes) + return img_paths, labels + + +def update_image_and_anno( + img_list: list, anno_list: list, flip_type: int = 1 +) -> tuple[list, list, list]: + """ + - img_list : list of all images + - anno_list : list of all annotations of specific image + - flip_type : 0 is vertical, 1 is horizontal + Return: + - new_imgs_list : image after resize + - new_annos_lists : list of new annotation after scale + - path_list : list the name of image file + """ + new_annos_lists = [] + path_list = [] + new_imgs_list = [] + for idx in range(len(img_list)): + new_annos = [] + path = img_list[idx] + path_list.append(path) + img_annos = anno_list[idx] + img = cv2.imread(path) + match flip_type: + case 1: + new_img = cv2.flip(img, flip_type) + for bbox in img_annos: + x_center_new = 1 - bbox[1] + new_annos.append([bbox[0], x_center_new, bbox[2], bbox[3], bbox[4]]) + case 0: + new_img = cv2.flip(img, flip_type) + for bbox in img_annos: + y_center_new = 1 - bbox[2] + new_annos.append([bbox[0], bbox[1], y_center_new, bbox[3], bbox[4]]) + new_annos_lists.append(new_annos) + new_imgs_list.append(new_img) + return new_imgs_list, new_annos_lists, path_list + + +def random_chars(number_char: int = 32) -> str: + """ + Automatic generate random 32 characters. + Get random string code: '7b7ad245cdff75241935e4dd860f3bad' + >>> len(random_chars(32)) + 32 + """ + assert number_char > 1, "The number of character should greater than 1" + letter_code = ascii_lowercase + digits + return "".join(random.choice(letter_code) for _ in range(number_char)) + + +if __name__ == "__main__": + main() + print("DONE ✅") diff --git a/computer_vision/gramian.py b/computer_vision/gramian.py new file mode 100644 index 000000000000..cc193867c9bf --- /dev/null +++ b/computer_vision/gramian.py @@ -0,0 +1,69 @@ +""" +Image style reconstruction with Gram matrices. + +https://en.wikipedia.org/wiki/Gram_matrix +https://en.wikipedia.org/wiki/Neural_style_transfer +https://arxiv.org/pdf/1603.08155#page=7&zoom=auto,-294,3 +""" + +import numpy as np + + +def gram_matrix(mat: np.ndarray) -> np.ndarray: + """ + Returns the Gram (Gramian) matrix of an image. + + :param mat: matrix of shape (C, H, W); C = color channels, H = height, W = width. + :type mat: np.ndarray + :return: matrix of shape (C, C). + :rtype: np.ndarray + + Examples + -------- + >>> gram_matrix(np.ones((2,5,5))) + array([[0.5, 0.5], + [0.5, 0.5]]) + >>> gram_matrix(np.ones((3,5,5))) + array([[0.33333333, 0.33333333, 0.33333333], + [0.33333333, 0.33333333, 0.33333333], + [0.33333333, 0.33333333, 0.33333333]]) + >>> gram_matrix(np.ones((3,5,5))).shape + (3, 3) + """ + color, height, width = mat.shape + vec = mat.reshape(color, height * width) + gram = vec @ vec.T + return gram / (color * height * width) + + +def gram_loss(input_features: np.ndarray, reference_features: np.ndarray) -> np.float64: + """ + Calculates the squared Frobenius norm of the difference between + the Gram matrices of the input and reference image. + + :param input_features: Feature map of shape (C, H, W) + :type input_features: np.ndarray + :param reference_features: Feature map of shape (C, H, W) + :type reference_features: np.ndarray + :return: Gram loss between the two feature maps. + :rtype: float64 + + Examples + -------- + >>> a = np.random.randn(3,5,5) + >>> gram_loss(a, a) + np.float64(0.0) + >>> a = np.zeros((3,5,5)) + >>> b = np.ones((3,5,5)) + >>> gram_loss(a, b) + np.float64(1.0) + """ + input_gram = gram_matrix(input_features) + reference_gram = gram_matrix(reference_features) + return np.sum(np.square(input_gram - reference_gram)).astype(np.float64) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/computer_vision/haralick_descriptors.py b/computer_vision/haralick_descriptors.py new file mode 100644 index 000000000000..f2f5edb57b89 --- /dev/null +++ b/computer_vision/haralick_descriptors.py @@ -0,0 +1,435 @@ +""" +https://en.wikipedia.org/wiki/Image_texture +https://en.wikipedia.org/wiki/Co-occurrence_matrix#Application_to_image_analysis +""" + +import imageio.v2 as imageio +import numpy as np +import numpy.typing as npt + + +def root_mean_square_error(original: np.ndarray, reference: np.ndarray) -> float: + """Simple implementation of Root Mean Squared Error + for two N dimensional numpy arrays. + + Examples: + >>> root_mean_square_error(np.array([1, 2, 3]), np.array([1, 2, 3])) + 0.0 + >>> root_mean_square_error(np.array([1, 2, 3]), np.array([2, 2, 2])) + 0.816496580927726 + >>> root_mean_square_error(np.array([1, 2, 3]), np.array([6, 4, 2])) + 3.1622776601683795 + """ + return float(np.sqrt(((original - reference) ** 2).mean())) + + +def normalize_image( + image: np.ndarray, cap: float = 255.0, data_type: npt.DTypeLike = np.uint8 +) -> np.ndarray: + """ + Normalizes image in Numpy 2D array format, between ranges 0-cap, + as to fit uint8 type. + + Args: + image: 2D numpy array representing image as matrix, with values in any range + cap: Maximum cap amount for normalization + data_type: numpy data type to set output variable to + Returns: + return 2D numpy array of type uint8, corresponding to limited range matrix + + Examples: + >>> normalize_image(np.array([[1, 2, 3], [4, 5, 10]]), + ... cap=1.0, data_type=np.float64) + array([[0. , 0.11111111, 0.22222222], + [0.33333333, 0.44444444, 1. ]]) + >>> normalize_image(np.array([[4, 4, 3], [1, 7, 2]])) + array([[127, 127, 85], + [ 0, 255, 42]], dtype=uint8) + """ + normalized = (image - np.min(image)) / (np.max(image) - np.min(image)) * cap + return normalized.astype(data_type) + + +def normalize_array(array: np.ndarray, cap: float = 1) -> np.ndarray: + """Normalizes a 1D array, between ranges 0-cap. + + Args: + array: List containing values to be normalized between cap range. + cap: Maximum cap amount for normalization. + Returns: + return 1D numpy array, corresponding to limited range array + + Examples: + >>> normalize_array(np.array([2, 3, 5, 7])) + array([0. , 0.2, 0.6, 1. ]) + >>> normalize_array(np.array([[5], [7], [11], [13]])) + array([[0. ], + [0.25], + [0.75], + [1. ]]) + """ + diff = np.max(array) - np.min(array) + return (array - np.min(array)) / (1 if diff == 0 else diff) * cap + + +def grayscale(image: np.ndarray) -> np.ndarray: + """ + Uses luminance weights to transform RGB channel to greyscale, by + taking the dot product between the channel and the weights. + + Example: + >>> grayscale(np.array([[[108, 201, 72], [255, 11, 127]], + ... [[56, 56, 56], [128, 255, 107]]])) + array([[158, 97], + [ 56, 200]], dtype=uint8) + """ + return np.dot(image[:, :, 0:3], [0.299, 0.587, 0.114]).astype(np.uint8) + + +def binarize(image: np.ndarray, threshold: float = 127.0) -> np.ndarray: + """ + Binarizes a grayscale image based on a given threshold value, + setting values to 1 or 0 accordingly. + + Examples: + >>> binarize(np.array([[128, 255], [101, 156]])) + array([[1, 1], + [0, 1]]) + >>> binarize(np.array([[0.07, 1], [0.51, 0.3]]), threshold=0.5) + array([[0, 1], + [1, 0]]) + """ + return np.where(image > threshold, 1, 0) + + +def transform( + image: np.ndarray, kind: str, kernel: np.ndarray | None = None +) -> np.ndarray: + """ + Simple image transformation using one of two available filter functions: + Erosion and Dilation. + + Args: + image: binarized input image, onto which to apply transformation + kind: Can be either 'erosion', in which case the :func:np.max + function is called, or 'dilation', when :func:np.min is used instead. + kernel: n x n kernel with shape < :attr:image.shape, + to be used when applying convolution to original image + + Returns: + returns a numpy array with same shape as input image, + corresponding to applied binary transformation. + + Examples: + >>> img = np.array([[1, 0.5], [0.2, 0.7]]) + >>> img = binarize(img, threshold=0.5) + >>> transform(img, 'erosion') + array([[1, 1], + [1, 1]], dtype=uint8) + >>> transform(img, 'dilation') + array([[0, 0], + [0, 0]], dtype=uint8) + """ + if kernel is None: + kernel = np.ones((3, 3)) + + if kind == "erosion": + constant = 1 + apply = np.max + else: + constant = 0 + apply = np.min + + center_x, center_y = (x // 2 for x in kernel.shape) + + # Use padded image when applying convolution + # to not go out of bounds of the original the image + transformed = np.zeros(image.shape, dtype=np.uint8) + padded = np.pad(image, 1, "constant", constant_values=constant) + + for x in range(center_x, padded.shape[0] - center_x): + for y in range(center_y, padded.shape[1] - center_y): + center = padded[ + x - center_x : x + center_x + 1, y - center_y : y + center_y + 1 + ] + # Apply transformation method to the centered section of the image + transformed[x - center_x, y - center_y] = apply(center[kernel == 1]) + + return transformed + + +def opening_filter(image: np.ndarray, kernel: np.ndarray | None = None) -> np.ndarray: + """ + Opening filter, defined as the sequence of + erosion and then a dilation filter on the same image. + + Examples: + >>> img = np.array([[1, 0.5], [0.2, 0.7]]) + >>> img = binarize(img, threshold=0.5) + >>> opening_filter(img) + array([[1, 1], + [1, 1]], dtype=uint8) + """ + if kernel is None: + np.ones((3, 3)) + + return transform(transform(image, "dilation", kernel), "erosion", kernel) + + +def closing_filter(image: np.ndarray, kernel: np.ndarray | None = None) -> np.ndarray: + """ + Opening filter, defined as the sequence of + dilation and then erosion filter on the same image. + + Examples: + >>> img = np.array([[1, 0.5], [0.2, 0.7]]) + >>> img = binarize(img, threshold=0.5) + >>> closing_filter(img) + array([[0, 0], + [0, 0]], dtype=uint8) + """ + if kernel is None: + kernel = np.ones((3, 3)) + return transform(transform(image, "erosion", kernel), "dilation", kernel) + + +def binary_mask( + image_gray: np.ndarray, image_map: np.ndarray +) -> tuple[np.ndarray, np.ndarray]: + """ + Apply binary mask, or thresholding based + on bit mask value (mapping mask is binary). + + Returns the mapped true value mask and its complementary false value mask. + + Example: + >>> img = np.array([[[108, 201, 72], [255, 11, 127]], + ... [[56, 56, 56], [128, 255, 107]]]) + >>> gray = grayscale(img) + >>> binary = binarize(gray) + >>> morphological = opening_filter(binary) + >>> binary_mask(gray, morphological) + (array([[1, 1], + [1, 1]], dtype=uint8), array([[158, 97], + [ 56, 200]], dtype=uint8)) + """ + true_mask, false_mask = image_gray.copy(), image_gray.copy() + true_mask[image_map == 1] = 1 + false_mask[image_map == 0] = 0 + + return true_mask, false_mask + + +def matrix_concurrency(image: np.ndarray, coordinate: tuple[int, int]) -> np.ndarray: + """ + Calculate sample co-occurrence matrix based on input image + as well as selected coordinates on image. + + Implementation is made using basic iteration, + as function to be performed (np.max) is non-linear and therefore + not callable on the frequency domain. + + Example: + >>> img = np.array([[[108, 201, 72], [255, 11, 127]], + ... [[56, 56, 56], [128, 255, 107]]]) + >>> gray = grayscale(img) + >>> binary = binarize(gray) + >>> morphological = opening_filter(binary) + >>> mask_1 = binary_mask(gray, morphological)[0] + >>> matrix_concurrency(mask_1, (0, 1)) + array([[0., 0.], + [0., 0.]]) + """ + matrix = np.zeros([np.max(image) + 1, np.max(image) + 1]) + + offset_x, offset_y = coordinate + + for x in range(1, image.shape[0] - 1): + for y in range(1, image.shape[1] - 1): + base_pixel = image[x, y] + offset_pixel = image[x + offset_x, y + offset_y] + + matrix[base_pixel, offset_pixel] += 1 + matrix_sum = np.sum(matrix) + return matrix / (1 if matrix_sum == 0 else matrix_sum) + + +def haralick_descriptors(matrix: np.ndarray) -> list[float]: + """Calculates all 8 Haralick descriptors based on co-occurrence input matrix. + All descriptors are as follows: + Maximum probability, Inverse Difference, Homogeneity, Entropy, + Energy, Dissimilarity, Contrast and Correlation + + Args: + matrix: Co-occurrence matrix to use as base for calculating descriptors. + + Returns: + Reverse ordered list of resulting descriptors + + Example: + >>> img = np.array([[[108, 201, 72], [255, 11, 127]], + ... [[56, 56, 56], [128, 255, 107]]]) + >>> gray = grayscale(img) + >>> binary = binarize(gray) + >>> morphological = opening_filter(binary) + >>> mask_1 = binary_mask(gray, morphological)[0] + >>> concurrency = matrix_concurrency(mask_1, (0, 1)) + >>> [float(f) for f in haralick_descriptors(concurrency)] + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + """ + # Function np.indices could be used for bigger input types, + # but np.ogrid works just fine + i, j = np.ogrid[0 : matrix.shape[0], 0 : matrix.shape[1]] # np.indices() + + # Pre-calculate frequent multiplication and subtraction + prod = np.multiply(i, j) + sub = np.subtract(i, j) + + # Calculate numerical value of Maximum Probability + maximum_prob = np.max(matrix) + # Using the definition for each descriptor individually to calculate its matrix + correlation = prod * matrix + energy = np.power(matrix, 2) + contrast = matrix * np.power(sub, 2) + + dissimilarity = matrix * np.abs(sub) + inverse_difference = matrix / (1 + np.abs(sub)) + homogeneity = matrix / (1 + np.power(sub, 2)) + entropy = -(matrix[matrix > 0] * np.log(matrix[matrix > 0])) + + # Sum values for descriptors ranging from the first one to the last, + # as all are their respective origin matrix and not the resulting value yet. + return [ + maximum_prob, + correlation.sum(), + energy.sum(), + contrast.sum(), + dissimilarity.sum(), + inverse_difference.sum(), + homogeneity.sum(), + entropy.sum(), + ] + + +def get_descriptors( + masks: tuple[np.ndarray, np.ndarray], coordinate: tuple[int, int] +) -> np.ndarray: + """ + Calculate all Haralick descriptors for a sequence of + different co-occurrence matrices, given input masks and coordinates. + + Example: + >>> img = np.array([[[108, 201, 72], [255, 11, 127]], + ... [[56, 56, 56], [128, 255, 107]]]) + >>> gray = grayscale(img) + >>> binary = binarize(gray) + >>> morphological = opening_filter(binary) + >>> get_descriptors(binary_mask(gray, morphological), (0, 1)) + array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) + """ + descriptors = np.array( + [haralick_descriptors(matrix_concurrency(mask, coordinate)) for mask in masks] + ) + + # Concatenate each individual descriptor into + # one single list containing sequence of descriptors + return np.concatenate(descriptors, axis=None) + + +def euclidean(point_1: np.ndarray, point_2: np.ndarray) -> float: + """ + Simple method for calculating the euclidean distance between two points, + with type np.ndarray. + + Example: + >>> a = np.array([1, 0, -2]) + >>> b = np.array([2, -1, 1]) + >>> euclidean(a, b) + 3.3166247903554 + """ + return float(np.sqrt(np.sum(np.square(point_1 - point_2)))) + + +def get_distances(descriptors: np.ndarray, base: int) -> list[tuple[int, float]]: + """ + Calculate all Euclidean distances between a selected base descriptor + and all other Haralick descriptors + The resulting comparison is return in decreasing order, + showing which descriptor is the most similar to the selected base. + + Args: + descriptors: Haralick descriptors to compare with base index + base: Haralick descriptor index to use as base when calculating respective + euclidean distance to other descriptors. + + Returns: + Ordered distances between descriptors + + Example: + >>> index = 1 + >>> img = np.array([[[108, 201, 72], [255, 11, 127]], + ... [[56, 56, 56], [128, 255, 107]]]) + >>> gray = grayscale(img) + >>> binary = binarize(gray) + >>> morphological = opening_filter(binary) + >>> get_distances(get_descriptors( + ... binary_mask(gray, morphological), (0, 1)), + ... index) + [(0, 0.0), (1, 0.0), (2, 0.0), (3, 0.0), (4, 0.0), (5, 0.0), \ +(6, 0.0), (7, 0.0), (8, 0.0), (9, 0.0), (10, 0.0), (11, 0.0), (12, 0.0), \ +(13, 0.0), (14, 0.0), (15, 0.0)] + """ + distances = np.array( + [euclidean(descriptor, descriptors[base]) for descriptor in descriptors] + ) + # Normalize distances between range [0, 1] + normalized_distances: list[float] = normalize_array(distances, 1).tolist() + enum_distances = list(enumerate(normalized_distances)) + enum_distances.sort(key=lambda tup: tup[1], reverse=True) + return enum_distances + + +if __name__ == "__main__": + # Index to compare haralick descriptors to + index = int(input()) + q_value_list = [int(value) for value in input().split()] + q_value = (q_value_list[0], q_value_list[1]) + + # Format is the respective filter to apply, + # can be either 1 for the opening filter or else for the closing + parameters = {"format": int(input()), "threshold": int(input())} + + # Number of images to perform methods on + b_number = int(input()) + + files, descriptors = [], [] + + for _ in range(b_number): + file = input().rstrip() + files.append(file) + + # Open given image and calculate morphological filter, + # respective masks and correspondent Harralick Descriptors. + image = imageio.imread(file).astype(np.float32) + gray = grayscale(image) + threshold = binarize(gray, parameters["threshold"]) + + morphological = ( + opening_filter(threshold) + if parameters["format"] == 1 + else closing_filter(threshold) + ) + masks = binary_mask(gray, morphological) + descriptors.append(get_descriptors(masks, q_value)) + + # Transform ordered distances array into a sequence of indexes + # corresponding to original file position + distances = get_distances(np.array(descriptors), index) + indexed_distances = np.array(distances).astype(np.uint8)[:, 0] + + # Finally, print distances considering the Haralick descriptions from the base + # file to all other images using the morphology method of choice. + print(f"Query: {files[index]}") + print("Ranking:") + for idx, file_idx in enumerate(indexed_distances): + print(f"({idx}) {files[file_idx]}", end="\n") diff --git a/computer_vision/harris_corner.py b/computer_vision/harris_corner.py new file mode 100644 index 000000000000..c47ba2019c4f --- /dev/null +++ b/computer_vision/harris_corner.py @@ -0,0 +1,71 @@ +import cv2 +import numpy as np + +""" +Harris Corner Detector +https://en.wikipedia.org/wiki/Harris_Corner_Detector +""" + + +class HarrisCorner: + def __init__(self, k: float, window_size: int) -> None: + """ + k : is an empirically determined constant in [0.04,0.06] + window_size : neighbourhoods considered + """ + + if k in (0.04, 0.06): + self.k = k + self.window_size = window_size + else: + raise ValueError("invalid k value") + + def __str__(self) -> str: + return str(self.k) + + def detect(self, img_path: str) -> tuple[cv2.Mat, list[list[int]]]: + """ + Returns the image with corners identified + img_path : path of the image + output : list of the corner positions, image + """ + + img = cv2.imread(img_path, 0) + h, w = img.shape + corner_list: list[list[int]] = [] + color_img = img.copy() + color_img = cv2.cvtColor(color_img, cv2.COLOR_GRAY2RGB) + dy, dx = np.gradient(img) + ixx = dx**2 + iyy = dy**2 + ixy = dx * dy + k = 0.04 + offset = self.window_size // 2 + for y in range(offset, h - offset): + for x in range(offset, w - offset): + wxx = ixx[ + y - offset : y + offset + 1, x - offset : x + offset + 1 + ].sum() + wyy = iyy[ + y - offset : y + offset + 1, x - offset : x + offset + 1 + ].sum() + wxy = ixy[ + y - offset : y + offset + 1, x - offset : x + offset + 1 + ].sum() + + det = (wxx * wyy) - (wxy**2) + trace = wxx + wyy + r = det - k * (trace**2) + # Can change the value + if r > 0.5: + corner_list.append([x, y, r]) + color_img.itemset((y, x, 0), 0) + color_img.itemset((y, x, 1), 0) + color_img.itemset((y, x, 2), 255) + return color_img, corner_list + + +if __name__ == "__main__": + edge_detect = HarrisCorner(0.04, 3) + color_img, _ = edge_detect.detect("path_to_image") + cv2.imwrite("detect.png", color_img) diff --git a/computer_vision/horn_schunck.py b/computer_vision/horn_schunck.py new file mode 100644 index 000000000000..f33b5b1c794b --- /dev/null +++ b/computer_vision/horn_schunck.py @@ -0,0 +1,131 @@ +""" +The Horn-Schunck method estimates the optical flow for every single pixel of +a sequence of images. +It works by assuming brightness constancy between two consecutive frames +and smoothness in the optical flow. + +Useful resources: +Wikipedia: https://en.wikipedia.org/wiki/Horn%E2%80%93Schunck_method +Paper: http://image.diku.dk/imagecanon/material/HornSchunckOptical_Flow.pdf +""" + +from typing import SupportsIndex + +import numpy as np +from scipy.ndimage import convolve + + +def warp( + image: np.ndarray, horizontal_flow: np.ndarray, vertical_flow: np.ndarray +) -> np.ndarray: + """ + Warps the pixels of an image into a new image using the horizontal and vertical + flows. + Pixels that are warped from an invalid location are set to 0. + + Parameters: + image: Grayscale image + horizontal_flow: Horizontal flow + vertical_flow: Vertical flow + + Returns: Warped image + + >>> warp(np.array([[0, 1, 2], [0, 3, 0], [2, 2, 2]]), \ + np.array([[0, 1, -1], [-1, 0, 0], [1, 1, 1]]), \ + np.array([[0, 0, 0], [0, 1, 0], [0, 0, 1]])) + array([[0, 0, 0], + [3, 1, 0], + [0, 2, 3]]) + """ + flow = np.stack((horizontal_flow, vertical_flow), 2) + + # Create a grid of all pixel coordinates and subtract the flow to get the + # target pixels coordinates + grid = np.stack( + np.meshgrid(np.arange(0, image.shape[1]), np.arange(0, image.shape[0])), 2 + ) + grid = np.round(grid - flow).astype(np.int32) + + # Find the locations outside of the original image + invalid = (grid < 0) | (grid >= np.array([image.shape[1], image.shape[0]])) + grid[invalid] = 0 + + warped = image[grid[:, :, 1], grid[:, :, 0]] + + # Set pixels at invalid locations to 0 + warped[invalid[:, :, 0] | invalid[:, :, 1]] = 0 + + return warped + + +def horn_schunck( + image0: np.ndarray, + image1: np.ndarray, + num_iter: SupportsIndex, + alpha: float | None = None, +) -> tuple[np.ndarray, np.ndarray]: + """ + This function performs the Horn-Schunck algorithm and returns the estimated + optical flow. It is assumed that the input images are grayscale and + normalized to be in [0, 1]. + + Parameters: + image0: First image of the sequence + image1: Second image of the sequence + alpha: Regularization constant + num_iter: Number of iterations performed + + Returns: estimated horizontal & vertical flow + + >>> np.round(horn_schunck(np.array([[0, 0, 2], [0, 0, 2]]), \ + np.array([[0, 2, 0], [0, 2, 0]]), alpha=0.1, num_iter=110)).\ + astype(np.int32) + array([[[ 0, -1, -1], + [ 0, -1, -1]], + + [[ 0, 0, 0], + [ 0, 0, 0]]], dtype=int32) + """ + if alpha is None: + alpha = 0.1 + + # Initialize flow + horizontal_flow = np.zeros_like(image0) + vertical_flow = np.zeros_like(image0) + + # Prepare kernels for the calculation of the derivatives and the average velocity + kernel_x = np.array([[-1, 1], [-1, 1]]) * 0.25 + kernel_y = np.array([[-1, -1], [1, 1]]) * 0.25 + kernel_t = np.array([[1, 1], [1, 1]]) * 0.25 + kernel_laplacian = np.array( + [[1 / 12, 1 / 6, 1 / 12], [1 / 6, 0, 1 / 6], [1 / 12, 1 / 6, 1 / 12]] + ) + + # Iteratively refine the flow + for _ in range(num_iter): + warped_image = warp(image0, horizontal_flow, vertical_flow) + derivative_x = convolve(warped_image, kernel_x) + convolve(image1, kernel_x) + derivative_y = convolve(warped_image, kernel_y) + convolve(image1, kernel_y) + derivative_t = convolve(warped_image, kernel_t) + convolve(image1, -kernel_t) + + avg_horizontal_velocity = convolve(horizontal_flow, kernel_laplacian) + avg_vertical_velocity = convolve(vertical_flow, kernel_laplacian) + + # This updates the flow as proposed in the paper (Step 12) + update = ( + derivative_x * avg_horizontal_velocity + + derivative_y * avg_vertical_velocity + + derivative_t + ) + update = update / (alpha**2 + derivative_x**2 + derivative_y**2) + + horizontal_flow = avg_horizontal_velocity - derivative_x * update + vertical_flow = avg_vertical_velocity - derivative_y * update + + return horizontal_flow, vertical_flow + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/computer_vision/intensity_based_segmentation.py b/computer_vision/intensity_based_segmentation.py new file mode 100644 index 000000000000..7f2b1141acc4 --- /dev/null +++ b/computer_vision/intensity_based_segmentation.py @@ -0,0 +1,62 @@ +# Source: "https://www.ijcse.com/docs/IJCSE11-02-03-117.pdf" + +# Importing necessary libraries +import matplotlib.pyplot as plt +import numpy as np +from PIL import Image + + +def segment_image(image: np.ndarray, thresholds: list[int]) -> np.ndarray: + """ + Performs image segmentation based on intensity thresholds. + + Args: + image: Input grayscale image as a 2D array. + thresholds: Intensity thresholds to define segments. + + Returns: + A labeled 2D array where each region corresponds to a threshold range. + + Example: + >>> img = np.array([[80, 120, 180], [40, 90, 150], [20, 60, 100]]) + >>> segment_image(img, [50, 100, 150]) + array([[1, 2, 3], + [0, 1, 2], + [0, 1, 1]], dtype=int32) + """ + # Initialize segmented array with zeros + segmented = np.zeros_like(image, dtype=np.int32) + + # Assign labels based on thresholds + for i, threshold in enumerate(thresholds): + segmented[image > threshold] = i + 1 + + return segmented + + +if __name__ == "__main__": + # Load the image + image_path = "path_to_image" # Replace with your image path + original_image = Image.open(image_path).convert("L") + image_array = np.array(original_image) + + # Define thresholds + thresholds = [50, 100, 150, 200] + + # Perform segmentation + segmented_image = segment_image(image_array, thresholds) + + # Display the results + plt.figure(figsize=(10, 5)) + + plt.subplot(1, 2, 1) + plt.title("Original Image") + plt.imshow(image_array, cmap="gray") + plt.axis("off") + + plt.subplot(1, 2, 2) + plt.title("Segmented Image") + plt.imshow(segmented_image, cmap="tab20") + plt.axis("off") + + plt.show() diff --git a/computer_vision/mean_threshold.py b/computer_vision/mean_threshold.py new file mode 100644 index 000000000000..47f9ebcb063b --- /dev/null +++ b/computer_vision/mean_threshold.py @@ -0,0 +1,30 @@ +from PIL import Image + +""" +Mean thresholding algorithm for image processing +https://en.wikipedia.org/wiki/Thresholding_(image_processing) +""" + + +def mean_threshold(image: Image.Image) -> Image.Image: + """ + image: is a grayscale PIL image object + """ + height, width = image.size + mean = 0 + pixels = image.load() + for i in range(width): + for j in range(height): + pixel = pixels[j, i] + mean += pixel + mean //= width * height + + for j in range(width): + for i in range(height): + pixels[i, j] = 255 if pixels[i, j] > mean else 0 + return image + + +if __name__ == "__main__": + image = mean_threshold(Image.open("path_to_image").convert("L")) + image.save("output_image_path") diff --git a/computer_vision/mosaic_augmentation.py b/computer_vision/mosaic_augmentation.py new file mode 100644 index 000000000000..753b8bf012ef --- /dev/null +++ b/computer_vision/mosaic_augmentation.py @@ -0,0 +1,188 @@ +"""Source: https://github.com/jason9075/opencv-mosaic-data-aug""" + +import glob +import os +import random +from string import ascii_lowercase, digits + +import cv2 +import numpy as np + +# Parameters +OUTPUT_SIZE = (720, 1280) # Height, Width +SCALE_RANGE = (0.4, 0.6) # if height or width lower than this scale, drop it. +FILTER_TINY_SCALE = 1 / 100 +LABEL_DIR = "" +IMG_DIR = "" +OUTPUT_DIR = "" +NUMBER_IMAGES = 250 + + +def main() -> None: + """ + Get images list and annotations list from input dir. + Update new images and annotations. + Save images and annotations in output dir. + """ + img_paths, annos = get_dataset(LABEL_DIR, IMG_DIR) + for index in range(NUMBER_IMAGES): + idxs = random.sample(range(len(annos)), 4) + new_image, new_annos, path = update_image_and_anno( + img_paths, + annos, + idxs, + OUTPUT_SIZE, + SCALE_RANGE, + filter_scale=FILTER_TINY_SCALE, + ) + + # Get random string code: '7b7ad245cdff75241935e4dd860f3bad' + letter_code = random_chars(32) + file_name = path.split(os.sep)[-1].rsplit(".", 1)[0] + file_root = f"{OUTPUT_DIR}/{file_name}_MOSAIC_{letter_code}" + cv2.imwrite(f"{file_root}.jpg", new_image, [cv2.IMWRITE_JPEG_QUALITY, 85]) + print(f"Succeeded {index + 1}/{NUMBER_IMAGES} with {file_name}") + annos_list = [] + for anno in new_annos: + width = anno[3] - anno[1] + height = anno[4] - anno[2] + x_center = anno[1] + width / 2 + y_center = anno[2] + height / 2 + obj = f"{anno[0]} {x_center} {y_center} {width} {height}" + annos_list.append(obj) + with open(f"{file_root}.txt", "w") as outfile: + outfile.write("\n".join(line for line in annos_list)) + + +def get_dataset(label_dir: str, img_dir: str) -> tuple[list, list]: + """ + - label_dir : Path to label include annotation of images + - img_dir : Path to folder contain images + Return : List of images path and labels + """ + img_paths = [] + labels = [] + for label_file in glob.glob(os.path.join(label_dir, "*.txt")): + label_name = label_file.split(os.sep)[-1].rsplit(".", 1)[0] + with open(label_file) as in_file: + obj_lists = in_file.readlines() + img_path = os.path.join(img_dir, f"{label_name}.jpg") + + boxes = [] + for obj_list in obj_lists: + obj = obj_list.rstrip("\n").split(" ") + xmin = float(obj[1]) - float(obj[3]) / 2 + ymin = float(obj[2]) - float(obj[4]) / 2 + xmax = float(obj[1]) + float(obj[3]) / 2 + ymax = float(obj[2]) + float(obj[4]) / 2 + + boxes.append([int(obj[0]), xmin, ymin, xmax, ymax]) + if not boxes: + continue + img_paths.append(img_path) + labels.append(boxes) + return img_paths, labels + + +def update_image_and_anno( + all_img_list: list, + all_annos: list, + idxs: list[int], + output_size: tuple[int, int], + scale_range: tuple[float, float], + filter_scale: float = 0.0, +) -> tuple[list, list, str]: + """ + - all_img_list : list of all images + - all_annos : list of all annotations of specific image + - idxs : index of image in list + - output_size : size of output image (Height, Width) + - scale_range : range of scale image + - filter_scale : the condition of downscale image and bounding box + Return: + - output_img : image after resize + - new_anno : list of new annotation after scale + - path[0] : get the name of image file + """ + output_img = np.zeros([output_size[0], output_size[1], 3], dtype=np.uint8) + scale_x = scale_range[0] + random.random() * (scale_range[1] - scale_range[0]) + scale_y = scale_range[0] + random.random() * (scale_range[1] - scale_range[0]) + divid_point_x = int(scale_x * output_size[1]) + divid_point_y = int(scale_y * output_size[0]) + + new_anno = [] + path_list = [] + for i, index in enumerate(idxs): + path = all_img_list[index] + path_list.append(path) + img_annos = all_annos[index] + img = cv2.imread(path) + match i: + case 0: # top-left + img = cv2.resize(img, (divid_point_x, divid_point_y)) + output_img[:divid_point_y, :divid_point_x, :] = img + for bbox in img_annos: + xmin = bbox[1] * scale_x + ymin = bbox[2] * scale_y + xmax = bbox[3] * scale_x + ymax = bbox[4] * scale_y + new_anno.append([bbox[0], xmin, ymin, xmax, ymax]) + case 1: # top-right + img = cv2.resize(img, (output_size[1] - divid_point_x, divid_point_y)) + output_img[:divid_point_y, divid_point_x : output_size[1], :] = img + for bbox in img_annos: + xmin = scale_x + bbox[1] * (1 - scale_x) + ymin = bbox[2] * scale_y + xmax = scale_x + bbox[3] * (1 - scale_x) + ymax = bbox[4] * scale_y + new_anno.append([bbox[0], xmin, ymin, xmax, ymax]) + case 2: # bottom-left + img = cv2.resize(img, (divid_point_x, output_size[0] - divid_point_y)) + output_img[divid_point_y : output_size[0], :divid_point_x, :] = img + for bbox in img_annos: + xmin = bbox[1] * scale_x + ymin = scale_y + bbox[2] * (1 - scale_y) + xmax = bbox[3] * scale_x + ymax = scale_y + bbox[4] * (1 - scale_y) + new_anno.append([bbox[0], xmin, ymin, xmax, ymax]) + case _: # bottom-right + img = cv2.resize( + img, + (output_size[1] - divid_point_x, output_size[0] - divid_point_y), + ) + output_img[ + divid_point_y : output_size[0], divid_point_x : output_size[1], : + ] = img + for bbox in img_annos: + xmin = scale_x + bbox[1] * (1 - scale_x) + ymin = scale_y + bbox[2] * (1 - scale_y) + xmax = scale_x + bbox[3] * (1 - scale_x) + ymax = scale_y + bbox[4] * (1 - scale_y) + new_anno.append([bbox[0], xmin, ymin, xmax, ymax]) + + # Remove bounding box small than scale of filter + if filter_scale > 0: + new_anno = [ + anno + for anno in new_anno + if filter_scale < (anno[3] - anno[1]) and filter_scale < (anno[4] - anno[2]) + ] + + return output_img, new_anno, path_list[0] + + +def random_chars(number_char: int) -> str: + """ + Automatic generate random 32 characters. + Get random string code: '7b7ad245cdff75241935e4dd860f3bad' + >>> len(random_chars(32)) + 32 + """ + assert number_char > 1, "The number of character should greater than 1" + letter_code = ascii_lowercase + digits + return "".join(random.choice(letter_code) for _ in range(number_char)) + + +if __name__ == "__main__": + main() + print("DONE ✅") diff --git a/computer_vision/otsu_threshold.py b/computer_vision/otsu_threshold.py new file mode 100644 index 000000000000..983fe060bce7 --- /dev/null +++ b/computer_vision/otsu_threshold.py @@ -0,0 +1,77 @@ +import numpy as np +from PIL import Image + +""" +Otsu thresholding algorithm for image processing +https://en.wikipedia.org/wiki/Otsu%27s_method +""" + + +def otsu_threshold(image: Image.Image) -> Image.Image: + """ + Applies Otsu's thresholding method to a grayscale image. + + Parameters: + image (PIL.Image.Image): A grayscale PIL image object. + + Returns: + PIL.Image.Image: A binary image after applying Otsu's thresholding. + + Example: + >>> from PIL import Image + >>> import numpy as np + >>> image_array = np.array( + ... [[0, 0, 0, 0], [255, 255, 255, 255], [0, 0, 0, 0], [255, 255, 255, 255]], + ... dtype=np.uint8 + ... ) + >>> image = Image.fromarray(image_array) + >>> binary_image = otsu_threshold(image) + >>> np.array(binary_image) + array([[ 0, 0, 0, 0], + [255, 255, 255, 255], + [ 0, 0, 0, 0], + [255, 255, 255, 255]], dtype=uint8) + """ + # Convert the image to numpy array + pixel_array = np.array(image) + + # Compute histogram + hist, _ = np.histogram(pixel_array, bins=256, range=(0, 256)) + + # Compute between class variance + total_pixels = pixel_array.size + current_max, threshold = 0.0, 0 # Ensure current_max is a float + sum_total, sum_foreground = 0.0, 0.0 # Ensure these are floats + weight_background, weight_foreground = 0.0, 0.0 # Ensure these are floats + + for i in range(256): + sum_total += i * hist[i] + + for i in range(256): + weight_background += hist[i] + if weight_background == 0: + continue + weight_foreground = total_pixels - weight_background + if weight_foreground == 0: + break + sum_foreground += i * hist[i] + + mean_background = sum_foreground / weight_background + mean_foreground = (sum_total - sum_foreground) / weight_foreground + + between_class_variance = ( + weight_background + * weight_foreground + * (mean_background - mean_foreground) ** 2 + ) + + if between_class_variance > current_max: + current_max = between_class_variance + threshold = i + + # Apply threshold to the image + binary_image = pixel_array > threshold + binary_image = binary_image.astype(np.uint8) * 255 + + # Convert numpy array back to PIL image + return Image.fromarray(binary_image) diff --git a/computer_vision/pooling_functions.py b/computer_vision/pooling_functions.py new file mode 100644 index 000000000000..09beabcba82d --- /dev/null +++ b/computer_vision/pooling_functions.py @@ -0,0 +1,135 @@ +# Source : https://computersciencewiki.org/index.php/Max-pooling_/_Pooling +# Importing the libraries +import numpy as np +from PIL import Image + + +# Maxpooling Function +def maxpooling(arr: np.ndarray, size: int, stride: int) -> np.ndarray: + """ + This function is used to perform maxpooling on the input array of 2D matrix(image) + Args: + arr: numpy array + size: size of pooling matrix + stride: the number of pixels shifts over the input matrix + Returns: + numpy array of maxpooled matrix + Sample Input Output: + >>> maxpooling([[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]], 2, 2) + array([[ 6., 8.], + [14., 16.]]) + >>> maxpooling([[147, 180, 122],[241, 76, 32],[126, 13, 157]], 2, 1) + array([[241., 180.], + [241., 157.]]) + """ + arr = np.array(arr) + if arr.shape[0] != arr.shape[1]: + raise ValueError("The input array is not a square matrix") + i = 0 + j = 0 + mat_i = 0 + mat_j = 0 + + # compute the shape of the output matrix + maxpool_shape = (arr.shape[0] - size) // stride + 1 + # initialize the output matrix with zeros of shape maxpool_shape + updated_arr = np.zeros((maxpool_shape, maxpool_shape)) + + while i < arr.shape[0]: + if i + size > arr.shape[0]: + # if the end of the matrix is reached, break + break + while j < arr.shape[1]: + # if the end of the matrix is reached, break + if j + size > arr.shape[1]: + break + # compute the maximum of the pooling matrix + updated_arr[mat_i][mat_j] = np.max(arr[i : i + size, j : j + size]) + # shift the pooling matrix by stride of column pixels + j += stride + mat_j += 1 + + # shift the pooling matrix by stride of row pixels + i += stride + mat_i += 1 + + # reset the column index to 0 + j = 0 + mat_j = 0 + + return updated_arr + + +# Averagepooling Function +def avgpooling(arr: np.ndarray, size: int, stride: int) -> np.ndarray: + """ + This function is used to perform avgpooling on the input array of 2D matrix(image) + Args: + arr: numpy array + size: size of pooling matrix + stride: the number of pixels shifts over the input matrix + Returns: + numpy array of avgpooled matrix + Sample Input Output: + >>> avgpooling([[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]], 2, 2) + array([[ 3., 5.], + [11., 13.]]) + >>> avgpooling([[147, 180, 122],[241, 76, 32],[126, 13, 157]], 2, 1) + array([[161., 102.], + [114., 69.]]) + """ + arr = np.array(arr) + if arr.shape[0] != arr.shape[1]: + raise ValueError("The input array is not a square matrix") + i = 0 + j = 0 + mat_i = 0 + mat_j = 0 + + # compute the shape of the output matrix + avgpool_shape = (arr.shape[0] - size) // stride + 1 + # initialize the output matrix with zeros of shape avgpool_shape + updated_arr = np.zeros((avgpool_shape, avgpool_shape)) + + while i < arr.shape[0]: + # if the end of the matrix is reached, break + if i + size > arr.shape[0]: + break + while j < arr.shape[1]: + # if the end of the matrix is reached, break + if j + size > arr.shape[1]: + break + # compute the average of the pooling matrix + updated_arr[mat_i][mat_j] = int(np.average(arr[i : i + size, j : j + size])) + # shift the pooling matrix by stride of column pixels + j += stride + mat_j += 1 + + # shift the pooling matrix by stride of row pixels + i += stride + mat_i += 1 + # reset the column index to 0 + j = 0 + mat_j = 0 + + return updated_arr + + +# Main Function +if __name__ == "__main__": + from doctest import testmod + + testmod(name="avgpooling", verbose=True) + + # Loading the image + image = Image.open("path_to_image") + + # Converting the image to numpy array and maxpooling, displaying the result + # Ensure that the image is a square matrix + + Image.fromarray(maxpooling(np.array(image), size=3, stride=2)).show() + + # Converting the image to numpy array and averagepooling, displaying the result + # Ensure that the image is a square matrix + + Image.fromarray(avgpooling(np.array(image), size=3, stride=2)).show() diff --git a/computer_vision/vision_transformer.py b/computer_vision/vision_transformer.py new file mode 100644 index 000000000000..1c3e7d07b48e --- /dev/null +++ b/computer_vision/vision_transformer.py @@ -0,0 +1,442 @@ +""" +Vision Transformer (ViT) for Image Classification + +This module implements the Vision Transformer architecture as described in the paper +"An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale" +by Dosovitskiy et al. (2020) + +Paper: https://arxiv.org/abs/2010.11929 + +The Vision Transformer splits an image into fixed-size patches, linearly embeds each +patch, adds position embeddings, and feeds the resulting sequence of vectors to a +standard Transformer encoder. For classification, a learnable classification token +is prepended to the sequence. + +Author: devvratpathak +""" + +import numpy as np + + +def create_patches( + image: np.ndarray, patch_size: int = 16 +) -> tuple[np.ndarray, tuple[int, int]]: + """ + Split an image into non-overlapping patches. + + Args: + image: Input image array of shape (height, width, channels) + patch_size: Size of each square patch (default: 16) + + Returns: + A tuple containing: + - patches: Array of shape (num_patches, patch_size, patch_size, channels) + - grid_size: Tuple (height_patches, width_patches) representing the grid + + Examples: + >>> img = np.random.rand(32, 32, 3) + >>> patches, grid = create_patches(img, patch_size=16) + >>> patches.shape + (4, 16, 16, 3) + >>> grid + (2, 2) + + >>> img = np.random.rand(224, 224, 3) + >>> patches, grid = create_patches(img, patch_size=16) + >>> patches.shape + (196, 16, 16, 3) + >>> grid + (14, 14) + """ + if len(image.shape) != 3: + msg = f"Expected 3D image, got shape {image.shape}" + raise ValueError(msg) + + height, width, channels = image.shape + + if height % patch_size != 0 or width % patch_size != 0: + msg = ( + f"Image dimensions ({height}x{width}) must be divisible by " + f"patch_size ({patch_size})" + ) + raise ValueError(msg) + + # Calculate number of patches in each dimension + num_patches_h = height // patch_size + num_patches_w = width // patch_size + + # Reshape image into patches + patches = image.reshape( + num_patches_h, patch_size, num_patches_w, patch_size, channels + ) + # Transpose to get patches in sequence + patches = patches.transpose(0, 2, 1, 3, 4) + # Reshape to (num_patches, patch_size, patch_size, channels) + patches = patches.reshape(-1, patch_size, patch_size, channels) + + return patches, (num_patches_h, num_patches_w) + + +def patch_embedding(patches: np.ndarray, embedding_dim: int = 768) -> np.ndarray: + """ + Linearly project flattened patches to embedding dimension. + + Args: + patches: Array of patches with shape + (num_patches, patch_size, patch_size, channels) + embedding_dim: Dimension of the embedding space (default: 768) + + Returns: + Embedded patches of shape (num_patches, embedding_dim) + + Examples: + >>> patches = np.random.rand(4, 16, 16, 3) + >>> embeddings = patch_embedding(patches, embedding_dim=768) + >>> embeddings.shape + (4, 768) + + >>> patches = np.random.rand(196, 16, 16, 3) + >>> embeddings = patch_embedding(patches, embedding_dim=512) + >>> embeddings.shape + (196, 512) + """ + num_patches = patches.shape[0] + # Flatten each patch + flattened = patches.reshape(num_patches, -1) + + # Linear projection (simplified - in practice this is a learned weight matrix) + # For demonstration, we use random projection + patch_dim = flattened.shape[1] + rng = np.random.default_rng() + projection_matrix = rng.standard_normal((patch_dim, embedding_dim)) * 0.02 + + embedded = flattened @ projection_matrix + + return embedded + + +def add_positional_encoding( + embeddings: np.ndarray, num_positions: int | None = None +) -> np.ndarray: + """ + Add learnable positional encodings to patch embeddings. + + Args: + embeddings: Embedded patches of shape (num_patches, embedding_dim) + num_positions: Number of positions (if None, uses num_patches + 1 for CLS token) + + Returns: + Embeddings with positional encoding of shape (num_positions, embedding_dim) + + Examples: + >>> embeddings = np.random.rand(4, 768) + >>> pos_embeddings = add_positional_encoding(embeddings) + >>> pos_embeddings.shape + (5, 768) + + >>> embeddings = np.random.rand(196, 512) + >>> pos_embeddings = add_positional_encoding(embeddings) + >>> pos_embeddings.shape + (197, 512) + """ + num_patches, embedding_dim = embeddings.shape + + if num_positions is None: + # Add 1 for the CLS token + num_positions = num_patches + 1 + + # Create learnable positional encodings (simplified - normally learned) + rng = np.random.default_rng() + positional_encodings = rng.standard_normal((num_positions, embedding_dim)) * 0.02 + + # Prepend CLS token + cls_token = rng.standard_normal((1, embedding_dim)) * 0.02 + + # Concatenate CLS token with patch embeddings + embeddings_with_cls = np.vstack([cls_token, embeddings]) + + # Add positional encodings + embeddings_with_pos = embeddings_with_cls + positional_encodings + + return embeddings_with_pos + + +def attention_mechanism( + query: np.ndarray, + key: np.ndarray, + value: np.ndarray, + mask: np.ndarray | None = None, +) -> tuple[np.ndarray, np.ndarray]: + """ + Compute scaled dot-product attention. + + Attention(Q, K, V) = softmax(QK^T / sqrt(d_k))V + + Args: + query: Query matrix of shape (seq_len, d_k) + key: Key matrix of shape (seq_len, d_k) + value: Value matrix of shape (seq_len, d_v) + mask: Optional attention mask + + Returns: + A tuple containing: + - output: Attention output of shape (seq_len, d_v) + - attention_weights: Attention weights of shape (seq_len, seq_len) + + Examples: + >>> q = np.random.rand(10, 64) + >>> k = np.random.rand(10, 64) + >>> v = np.random.rand(10, 64) + >>> output, weights = attention_mechanism(q, k, v) + >>> output.shape + (10, 64) + >>> weights.shape + (10, 10) + >>> np.allclose(weights.sum(axis=1), 1.0) + True + """ + d_k = query.shape[-1] + + # Compute attention scores: QK^T / sqrt(d_k) + scores = query @ key.T / np.sqrt(d_k) + + # Apply mask if provided + if mask is not None: + scores = np.where(mask, scores, -1e9) + + # Apply softmax to get attention weights + exp_scores = np.exp(scores - np.max(scores, axis=-1, keepdims=True)) + attention_weights = exp_scores / np.sum(exp_scores, axis=-1, keepdims=True) + + # Compute weighted sum of values + output = attention_weights @ value + + return output, attention_weights + + +def layer_norm(embeddings: np.ndarray, epsilon: float = 1e-6) -> np.ndarray: + """ + Apply Layer Normalization. + + Args: + embeddings: Input array of shape (seq_len, embedding_dim) + epsilon: Small constant for numerical stability (default: 1e-6) + + Returns: + Normalized array of same shape as input + + Examples: + >>> embeddings = np.random.rand(10, 768) + >>> normalized = layer_norm(embeddings) + >>> normalized.shape + (10, 768) + >>> np.allclose(normalized.mean(axis=1), 0.0, atol=1e-6) + True + >>> np.allclose(normalized.std(axis=1), 1.0, atol=1e-6) + True + """ + mean = embeddings.mean(axis=-1, keepdims=True) + std = embeddings.std(axis=-1, keepdims=True) + return (embeddings - mean) / (std + epsilon) + + +def feedforward_network(embeddings: np.ndarray, hidden_dim: int = 3072) -> np.ndarray: + """ + Apply position-wise feed-forward network. + + FFN(x) = max(0, xW1 + b1)W2 + b2 + + Args: + embeddings: Input array of shape (seq_len, embedding_dim) + hidden_dim: Hidden dimension size (default: 3072, typically 4x embedding_dim) + + Returns: + Output array of shape (seq_len, embedding_dim) + + Examples: + >>> embeddings = np.random.rand(10, 768) + >>> output = feedforward_network(embeddings, hidden_dim=3072) + >>> output.shape + (10, 768) + + >>> embeddings = np.random.rand(197, 512) + >>> output = feedforward_network(embeddings, hidden_dim=2048) + >>> output.shape + (197, 512) + """ + embedding_dim = embeddings.shape[1] + rng = np.random.default_rng() + + # First linear layer + w1 = rng.standard_normal((embedding_dim, hidden_dim)) * 0.02 + b1 = np.zeros(hidden_dim) + hidden = embeddings @ w1 + b1 + + # GELU activation (approximation) + gelu_factor = np.sqrt(2 / np.pi) * (hidden + 0.044715 * hidden**3) + hidden = 0.5 * hidden * (1 + np.tanh(gelu_factor)) + + # Second linear layer + w2 = rng.standard_normal((hidden_dim, embedding_dim)) * 0.02 + b2 = np.zeros(embedding_dim) + output = hidden @ w2 + b2 + + return output + + +def transformer_encoder_block( + embeddings: np.ndarray, + num_heads: int = 12, # noqa: ARG001 + hidden_dim: int = 3072, +) -> np.ndarray: + """ + Apply a single Transformer encoder block. + + The block consists of: + 1. Multi-head self-attention with residual connection and layer norm + 2. Feed-forward network with residual connection and layer norm + + Args: + embeddings: Input array of shape (seq_len, embedding_dim) + num_heads: Number of attention heads (default: 12, kept for API) + hidden_dim: Hidden dimension for FFN (default: 3072) + + Returns: + Output array of shape (seq_len, embedding_dim) + + Examples: + >>> embeddings = np.random.rand(197, 768) + >>> output = transformer_encoder_block( + ... embeddings, num_heads=12, hidden_dim=3072 + ... ) + >>> output.shape + (197, 768) + + >>> embeddings = np.random.rand(50, 512) + >>> output = transformer_encoder_block( + ... embeddings, num_heads=8, hidden_dim=2048 + ... ) + >>> output.shape + (50, 512) + """ + # Multi-head self-attention (simplified - using single head for demonstration) + # In practice, this would split into multiple heads + # num_heads parameter is kept for API compatibility + attention_output, _ = attention_mechanism(embeddings, embeddings, embeddings) + + # Add residual connection and apply layer norm + embeddings = layer_norm(embeddings + attention_output) + + # Feed-forward network + ffn_output = feedforward_network(embeddings, hidden_dim) + + # Add residual connection and apply layer norm + embeddings = layer_norm(embeddings + ffn_output) + + return embeddings + + +def vision_transformer( + image: np.ndarray, + patch_size: int = 16, + embedding_dim: int = 768, + num_layers: int = 12, + num_heads: int = 12, + hidden_dim: int = 3072, + num_classes: int = 1000, +) -> np.ndarray: + """ + Apply Vision Transformer for image classification. + + Architecture: + 1. Split image into patches + 2. Linear projection of flattened patches + 3. Add positional embeddings + 4. Pass through Transformer encoder layers + 5. Extract CLS token and apply classification head + + Args: + image: Input image array of shape (height, width, channels) + patch_size: Size of each patch (default: 16) + embedding_dim: Embedding dimension (default: 768) + num_layers: Number of Transformer layers (default: 12) + num_heads: Number of attention heads (default: 12) + hidden_dim: Hidden dimension in FFN (default: 3072) + num_classes: Number of output classes (default: 1000) + + Returns: + Class logits of shape (num_classes,) + + Examples: + >>> img = np.random.rand(224, 224, 3) + >>> logits = vision_transformer(img, patch_size=16, num_classes=10) + >>> logits.shape + (10,) + + >>> img = np.random.rand(32, 32, 3) + >>> logits = vision_transformer( + ... img, patch_size=16, embedding_dim=512, num_layers=6, num_classes=100 + ... ) + >>> logits.shape + (100,) + """ + # Step 1: Create patches + patches, _ = create_patches(image, patch_size) + + # Step 2: Embed patches + embeddings = patch_embedding(patches, embedding_dim) + + # Step 3: Add positional encodings (includes CLS token) + embeddings = add_positional_encoding(embeddings) + + # Step 4: Pass through Transformer encoder layers + for _ in range(num_layers): + embeddings = transformer_encoder_block(embeddings, num_heads, hidden_dim) + + # Step 5: Extract CLS token (first token) + cls_token = embeddings[0] + + # Step 6: Classification head (linear layer) + rng = np.random.default_rng() + classifier_weights = rng.standard_normal((embedding_dim, num_classes)) * 0.02 + classifier_bias = np.zeros(num_classes) + logits = cls_token @ classifier_weights + classifier_bias + + return logits + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + # Example usage + print("Vision Transformer Example") + print("=" * 50) + + # Create a sample image (224x224x3 for ImageNet-style input) + rng = np.random.default_rng() + sample_image = rng.random((224, 224, 3)) + print(f"Input image shape: {sample_image.shape}") + + # Apply Vision Transformer + logits = vision_transformer( + sample_image, + patch_size=16, + embedding_dim=768, + num_layers=12, + num_heads=12, + hidden_dim=3072, + num_classes=1000, + ) + + print(f"Output logits shape: {logits.shape}") + print(f"Predicted class: {np.argmax(logits)}") + + # Demonstrate patch creation + print("\n" + "=" * 50) + print("Patch Creation Example") + patches, grid = create_patches(sample_image, patch_size=16) + print(f"Number of patches: {patches.shape[0]}") + print(f"Patch size: {patches.shape[1:3]}") + print(f"Grid size: {grid}") diff --git a/conversions/README.md b/conversions/README.md new file mode 100644 index 000000000000..ec3d931fd828 --- /dev/null +++ b/conversions/README.md @@ -0,0 +1,6 @@ +# Conversion + +Conversion programs convert a type of data, a number from a numerical base or unit into one of another type, base or unit, e.g. binary to decimal, integer to string or foot to meters. + +* +* diff --git a/conversions/__init__.py b/conversions/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/conversions/astronomical_length_scale_conversion.py b/conversions/astronomical_length_scale_conversion.py new file mode 100644 index 000000000000..0f413644906d --- /dev/null +++ b/conversions/astronomical_length_scale_conversion.py @@ -0,0 +1,106 @@ +""" +Conversion of length units. +Available Units: +Metre, Kilometre, Megametre, Gigametre, +Terametre, Petametre, Exametre, Zettametre, Yottametre + +USAGE : +-> Import this file into their respective project. +-> Use the function length_conversion() for conversion of length units. +-> Parameters : + -> value : The number of from units you want to convert + -> from_type : From which type you want to convert + -> to_type : To which type you want to convert + +REFERENCES : +-> Wikipedia reference: https://en.wikipedia.org/wiki/Meter +-> Wikipedia reference: https://en.wikipedia.org/wiki/Kilometer +-> Wikipedia reference: https://en.wikipedia.org/wiki/Orders_of_magnitude_(length) +""" + +UNIT_SYMBOL = { + "meter": "m", + "kilometer": "km", + "megametre": "Mm", + "gigametre": "Gm", + "terametre": "Tm", + "petametre": "Pm", + "exametre": "Em", + "zettametre": "Zm", + "yottametre": "Ym", +} +# Exponent of the factor(meter) +METRIC_CONVERSION = { + "m": 0, + "km": 3, + "Mm": 6, + "Gm": 9, + "Tm": 12, + "Pm": 15, + "Em": 18, + "Zm": 21, + "Ym": 24, +} + + +def length_conversion(value: float, from_type: str, to_type: str) -> float: + """ + Conversion between astronomical length units. + + >>> length_conversion(1, "meter", "kilometer") + 0.001 + >>> length_conversion(1, "meter", "megametre") + 1e-06 + >>> length_conversion(1, "gigametre", "meter") + 1000000000 + >>> length_conversion(1, "gigametre", "terametre") + 0.001 + >>> length_conversion(1, "petametre", "terametre") + 1000 + >>> length_conversion(1, "petametre", "exametre") + 0.001 + >>> length_conversion(1, "terametre", "zettametre") + 1e-09 + >>> length_conversion(1, "yottametre", "zettametre") + 1000 + >>> length_conversion(4, "wrongUnit", "inch") + Traceback (most recent call last): + ... + ValueError: Invalid 'from_type' value: 'wrongUnit'. + Conversion abbreviations are: m, km, Mm, Gm, Tm, Pm, Em, Zm, Ym + """ + + from_sanitized = from_type.lower().strip("s") + to_sanitized = to_type.lower().strip("s") + + from_sanitized = UNIT_SYMBOL.get(from_sanitized, from_sanitized) + to_sanitized = UNIT_SYMBOL.get(to_sanitized, to_sanitized) + + if from_sanitized not in METRIC_CONVERSION: + msg = ( + f"Invalid 'from_type' value: {from_type!r}.\n" + f"Conversion abbreviations are: {', '.join(METRIC_CONVERSION)}" + ) + raise ValueError(msg) + if to_sanitized not in METRIC_CONVERSION: + msg = ( + f"Invalid 'to_type' value: {to_type!r}.\n" + f"Conversion abbreviations are: {', '.join(METRIC_CONVERSION)}" + ) + raise ValueError(msg) + from_exponent = METRIC_CONVERSION[from_sanitized] + to_exponent = METRIC_CONVERSION[to_sanitized] + exponent = 1 + + if from_exponent > to_exponent: + exponent = from_exponent - to_exponent + else: + exponent = -(to_exponent - from_exponent) + + return value * pow(10, exponent) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/conversions/base64_to_binary.py b/conversions/base64_to_binary.py new file mode 100644 index 000000000000..6973a303e2b2 --- /dev/null +++ b/conversions/base64_to_binary.py @@ -0,0 +1,112 @@ +""" +* Author: Siddharth Singh (https://github.com/coolsidd) +* Description: Convert a binary string to Base64. +References for better understanding: +https://en.wikipedia.org/wiki/Base64 +""" + +B64_TO_BITS = { + "A": "000000", + "B": "000001", + "C": "000010", + "D": "000011", + "E": "000100", + "F": "000101", + "G": "000110", + "H": "000111", + "I": "001000", + "J": "001001", + "K": "001010", + "L": "001011", + "M": "001100", + "N": "001101", + "O": "001110", + "P": "001111", + "Q": "010000", + "R": "010001", + "S": "010010", + "T": "010011", + "U": "010100", + "V": "010101", + "W": "010110", + "X": "010111", + "Y": "011000", + "Z": "011001", + "a": "011010", + "b": "011011", + "c": "011100", + "d": "011101", + "e": "011110", + "f": "011111", + "g": "100000", + "h": "100001", + "i": "100010", + "j": "100011", + "k": "100100", + "l": "100101", + "m": "100110", + "n": "100111", + "o": "101000", + "p": "101001", + "q": "101010", + "r": "101011", + "s": "101100", + "t": "101101", + "u": "101110", + "v": "101111", + "w": "110000", + "x": "110001", + "y": "110010", + "z": "110011", + "0": "110100", + "1": "110101", + "2": "110110", + "3": "110111", + "4": "111000", + "5": "111001", + "6": "111010", + "7": "111011", + "8": "111100", + "9": "111101", + "+": "111110", + "/": "111111", +} + + +def base64_to_bin(base64_str: str) -> str: + """Convert a base64 value to its binary equivalent. + + >>> base64_to_bin("ABC") + '000000000001000010' + >>> base64_to_bin(" -abc ") + '-011010011011011100' + >>> base64_to_bin(" a-bc ") + Traceback (most recent call last): + ... + ValueError: Invalid base64 string. Invalid char - at pos 1 + >>> base64_to_bin("") + Traceback (most recent call last): + ... + ValueError: Empty string was passed to the function + """ + b64_str = str(base64_str).strip() + if not b64_str: + raise ValueError("Empty string was passed to the function") + is_negative = b64_str[0] == "-" + if is_negative: + b64_str = b64_str[1:] + bin_str = "" + for i, x in enumerate(b64_str): + if x not in B64_TO_BITS: + exception_message = f"Invalid base64 string. Invalid char {x} at pos {i}" + raise ValueError(exception_message) + bin_str += B64_TO_BITS[x] + if is_negative: + bin_str = "-" + bin_str + return bin_str + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/binary_to_base64.py b/conversions/binary_to_base64.py new file mode 100644 index 000000000000..1ea9230cda76 --- /dev/null +++ b/conversions/binary_to_base64.py @@ -0,0 +1,120 @@ +""" +* Author: Siddharth Singh (https://github.com/coolsidd) +* Description: Convert a binary string to Base64. + +References for better understanding: +https://en.wikipedia.org/wiki/Base64 +""" + +BITS_TO_B64 = { + "000000": "A", + "000001": "B", + "000010": "C", + "000011": "D", + "000100": "E", + "000101": "F", + "000110": "G", + "000111": "H", + "001000": "I", + "001001": "J", + "001010": "K", + "001011": "L", + "001100": "M", + "001101": "N", + "001110": "O", + "001111": "P", + "010000": "Q", + "010001": "R", + "010010": "S", + "010011": "T", + "010100": "U", + "010101": "V", + "010110": "W", + "010111": "X", + "011000": "Y", + "011001": "Z", + "011010": "a", + "011011": "b", + "011100": "c", + "011101": "d", + "011110": "e", + "011111": "f", + "100000": "g", + "100001": "h", + "100010": "i", + "100011": "j", + "100100": "k", + "100101": "l", + "100110": "m", + "100111": "n", + "101000": "o", + "101001": "p", + "101010": "q", + "101011": "r", + "101100": "s", + "101101": "t", + "101110": "u", + "101111": "v", + "110000": "w", + "110001": "x", + "110010": "y", + "110011": "z", + "110100": "0", + "110101": "1", + "110110": "2", + "110111": "3", + "111000": "4", + "111001": "5", + "111010": "6", + "111011": "7", + "111100": "8", + "111101": "9", + "111110": "+", + "111111": "/", +} + + +def bin_to_base64(bin_str: str) -> str: + """Convert a binary value to its base64 equivalent + + + >>> bin_to_base64("000001") + 'AB' + >>> bin_to_base64("0") + 'A' + >>> bin_to_base64("1") + 'B' + >>> bin_to_base64("-1") + '-B' + >>> bin_to_base64(" -000001 ") + '-AB' + >>> bin_to_base64("0-0") + Traceback (most recent call last): + ... + ValueError: Non-binary value was passed to the function + >>> bin_to_base64("") + Traceback (most recent call last): + ... + ValueError: Empty string was passed to the function + """ + bin_str = str(bin_str).strip() + if not bin_str: + raise ValueError("Empty string was passed to the function") + is_negative = bin_str[0] == "-" + if is_negative: + bin_str = bin_str[1:] + if not all(char in "01" for char in bin_str): + raise ValueError("Non-binary value was passed to the function") + bin_str = "0" * (6 * (divmod(len(bin_str), 6)[0] + 1) - len(bin_str)) + bin_str + base64_str = "" + while len(bin_str) > 0: + base64_str += BITS_TO_B64[bin_str[:6]] + bin_str = bin_str[6:] + + return "-" + base64_str if is_negative else base64_str + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/conversions/binary_to_decimal.py b/conversions/binary_to_decimal.py new file mode 100644 index 000000000000..914a9318c225 --- /dev/null +++ b/conversions/binary_to_decimal.py @@ -0,0 +1,43 @@ +def bin_to_decimal(bin_string: str) -> int: + """ + Convert a binary value to its decimal equivalent + + >>> bin_to_decimal("101") + 5 + >>> bin_to_decimal(" 1010 ") + 10 + >>> bin_to_decimal("-11101") + -29 + >>> bin_to_decimal("0") + 0 + >>> bin_to_decimal("a") + Traceback (most recent call last): + ... + ValueError: Non-binary value was passed to the function + >>> bin_to_decimal("") + Traceback (most recent call last): + ... + ValueError: Empty string was passed to the function + >>> bin_to_decimal("39") + Traceback (most recent call last): + ... + ValueError: Non-binary value was passed to the function + """ + bin_string = str(bin_string).strip() + if not bin_string: + raise ValueError("Empty string was passed to the function") + is_negative = bin_string[0] == "-" + if is_negative: + bin_string = bin_string[1:] + if not all(char in "01" for char in bin_string): + raise ValueError("Non-binary value was passed to the function") + decimal_number = 0 + for char in bin_string: + decimal_number = 2 * decimal_number + int(char) + return -decimal_number if is_negative else decimal_number + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/conversions/binary_to_excess3.py b/conversions/binary_to_excess3.py new file mode 100644 index 000000000000..c410450df9dd --- /dev/null +++ b/conversions/binary_to_excess3.py @@ -0,0 +1,34 @@ +def binary_to_excess3(binary_str: str) -> str: + """ + Convert a binary number (as a string) to its Excess-3 code. + https://en.wikipedia.org/wiki/Excess-3 + + Args: + binary_str (str): Binary number as a string (e.g., "1010"). + + Returns: + str: Excess-3 code as a binary string. + + Example: + >>> binary_to_excess3("1010") + '1101' + """ + # Convert binary to decimal + decimal_value = int(binary_str, 2) + + # Add 3 (Excess-3 encoding) + excess3_value = decimal_value + 3 + + # Convert back to 4-bit binary + excess3_binary = format(excess3_value, "04b") + + return excess3_binary + + +if __name__ == "__main__": + from dostest import testmod + + testmod() + binary_input = input("Enter a 4-bit binary number: ") + excess3_output = binary_to_excess3(binary_input) + print(f"Excess-3 code of {binary_input} is: {excess3_output}") diff --git a/conversions/binary_to_gray.py b/conversions/binary_to_gray.py new file mode 100644 index 000000000000..f68e3a8832dd --- /dev/null +++ b/conversions/binary_to_gray.py @@ -0,0 +1,50 @@ +""" +Binary to Gray code conversion algorithm. + +Reference: +https://en.wikipedia.org/wiki/Gray_code +""" + + +def binary_to_gray(binary: str) -> str: + """ + Convert a binary number (as string) to its equivalent Gray code. + + The Gray code is generated by XOR-ing each bit with the bit just before it. + + Args: + binary (str): A string representing a binary number (e.g., "10101010"). + + Returns: + str: The corresponding Gray code string. + + Example: + >>> binary_to_gray("10101010") + '11111111' + + >>> binary_to_gray("1101") + '1011' + """ + # Convert binary string to integer + binary_int = int(binary, 2) + + # XOR the binary number with itself shifted right by 1 bit + gray_int = binary_int ^ (binary_int >> 1) + + # Convert the integer result back to a binary string (remove '0b' prefix) + gray_code = bin(gray_int)[2:] + + # Pad with leading zeros to maintain same bit length as input + return gray_code.zfill(len(binary)) + + +if __name__ == "__main__": + # Take input from user + binary_input = input("Enter a binary number: ").strip() + + # Validate the input (only 0s and 1s allowed) + if not all(bit in "01" for bit in binary_input): + print("â�Œ Invalid input! Please enter only 0s and 1s.") + else: + result = binary_to_gray(binary_input) + print(f"✅ The Gray code for binary {binary_input} is: {result}") diff --git a/conversions/binary_to_gray_code.py b/conversions/binary_to_gray_code.py new file mode 100644 index 000000000000..494cad66f503 --- /dev/null +++ b/conversions/binary_to_gray_code.py @@ -0,0 +1,224 @@ +""" +Convert between Binary and Gray Code representations. + +Gray Code (also known as reflected binary code) is a binary numeral system where +two successive values differ in only one bit. This property makes it useful in +error correction, digital communications, and position encoders. + +Wikipedia: https://en.wikipedia.org/wiki/Gray_code +""" + + +def binary_to_gray(binary_number: int) -> int: + """ + Convert a binary number to its Gray Code equivalent. + + The algorithm works by XORing the binary number with itself right-shifted by 1. + Formula: gray = binary XOR (binary >> 1) + + Args: + binary_number: A positive integer representing a binary number + + Returns: + The Gray Code equivalent as an integer + + Examples: + >>> binary_to_gray(0) + 0 + >>> binary_to_gray(1) + 1 + >>> binary_to_gray(2) + 3 + >>> binary_to_gray(3) + 2 + >>> binary_to_gray(4) + 6 + >>> binary_to_gray(7) + 4 + >>> binary_to_gray(10) + 15 + >>> binary_to_gray(15) + 8 + >>> binary_to_gray(255) + 128 + >>> binary_to_gray(-1) + Traceback (most recent call last): + ... + ValueError: Input must be a non-negative integer + >>> binary_to_gray(3.5) + Traceback (most recent call last): + ... + TypeError: Input must be an integer + """ + if not isinstance(binary_number, int): + raise TypeError("Input must be an integer") + if binary_number < 0: + raise ValueError("Input must be a non-negative integer") + + return binary_number ^ (binary_number >> 1) + + +def gray_to_binary(gray_number: int) -> int: + """ + Convert a Gray Code number to its binary equivalent. + + The algorithm works by repeatedly XORing the gray code with itself right-shifted, + until the right-shifted value becomes 0. + + Args: + gray_number: A positive integer representing a Gray Code number + + Returns: + The binary equivalent as an integer + + Examples: + >>> gray_to_binary(0) + 0 + >>> gray_to_binary(1) + 1 + >>> gray_to_binary(3) + 2 + >>> gray_to_binary(2) + 3 + >>> gray_to_binary(6) + 4 + >>> gray_to_binary(4) + 7 + >>> gray_to_binary(15) + 10 + >>> gray_to_binary(8) + 15 + >>> gray_to_binary(128) + 255 + >>> gray_to_binary(-1) + Traceback (most recent call last): + ... + ValueError: Input must be a non-negative integer + >>> gray_to_binary(5.5) + Traceback (most recent call last): + ... + TypeError: Input must be an integer + """ + if not isinstance(gray_number, int): + raise TypeError("Input must be an integer") + if gray_number < 0: + raise ValueError("Input must be a non-negative integer") + + binary_number = gray_number + gray_number >>= 1 + + while gray_number: + binary_number ^= gray_number + gray_number >>= 1 + + return binary_number + + +def decimal_to_gray(decimal_number: int) -> str: + """ + Convert a decimal number to its Gray Code representation as a binary string. + + Args: + decimal_number: A positive integer in decimal + + Returns: + Gray Code representation as a binary string + + Examples: + >>> decimal_to_gray(0) + '0' + >>> decimal_to_gray(1) + '1' + >>> decimal_to_gray(2) + '11' + >>> decimal_to_gray(3) + '10' + >>> decimal_to_gray(4) + '110' + >>> decimal_to_gray(10) + '1111' + >>> decimal_to_gray(15) + '1000' + >>> decimal_to_gray(-1) + Traceback (most recent call last): + ... + ValueError: Input must be a non-negative integer + """ + if not isinstance(decimal_number, int): + raise TypeError("Input must be an integer") + if decimal_number < 0: + raise ValueError("Input must be a non-negative integer") + + gray_code = binary_to_gray(decimal_number) + return bin(gray_code)[2:] # Remove '0b' prefix + + +def gray_to_decimal(gray_string: str) -> int: + """ + Convert a Gray Code binary string to its decimal equivalent. + + Args: + gray_string: A string of 0s and 1s representing Gray Code + + Returns: + The decimal equivalent as an integer + + Examples: + >>> gray_to_decimal('0') + 0 + >>> gray_to_decimal('1') + 1 + >>> gray_to_decimal('11') + 2 + >>> gray_to_decimal('10') + 3 + >>> gray_to_decimal('110') + 4 + >>> gray_to_decimal('1111') + 10 + >>> gray_to_decimal('1000') + 15 + >>> gray_to_decimal('invalid') + Traceback (most recent call last): + ... + ValueError: Invalid binary string + >>> gray_to_decimal('') + Traceback (most recent call last): + ... + ValueError: Input string cannot be empty + """ + if not gray_string: + raise ValueError("Input string cannot be empty") + + # Validate binary string + if not all(bit in "01" for bit in gray_string): + raise ValueError("Invalid binary string") + + gray_number = int(gray_string, 2) + return gray_to_binary(gray_number) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + # Interactive demonstration + print("=== Binary to Gray Code Converter ===\n") + + # Demonstrate conversions for 0-15 + print("Decimal | Binary | Gray Code") + print("--------|----------|----------") + for i in range(16): + binary = bin(i)[2:].zfill(4) + gray = decimal_to_gray(i).zfill(4) + print(f"{i:7} | {binary:8} | {gray:9}") + + print("\n=== Verification: Gray to Binary ===\n") + print("Gray Code | Binary | Decimal") + print("----------|----------|--------") + for i in range(16): + gray = decimal_to_gray(i).zfill(4) + decimal = gray_to_decimal(gray) + binary = bin(decimal)[2:].zfill(4) + print(f"{gray:9} | {binary:8} | {decimal:7}") diff --git a/conversions/binary_to_hexadecimal.py b/conversions/binary_to_hexadecimal.py new file mode 100644 index 000000000000..a3855bb70b52 --- /dev/null +++ b/conversions/binary_to_hexadecimal.py @@ -0,0 +1,66 @@ +BITS_TO_HEX = { + "0000": "0", + "0001": "1", + "0010": "2", + "0011": "3", + "0100": "4", + "0101": "5", + "0110": "6", + "0111": "7", + "1000": "8", + "1001": "9", + "1010": "a", + "1011": "b", + "1100": "c", + "1101": "d", + "1110": "e", + "1111": "f", +} + + +def bin_to_hexadecimal(binary_str: str) -> str: + """ + Converting a binary string into hexadecimal using Grouping Method + + >>> bin_to_hexadecimal('101011111') + '0x15f' + >>> bin_to_hexadecimal(' 1010 ') + '0x0a' + >>> bin_to_hexadecimal('-11101') + '-0x1d' + >>> bin_to_hexadecimal('a') + Traceback (most recent call last): + ... + ValueError: Non-binary value was passed to the function + >>> bin_to_hexadecimal('') + Traceback (most recent call last): + ... + ValueError: Empty string was passed to the function + """ + # Sanitising parameter + binary_str = str(binary_str).strip() + + # Exceptions + if not binary_str: + raise ValueError("Empty string was passed to the function") + is_negative = binary_str[0] == "-" + binary_str = binary_str[1:] if is_negative else binary_str + if not all(char in "01" for char in binary_str): + raise ValueError("Non-binary value was passed to the function") + + binary_str = ( + "0" * (4 * (divmod(len(binary_str), 4)[0] + 1) - len(binary_str)) + binary_str + ) + + hexadecimal = [] + for x in range(0, len(binary_str), 4): + hexadecimal.append(BITS_TO_HEX[binary_str[x : x + 4]]) + hexadecimal_str = "0x" + "".join(hexadecimal) + + return "-" + hexadecimal_str if is_negative else hexadecimal_str + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/conversions/binary_to_octal.py b/conversions/binary_to_octal.py new file mode 100644 index 000000000000..82f81e06234a --- /dev/null +++ b/conversions/binary_to_octal.py @@ -0,0 +1,45 @@ +""" +The function below will convert any binary string to the octal equivalent. + +>>> bin_to_octal("1111") +'17' + +>>> bin_to_octal("101010101010011") +'52523' + +>>> bin_to_octal("") +Traceback (most recent call last): + ... +ValueError: Empty string was passed to the function +>>> bin_to_octal("a-1") +Traceback (most recent call last): + ... +ValueError: Non-binary value was passed to the function +""" + + +def bin_to_octal(bin_string: str) -> str: + if not all(char in "01" for char in bin_string): + raise ValueError("Non-binary value was passed to the function") + if not bin_string: + raise ValueError("Empty string was passed to the function") + oct_string = "" + while len(bin_string) % 3 != 0: + bin_string = "0" + bin_string + bin_string_in_3_list = [ + bin_string[index : index + 3] + for index in range(len(bin_string)) + if index % 3 == 0 + ] + for bin_group in bin_string_in_3_list: + oct_val = 0 + for index, val in enumerate(bin_group): + oct_val += int(2 ** (2 - index) * int(val)) + oct_string += str(oct_val) + return oct_string + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/conversions/convert_number_to_words.py b/conversions/convert_number_to_words.py new file mode 100644 index 000000000000..6aa43738b9fe --- /dev/null +++ b/conversions/convert_number_to_words.py @@ -0,0 +1,205 @@ +from enum import Enum +from typing import Literal + + +class NumberingSystem(Enum): + SHORT = ( + (15, "quadrillion"), + (12, "trillion"), + (9, "billion"), + (6, "million"), + (3, "thousand"), + (2, "hundred"), + ) + + LONG = ( + (15, "billiard"), + (9, "milliard"), + (6, "million"), + (3, "thousand"), + (2, "hundred"), + ) + + INDIAN = ( + (14, "crore crore"), + (12, "lakh crore"), + (7, "crore"), + (5, "lakh"), + (3, "thousand"), + (2, "hundred"), + ) + + @classmethod + def max_value(cls, system: str) -> int: + """ + Gets the max value supported by the given number system. + + >>> NumberingSystem.max_value("short") == 10**18 - 1 + True + >>> NumberingSystem.max_value("long") == 10**21 - 1 + True + >>> NumberingSystem.max_value("indian") == 10**19 - 1 + True + """ + match system_enum := cls[system.upper()]: + case cls.SHORT: + max_exp = system_enum.value[0][0] + 3 + case cls.LONG: + max_exp = system_enum.value[0][0] + 6 + case cls.INDIAN: + max_exp = 19 + case _: + raise ValueError("Invalid numbering system") + return 10**max_exp - 1 + + +class NumberWords(Enum): + ONES = { # noqa: RUF012 + 0: "", + 1: "one", + 2: "two", + 3: "three", + 4: "four", + 5: "five", + 6: "six", + 7: "seven", + 8: "eight", + 9: "nine", + } + + TEENS = { # noqa: RUF012 + 0: "ten", + 1: "eleven", + 2: "twelve", + 3: "thirteen", + 4: "fourteen", + 5: "fifteen", + 6: "sixteen", + 7: "seventeen", + 8: "eighteen", + 9: "nineteen", + } + + TENS = { # noqa: RUF012 + 2: "twenty", + 3: "thirty", + 4: "forty", + 5: "fifty", + 6: "sixty", + 7: "seventy", + 8: "eighty", + 9: "ninety", + } + + +def convert_small_number(num: int) -> str: + """ + Converts small, non-negative integers with irregular constructions in English (i.e., + numbers under 100) into words. + + >>> convert_small_number(0) + 'zero' + >>> convert_small_number(5) + 'five' + >>> convert_small_number(10) + 'ten' + >>> convert_small_number(15) + 'fifteen' + >>> convert_small_number(20) + 'twenty' + >>> convert_small_number(25) + 'twenty-five' + >>> convert_small_number(-1) + Traceback (most recent call last): + ... + ValueError: This function only accepts non-negative integers + >>> convert_small_number(123) + Traceback (most recent call last): + ... + ValueError: This function only converts numbers less than 100 + """ + if num < 0: + raise ValueError("This function only accepts non-negative integers") + if num >= 100: + raise ValueError("This function only converts numbers less than 100") + tens, ones = divmod(num, 10) + if tens == 0: + return NumberWords.ONES.value[ones] or "zero" + if tens == 1: + return NumberWords.TEENS.value[ones] + return ( + NumberWords.TENS.value[tens] + + ("-" if NumberWords.ONES.value[ones] else "") + + NumberWords.ONES.value[ones] + ) + + +def convert_number( + num: int, system: Literal["short", "long", "indian"] = "short" +) -> str: + """ + Converts an integer to English words. + + :param num: The integer to be converted + :param system: The numbering system (short, long, or Indian) + + >>> convert_number(0) + 'zero' + >>> convert_number(1) + 'one' + >>> convert_number(100) + 'one hundred' + >>> convert_number(-100) + 'negative one hundred' + >>> convert_number(123_456_789_012_345) # doctest: +NORMALIZE_WHITESPACE + 'one hundred twenty-three trillion four hundred fifty-six billion + seven hundred eighty-nine million twelve thousand three hundred forty-five' + >>> convert_number(123_456_789_012_345, "long") # doctest: +NORMALIZE_WHITESPACE + 'one hundred twenty-three thousand four hundred fifty-six milliard + seven hundred eighty-nine million twelve thousand three hundred forty-five' + >>> convert_number(12_34_56_78_90_12_345, "indian") # doctest: +NORMALIZE_WHITESPACE + 'one crore crore twenty-three lakh crore + forty-five thousand six hundred seventy-eight crore + ninety lakh twelve thousand three hundred forty-five' + >>> convert_number(10**18) + Traceback (most recent call last): + ... + ValueError: Input number is too large + >>> convert_number(10**21, "long") + Traceback (most recent call last): + ... + ValueError: Input number is too large + >>> convert_number(10**19, "indian") + Traceback (most recent call last): + ... + ValueError: Input number is too large + """ + word_groups = [] + + if num < 0: + word_groups.append("negative") + num *= -1 + + if num > NumberingSystem.max_value(system): + raise ValueError("Input number is too large") + + for power, unit in NumberingSystem[system.upper()].value: + digit_group, num = divmod(num, 10**power) + if digit_group > 0: + word_group = ( + convert_number(digit_group, system) + if digit_group >= 100 + else convert_small_number(digit_group) + ) + word_groups.append(f"{word_group} {unit}") + if num > 0 or not word_groups: # word_groups is only empty if input num was 0 + word_groups.append(convert_small_number(num)) + return " ".join(word_groups) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + print(f"{convert_number(123456789) = }") diff --git a/conversions/decimal_to_any.py b/conversions/decimal_to_any.py new file mode 100644 index 000000000000..b257761eff40 --- /dev/null +++ b/conversions/decimal_to_any.py @@ -0,0 +1,106 @@ +"""Convert a positive Decimal Number to Any Other Representation""" + +from string import ascii_uppercase + +ALPHABET_VALUES = {str(ord(c) - 55): c for c in ascii_uppercase} + + +def decimal_to_any(num: int, base: int) -> str: + """ + Convert a positive integer to another base as str. + >>> decimal_to_any(0, 2) + '0' + >>> decimal_to_any(5, 4) + '11' + >>> decimal_to_any(20, 3) + '202' + >>> decimal_to_any(58, 16) + '3A' + >>> decimal_to_any(243, 17) + 'E5' + >>> decimal_to_any(34923, 36) + 'QY3' + >>> decimal_to_any(10, 11) + 'A' + >>> decimal_to_any(16, 16) + '10' + >>> decimal_to_any(36, 36) + '10' + >>> # negatives will error + >>> decimal_to_any(-45, 8) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: parameter must be positive int + >>> # floats will error + >>> decimal_to_any(34.4, 6) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + TypeError: int() can't convert non-string with explicit base + >>> # a float base will error + >>> decimal_to_any(5, 2.5) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + TypeError: 'float' object cannot be interpreted as an integer + >>> # a str base will error + >>> decimal_to_any(10, '16') # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + TypeError: 'str' object cannot be interpreted as an integer + >>> # a base less than 2 will error + >>> decimal_to_any(7, 0) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: base must be >= 2 + >>> # a base greater than 36 will error + >>> decimal_to_any(34, 37) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: base must be <= 36 + """ + if isinstance(num, float): + raise TypeError("int() can't convert non-string with explicit base") + if num < 0: + raise ValueError("parameter must be positive int") + if isinstance(base, str): + raise TypeError("'str' object cannot be interpreted as an integer") + if isinstance(base, float): + raise TypeError("'float' object cannot be interpreted as an integer") + if base in (0, 1): + raise ValueError("base must be >= 2") + if base > 36: + raise ValueError("base must be <= 36") + new_value = "" + mod = 0 + div = 0 + while div != 1: + div, mod = divmod(num, base) + if base >= 11 and 9 < mod < 36: + actual_value = ALPHABET_VALUES[str(mod)] + else: + actual_value = str(mod) + new_value += actual_value + div = num // base + num = div + match div: + case 0: + return str(new_value[::-1]) + case 1: + new_value += str(div) + return str(new_value[::-1]) + + return new_value[::-1] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + for base in range(2, 37): + for num in range(1000): + assert int(decimal_to_any(num, base), base) == num, ( + num, + base, + decimal_to_any(num, base), + int(decimal_to_any(num, base), base), + ) diff --git a/conversions/decimal_to_binary.py b/conversions/decimal_to_binary.py new file mode 100644 index 000000000000..cf2b6040ec2a --- /dev/null +++ b/conversions/decimal_to_binary.py @@ -0,0 +1,109 @@ +"""Convert a Decimal Number to a Binary Number.""" + + +def decimal_to_binary_iterative(num: int) -> str: + """ + Convert an Integer Decimal Number to a Binary Number as str. + >>> decimal_to_binary_iterative(0) + '0b0' + >>> decimal_to_binary_iterative(2) + '0b10' + >>> decimal_to_binary_iterative(7) + '0b111' + >>> decimal_to_binary_iterative(35) + '0b100011' + >>> # negatives work too + >>> decimal_to_binary_iterative(-2) + '-0b10' + >>> # other floats will error + >>> decimal_to_binary_iterative(16.16) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + TypeError: 'float' object cannot be interpreted as an integer + >>> # strings will error as well + >>> decimal_to_binary_iterative('0xfffff') # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + TypeError: 'str' object cannot be interpreted as an integer + """ + + if isinstance(num, float): + raise TypeError("'float' object cannot be interpreted as an integer") + if isinstance(num, str): + raise TypeError("'str' object cannot be interpreted as an integer") + + if num == 0: + return "0b0" + + negative = False + + if num < 0: + negative = True + num = -num + + binary: list[int] = [] + while num > 0: + binary.insert(0, num % 2) + num >>= 1 + + if negative: + return "-0b" + "".join(str(e) for e in binary) + + return "0b" + "".join(str(e) for e in binary) + + +def decimal_to_binary_recursive_helper(decimal: int) -> str: + """ + Take a positive integer value and return its binary equivalent. + >>> decimal_to_binary_recursive_helper(1000) + '1111101000' + >>> decimal_to_binary_recursive_helper("72") + '1001000' + >>> decimal_to_binary_recursive_helper("number") + Traceback (most recent call last): + ... + ValueError: invalid literal for int() with base 10: 'number' + """ + decimal = int(decimal) + if decimal in (0, 1): # Exit cases for the recursion + return str(decimal) + div, mod = divmod(decimal, 2) + return decimal_to_binary_recursive_helper(div) + str(mod) + + +def decimal_to_binary_recursive(number: str) -> str: + """ + Take an integer value and raise ValueError for wrong inputs, + call the function above and return the output with prefix "0b" & "-0b" + for positive and negative integers respectively. + >>> decimal_to_binary_recursive(0) + '0b0' + >>> decimal_to_binary_recursive(40) + '0b101000' + >>> decimal_to_binary_recursive(-40) + '-0b101000' + >>> decimal_to_binary_recursive(40.8) + Traceback (most recent call last): + ... + ValueError: Input value is not an integer + >>> decimal_to_binary_recursive("forty") + Traceback (most recent call last): + ... + ValueError: Input value is not an integer + """ + number = str(number).strip() + if not number: + raise ValueError("No input value was provided") + negative = "-" if number.startswith("-") else "" + number = number.lstrip("-") + if not number.isnumeric(): + raise ValueError("Input value is not an integer") + return f"{negative}0b{decimal_to_binary_recursive_helper(int(number))}" + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + print(decimal_to_binary_recursive(input("Input a decimal number: "))) diff --git a/conversions/decimal_to_hexadecimal.py b/conversions/decimal_to_hexadecimal.py new file mode 100644 index 000000000000..ee79592de5ca --- /dev/null +++ b/conversions/decimal_to_hexadecimal.py @@ -0,0 +1,80 @@ +"""Convert Base 10 (Decimal) Values to Hexadecimal Representations""" + +# set decimal value for each hexadecimal digit +values = { + 0: "0", + 1: "1", + 2: "2", + 3: "3", + 4: "4", + 5: "5", + 6: "6", + 7: "7", + 8: "8", + 9: "9", + 10: "a", + 11: "b", + 12: "c", + 13: "d", + 14: "e", + 15: "f", +} + + +def decimal_to_hexadecimal(decimal: float) -> str: + """ + take integer decimal value, return hexadecimal representation as str beginning + with 0x + >>> decimal_to_hexadecimal(5) + '0x5' + >>> decimal_to_hexadecimal(15) + '0xf' + >>> decimal_to_hexadecimal(37) + '0x25' + >>> decimal_to_hexadecimal(255) + '0xff' + >>> decimal_to_hexadecimal(4096) + '0x1000' + >>> decimal_to_hexadecimal(999098) + '0xf3eba' + >>> # negatives work too + >>> decimal_to_hexadecimal(-256) + '-0x100' + >>> # floats are acceptable if equivalent to an int + >>> decimal_to_hexadecimal(17.0) + '0x11' + >>> # other floats will error + >>> decimal_to_hexadecimal(16.16) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + AssertionError + >>> # strings will error as well + >>> decimal_to_hexadecimal('0xfffff') # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + AssertionError + >>> # results are the same when compared to Python's default hex function + >>> decimal_to_hexadecimal(-256) == hex(-256) + True + """ + assert isinstance(decimal, (int, float)) + assert decimal == int(decimal) + decimal = int(decimal) + hexadecimal = "" + negative = False + if decimal < 0: + negative = True + decimal *= -1 + while decimal > 0: + decimal, remainder = divmod(decimal, 16) + hexadecimal = values[remainder] + hexadecimal + hexadecimal = "0x" + hexadecimal + if negative: + hexadecimal = "-" + hexadecimal + return hexadecimal + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/decimal_to_octal.py b/conversions/decimal_to_octal.py new file mode 100644 index 000000000000..4c313bddf64c --- /dev/null +++ b/conversions/decimal_to_octal.py @@ -0,0 +1,43 @@ +"""Convert a Decimal Number to an Octal Number.""" + +import math + +# Modified from: +# https://github.com/TheAlgorithms/Javascript/blob/master/Conversions/DecimalToOctal.js + + +def decimal_to_octal(num: int) -> str: + """Convert a Decimal Number to an Octal Number. + + >>> all(decimal_to_octal(i) == oct(i) for i + ... in (0, 2, 8, 64, 65, 216, 255, 256, 512)) + True + """ + octal = 0 + counter = 0 + while num > 0: + remainder = num % 8 + octal = octal + (remainder * math.floor(math.pow(10, counter))) + counter += 1 + num = math.floor(num / 8) # basically /= 8 without remainder if any + # This formatting removes trailing '.0' from `octal`. + return f"0o{int(octal)}" + + +def main() -> None: + """Print octal equivalents of decimal numbers.""" + print("\n2 in octal is:") + print(decimal_to_octal(2)) # = 2 + print("\n8 in octal is:") + print(decimal_to_octal(8)) # = 10 + print("\n65 in octal is:") + print(decimal_to_octal(65)) # = 101 + print("\n216 in octal is:") + print(decimal_to_octal(216)) # = 330 + print("\n512 in octal is:") + print(decimal_to_octal(512)) # = 1000 + print("\n") + + +if __name__ == "__main__": + main() diff --git a/conversions/endianness.py b/conversions/endianness.py new file mode 100644 index 000000000000..291112fa42a2 --- /dev/null +++ b/conversions/endianness.py @@ -0,0 +1,329 @@ +""" +Endianness Conversion Algorithm + +This module implements endianness (byte order) conversion utilities for converting +between big-endian and little-endian representations of multi-byte integers. + +Endianness refers to the order of bytes in a multi-byte data type: +- Big-endian: Most significant byte first (e.g., 0x12345678 → [0x12, 0x34, 0x56, 0x78]) +- Little-endian: Least significant byte first + (e.g., 0x12345678 → [0x78, 0x56, 0x34, 0x12]) + +Common uses: +- Network protocols (TCP/IP uses big-endian for multi-byte fields) +- File format parsing (PNG, JPEG headers specify endianness) +- Cryptographic algorithms (MD5 uses little-endian, SHA-256 uses big-endian) +- Binary data serialization (Protocol Buffers, MessagePack) +- Hardware interfacing (ARM is bi-endian, x86 is strictly little-endian) +- Cross-platform data exchange + +Real-world examples: +- IP addresses are transmitted in big-endian (network byte order) +- Modern ARM and x86 processors typically operate in little-endian mode +- Java class files use big-endian format +- Bitcoin uses little-endian for block hashes +- USB and PCI protocols use little-endian + +References: +- https://en.wikipedia.org/wiki/Endianness +- RFC 1700 (Network Byte Order) +- https://tools.ietf.org/html/rfc1700 +""" + + +def swap_endianness_16(value: int) -> int: + """ + Swap the byte order of a 16-bit integer. + + Args: + value: 16-bit integer (0 to 65,535) + + Returns: + Integer with swapped byte order + + Raises: + ValueError: If value is negative or exceeds 16-bit range + + >>> swap_endianness_16(0x1234) + 13330 + + >>> hex(swap_endianness_16(0x1234)) + '0x3412' + + >>> swap_endianness_16(0xABCD) + 52651 + + >>> hex(swap_endianness_16(0xABCD)) + '0xcdab' + + >>> swap_endianness_16(0) + 0 + + >>> swap_endianness_16(0xFFFF) + 65535 + """ + if value < 0 or value > 0xFFFF: + msg = f"value must be between 0 and {0xFFFF}" + raise ValueError(msg) + + return ((value & 0xFF) << 8) | ((value & 0xFF00) >> 8) + + +def swap_endianness_32(value: int) -> int: + """ + Swap the byte order of a 32-bit integer. + + Args: + value: 32-bit integer (0 to 4,294,967,295) + + Returns: + Integer with swapped byte order + + Raises: + ValueError: If value is negative or exceeds 32-bit range + + >>> swap_endianness_32(0x12345678) + 2018915346 + + >>> hex(swap_endianness_32(0x12345678)) + '0x78563412' + + >>> swap_endianness_32(0xDEADBEEF) + 4022250974 + + >>> hex(swap_endianness_32(0xDEADBEEF)) + '0xefbeadde' + + >>> swap_endianness_32(0) + 0 + + >>> swap_endianness_32(0xFFFFFFFF) + 4294967295 + + >>> swap_endianness_32(0x01020304) + 67305985 + """ + if value < 0 or value > 0xFFFFFFFF: + msg = f"value must be between 0 and {0xFFFFFFFF}" + raise ValueError(msg) + + return ( + ((value & 0x000000FF) << 24) + | ((value & 0x0000FF00) << 8) + | ((value & 0x00FF0000) >> 8) + | ((value & 0xFF000000) >> 24) + ) + + +def swap_endianness_64(value: int) -> int: + """ + Swap the byte order of a 64-bit integer. + + Args: + value: 64-bit integer (0 to 18,446,744,073,709,551,615) + + Returns: + Integer with swapped byte order + + Raises: + ValueError: If value is negative or exceeds 64-bit range + + >>> swap_endianness_64(0x0123456789ABCDEF) + 17279655951921914625 + + >>> hex(swap_endianness_64(0x0123456789ABCDEF)) + '0xefcdab8967452301' + + >>> swap_endianness_64(0xFEDCBA9876543210) + 1167088121787636990 + + >>> hex(swap_endianness_64(0xFEDCBA9876543210)) + '0x1032547698badcfe' + + >>> swap_endianness_64(0) + 0 + + >>> swap_endianness_64(0xFFFFFFFFFFFFFFFF) + 18446744073709551615 + """ + if value < 0 or value > 0xFFFFFFFFFFFFFFFF: + msg = f"value must be between 0 and {0xFFFFFFFFFFFFFFFF}" + raise ValueError(msg) + + return ( + ((value & 0x00000000000000FF) << 56) + | ((value & 0x000000000000FF00) << 40) + | ((value & 0x0000000000FF0000) << 24) + | ((value & 0x00000000FF000000) << 8) + | ((value & 0x000000FF00000000) >> 8) + | ((value & 0x0000FF0000000000) >> 24) + | ((value & 0x00FF000000000000) >> 40) + | ((value & 0xFF00000000000000) >> 56) + ) + + +def bytes_to_int_little(data: bytes) -> int: + """ + Convert bytes to integer using little-endian byte order. + + Args: + data: Byte sequence to convert (1-8 bytes) + + Returns: + Integer representation in little-endian order + + Raises: + TypeError: If data is not bytes + ValueError: If data is empty or exceeds 8 bytes + + >>> bytes_to_int_little(b'\\x78\\x56\\x34\\x12') + 305419896 + + >>> hex(bytes_to_int_little(b'\\x78\\x56\\x34\\x12')) + '0x12345678' + + >>> bytes_to_int_little(b'\\x01\\x02') + 513 + + >>> bytes_to_int_little(b'\\xff') + 255 + + >>> bytes_to_int_little(b'\\x00\\x00\\x00\\x01') + 16777216 + """ + if not data or len(data) > 8: + msg = "data must be between 1 and 8 bytes" + raise ValueError(msg) + + return int.from_bytes(data, byteorder="little") + + +def bytes_to_int_big(data: bytes) -> int: + """ + Convert bytes to integer using big-endian byte order. + + Args: + data: Byte sequence to convert (1-8 bytes) + + Returns: + Integer representation in big-endian order + + Raises: + TypeError: If data is not bytes + ValueError: If data is empty or exceeds 8 bytes + + >>> bytes_to_int_big(b'\\x12\\x34\\x56\\x78') + 305419896 + + >>> hex(bytes_to_int_big(b'\\x12\\x34\\x56\\x78')) + '0x12345678' + + >>> bytes_to_int_big(b'\\x01\\x02') + 258 + + >>> bytes_to_int_big(b'\\xff') + 255 + + >>> bytes_to_int_big(b'\\x00\\x00\\x00\\x01') + 1 + """ + if not data or len(data) > 8: + msg = "data must be between 1 and 8 bytes" + raise ValueError(msg) + + return int.from_bytes(data, byteorder="big") + + +def int_to_bytes_little(value: int, num_bytes: int) -> bytes: + """ + Convert integer to bytes using little-endian byte order. + + Args: + value: Integer to convert (non-negative) + num_bytes: Number of bytes in output (1-8) + + Returns: + Bytes representation in little-endian order + + Raises: + ValueError: If value is negative, num_bytes invalid, or value too large + + >>> int_to_bytes_little(0x12345678, 4) + b'xV4\\x12' + + >>> int_to_bytes_little(513, 2) + b'\\x01\\x02' + + >>> int_to_bytes_little(255, 1) + b'\\xff' + + >>> int_to_bytes_little(16777216, 4) + b'\\x00\\x00\\x00\\x01' + """ + if value < 0: + msg = "value must be non-negative" + raise ValueError(msg) + + if num_bytes < 1 or num_bytes > 8: + msg = "num_bytes must be between 1 and 8" + raise ValueError(msg) + + if value >= (1 << (num_bytes * 8)): + msg = f"value {value} too large for {num_bytes} bytes" + raise ValueError(msg) + + return value.to_bytes(num_bytes, byteorder="little") + + +def int_to_bytes_big(value: int, num_bytes: int) -> bytes: + """ + Convert integer to bytes using big-endian byte order. + + Args: + value: Integer to convert (non-negative) + num_bytes: Number of bytes in output (1-8) + + Returns: + Bytes representation in big-endian order + + Raises: + ValueError: If value is negative, num_bytes invalid, or value too large + + >>> int_to_bytes_big(0x12345678, 4) + b'\\x124Vx' + + >>> int_to_bytes_big(258, 2) + b'\\x01\\x02' + + >>> int_to_bytes_big(255, 1) + b'\\xff' + + >>> int_to_bytes_big(1, 4) + b'\\x00\\x00\\x00\\x01' + """ + if value < 0: + msg = "value must be non-negative" + raise ValueError(msg) + + if num_bytes < 1 or num_bytes > 8: + msg = "num_bytes must be between 1 and 8" + raise ValueError(msg) + + if value >= (1 << (num_bytes * 8)): + msg = f"value {value} too large for {num_bytes} bytes" + raise ValueError(msg) + + return value.to_bytes(num_bytes, byteorder="big") + + +if __name__ == "__main__": + import doctest + + _ = doctest.testmod() + + print("Endianness Conversion Examples:") + print(f"16-bit: 0x1234 → {hex(swap_endianness_16(0x1234))}") + print(f"32-bit: 0x12345678 → {hex(swap_endianness_32(0x12345678))}") + print(f"64-bit: 0x0123456789ABCDEF → {hex(swap_endianness_64(0x0123456789ABCDEF))}") + print(f"Bytes to int (little): {bytes_to_int_little(b'\x78\x56\x34\x12'):#x}") + print(f"Bytes to int (big): {bytes_to_int_big(b'\x12\x34\x56\x78'):#x}") diff --git a/conversions/energy_conversions.py b/conversions/energy_conversions.py new file mode 100644 index 000000000000..51de6b313928 --- /dev/null +++ b/conversions/energy_conversions.py @@ -0,0 +1,114 @@ +""" +Conversion of energy units. + +Available units: joule, kilojoule, megajoule, gigajoule,\ + wattsecond, watthour, kilowatthour, newtonmeter, calorie_nutr,\ + kilocalorie_nutr, electronvolt, britishthermalunit_it, footpound + +USAGE : +-> Import this file into their respective project. +-> Use the function energy_conversion() for conversion of energy units. +-> Parameters : + -> from_type : From which type you want to convert + -> to_type : To which type you want to convert + -> value : the value which you want to convert + +REFERENCES : +-> Wikipedia reference: https://en.wikipedia.org/wiki/Units_of_energy +-> Wikipedia reference: https://en.wikipedia.org/wiki/Joule +-> Wikipedia reference: https://en.wikipedia.org/wiki/Kilowatt-hour +-> Wikipedia reference: https://en.wikipedia.org/wiki/Newton-metre +-> Wikipedia reference: https://en.wikipedia.org/wiki/Calorie +-> Wikipedia reference: https://en.wikipedia.org/wiki/Electronvolt +-> Wikipedia reference: https://en.wikipedia.org/wiki/British_thermal_unit +-> Wikipedia reference: https://en.wikipedia.org/wiki/Foot-pound_(energy) +-> Unit converter reference: https://www.unitconverters.net/energy-converter.html +""" + +ENERGY_CONVERSION: dict[str, float] = { + "joule": 1.0, + "kilojoule": 1_000, + "megajoule": 1_000_000, + "gigajoule": 1_000_000_000, + "wattsecond": 1.0, + "watthour": 3_600, + "kilowatthour": 3_600_000, + "newtonmeter": 1.0, + "calorie_nutr": 4_186.8, + "kilocalorie_nutr": 4_186_800.00, + "electronvolt": 1.602_176_634e-19, + "britishthermalunit_it": 1_055.055_85, + "footpound": 1.355_818, +} + + +def energy_conversion(from_type: str, to_type: str, value: float) -> float: + """ + Conversion of energy units. + >>> energy_conversion("joule", "joule", 1) + 1.0 + >>> energy_conversion("joule", "kilojoule", 1) + 0.001 + >>> energy_conversion("joule", "megajoule", 1) + 1e-06 + >>> energy_conversion("joule", "gigajoule", 1) + 1e-09 + >>> energy_conversion("joule", "wattsecond", 1) + 1.0 + >>> energy_conversion("joule", "watthour", 1) + 0.0002777777777777778 + >>> energy_conversion("joule", "kilowatthour", 1) + 2.7777777777777776e-07 + >>> energy_conversion("joule", "newtonmeter", 1) + 1.0 + >>> energy_conversion("joule", "calorie_nutr", 1) + 0.00023884589662749592 + >>> energy_conversion("joule", "kilocalorie_nutr", 1) + 2.388458966274959e-07 + >>> energy_conversion("joule", "electronvolt", 1) + 6.241509074460763e+18 + >>> energy_conversion("joule", "britishthermalunit_it", 1) + 0.0009478171226670134 + >>> energy_conversion("joule", "footpound", 1) + 0.7375621211696556 + >>> energy_conversion("joule", "megajoule", 1000) + 0.001 + >>> energy_conversion("calorie_nutr", "kilocalorie_nutr", 1000) + 1.0 + >>> energy_conversion("kilowatthour", "joule", 10) + 36000000.0 + >>> energy_conversion("britishthermalunit_it", "footpound", 1) + 778.1692306784539 + >>> energy_conversion("watthour", "joule", "a") # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + TypeError: unsupported operand type(s) for /: 'str' and 'float' + >>> energy_conversion("wrongunit", "joule", 1) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Incorrect 'from_type' or 'to_type' value: 'wrongunit', 'joule' + Valid values are: joule, ... footpound + >>> energy_conversion("joule", "wrongunit", 1) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Incorrect 'from_type' or 'to_type' value: 'joule', 'wrongunit' + Valid values are: joule, ... footpound + >>> energy_conversion("123", "abc", 1) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Incorrect 'from_type' or 'to_type' value: '123', 'abc' + Valid values are: joule, ... footpound + """ + if to_type not in ENERGY_CONVERSION or from_type not in ENERGY_CONVERSION: + msg = ( + f"Incorrect 'from_type' or 'to_type' value: {from_type!r}, {to_type!r}\n" + f"Valid values are: {', '.join(ENERGY_CONVERSION)}" + ) + raise ValueError(msg) + return value * ENERGY_CONVERSION[from_type] / ENERGY_CONVERSION[to_type] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/excel_title_to_column.py b/conversions/excel_title_to_column.py new file mode 100644 index 000000000000..235b6a64a647 --- /dev/null +++ b/conversions/excel_title_to_column.py @@ -0,0 +1,49 @@ +def excel_title_to_column(column_title: str) -> int: + """ + Given a string column_title that represents + the column title in an Excel sheet, return + its corresponding column number. + + >>> excel_title_to_column("A") + 1 + >>> excel_title_to_column("B") + 2 + >>> excel_title_to_column("AB") + 28 + >>> excel_title_to_column("Z") + 26 + >>> excel_title_to_column("a") + 1 + >>> excel_title_to_column("ab") + 28 + >>> excel_title_to_column("") + Traceback (most recent call last): + ... + ValueError: Column title must contain only alphabetic characters. + >>> excel_title_to_column("A1") + Traceback (most recent call last): + ... + ValueError: Column title must contain only alphabetic characters. + """ + if not column_title or not column_title.isalpha(): + msg = "Column title must contain only alphabetic characters." + raise ValueError(msg) + + column_title = column_title.upper() + answer = 0 + index = len(column_title) - 1 + power = 0 + + while index >= 0: + value = (ord(column_title[index]) - 64) * pow(26, power) + answer += value + power += 1 + index -= 1 + + return answer + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/conversions/hex_to_bin.py b/conversions/hex_to_bin.py new file mode 100644 index 000000000000..b872ab5cbce6 --- /dev/null +++ b/conversions/hex_to_bin.py @@ -0,0 +1,56 @@ +def hex_to_bin(hex_num: str) -> int: + """ + Convert a hexadecimal value to its binary equivalent + #https://stackoverflow.com/questions/1425493/convert-hex-to-binary + Here, we have used the bitwise right shift operator: >> + Shifts the bits of the number to the right and fills 0 on voids left as a result. + Similar effect as of dividing the number with some power of two. + Example: + a = 10 + a >> 1 = 5 + + >>> hex_to_bin("AC") + 10101100 + >>> hex_to_bin("9A4") + 100110100100 + >>> hex_to_bin(" 12f ") + 100101111 + >>> hex_to_bin("FfFf") + 1111111111111111 + >>> hex_to_bin("-fFfF") + -1111111111111111 + >>> hex_to_bin("F-f") + Traceback (most recent call last): + ... + ValueError: Invalid value was passed to the function + >>> hex_to_bin("") + Traceback (most recent call last): + ... + ValueError: No value was passed to the function + """ + + hex_num = hex_num.strip() + if not hex_num: + raise ValueError("No value was passed to the function") + + is_negative = hex_num[0] == "-" + if is_negative: + hex_num = hex_num[1:] + + try: + int_num = int(hex_num, 16) + except ValueError: + raise ValueError("Invalid value was passed to the function") + + bin_str = "" + while int_num > 0: + bin_str = str(int_num % 2) + bin_str + int_num >>= 1 + + return int(("-" + bin_str) if is_negative else bin_str) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/hex_to_rgb.py b/conversions/hex_to_rgb.py new file mode 100644 index 000000000000..6d97acb4fe4e --- /dev/null +++ b/conversions/hex_to_rgb.py @@ -0,0 +1,79 @@ +""" +* Author: Cicero Tiago Carneiro Valentim (https://github.com/cicerotcv) +* Description: Convert hexadecimal (#FF2000) color to RGB (rgb(255, 32, 0)). + +References: +https://www.w3schools.com/colors/colors_rgb.asp +https://www.w3schools.com/colors/colors_hexadecimal.asp +""" + + +def hex_to_rgb(hex_color: str) -> str: + """ + Converts a hexadecimal color code to RGB values. + + Args: + hex_color (str): A hexadecimal color code, e.g., "#RGB" or "#RRGGBB". + + Returns: + str: A string representation of a rgb value "rgb(r, g, b)" containing + three integers. + + Raises: + ValueError: If the input hex_color is not a valid hexadecimal color code. + + Examples: + >>> hex_to_rgb("#FF0000") + 'rgb(255, 0, 0)' + + >>> hex_to_rgb("#00FF00") + 'rgb(0, 255, 0)' + + >>> hex_to_rgb("#123") + 'rgb(17, 34, 51)' + + >>> hex_to_rgb("#000000") + 'rgb(0, 0, 0)' + + >>> hex_to_rgb("#FFFFFF") + 'rgb(255, 255, 255)' + + >>> hex_to_rgb("#0088FF") + 'rgb(0, 136, 255)' + + >>> hex_to_rgb("#0032ccAA") + Traceback (most recent call last): + ... + ValueError: Invalid hex color code + + >>> hex_to_rgb("#0032") + Traceback (most recent call last): + ... + ValueError: Invalid hex color code + + Note: + - The function supports both 6-digit and 3-digit hex color codes. + """ + + hex_color = hex_color.lstrip("#") + + # Check if the input is a valid hex color code + if not (len(hex_color) == 6 or len(hex_color) == 3): + raise ValueError("Invalid hex color code") + + # Expand 3-digit hex codes to 6 digits (e.g., "#123" to "#112233") + if len(hex_color) == 3: + hex_color = "".join([char * 2 for char in hex_color]) + + # Parse the hex values to integers + red = int(hex_color[0:2], 16) + green = int(hex_color[2:4], 16) + blue = int(hex_color[4:6], 16) + + return f"rgb({red}, {green}, {blue})" + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/hexadecimal_to_decimal.py b/conversions/hexadecimal_to_decimal.py new file mode 100644 index 000000000000..209e4aebb368 --- /dev/null +++ b/conversions/hexadecimal_to_decimal.py @@ -0,0 +1,49 @@ +hex_table = {hex(i)[2:]: i for i in range(16)} # Use [:2] to strip off the leading '0x' + + +def hex_to_decimal(hex_string: str) -> int: + """ + Convert a hexadecimal value to its decimal equivalent + #https://www.programiz.com/python-programming/methods/built-in/hex + + >>> hex_to_decimal("a") + 10 + >>> hex_to_decimal("12f") + 303 + >>> hex_to_decimal(" 12f ") + 303 + >>> hex_to_decimal("FfFf") + 65535 + >>> hex_to_decimal("-Ff") + -255 + >>> hex_to_decimal("F-f") + Traceback (most recent call last): + ... + ValueError: Non-hexadecimal value was passed to the function + >>> hex_to_decimal("") + Traceback (most recent call last): + ... + ValueError: Empty string was passed to the function + >>> hex_to_decimal("12m") + Traceback (most recent call last): + ... + ValueError: Non-hexadecimal value was passed to the function + """ + hex_string = hex_string.strip().lower() + if not hex_string: + raise ValueError("Empty string was passed to the function") + is_negative = hex_string[0] == "-" + if is_negative: + hex_string = hex_string[1:] + if not all(char in hex_table for char in hex_string): + raise ValueError("Non-hexadecimal value was passed to the function") + decimal_number = 0 + for char in hex_string: + decimal_number = 16 * decimal_number + hex_table[char] + return -decimal_number if is_negative else decimal_number + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/conversions/int_to_negative_binary_base.py b/conversions/int_to_negative_binary_base.py new file mode 100644 index 000000000000..3f8a0042fff3 --- /dev/null +++ b/conversions/int_to_negative_binary_base.py @@ -0,0 +1,30 @@ +def decimal_to_negative_binary(number: int) -> int: + """ + a conversion algorithm from decimal + to negative binary base + + https://en.wikipedia.org/wiki/Negative_base#:~:text=Binary-,Negabinary,-Ternary + + >>> decimal_to_negative_binary(10) + 11110 + >>> decimal_to_negative_binary(-10) + 1010 + >>> decimal_to_negative_binary(-30) + 100110 + >>> decimal_to_negative_binary(30) + 1100010 + """ + + if number == 0: + return 0 + result_str = "" + while number != 0: + number, remainder = divmod(number, -2) + if remainder < 0: + number, remainder = number + 1, remainder + 2 + result_str = str(remainder) + result_str + return int(result_str) + + +if __name__ == "__main__": + __import__("doctest").testmod() diff --git a/conversions/ipv4_conversion.py b/conversions/ipv4_conversion.py new file mode 100644 index 000000000000..862309b7251e --- /dev/null +++ b/conversions/ipv4_conversion.py @@ -0,0 +1,85 @@ +# https://www.geeksforgeeks.org/convert-ip-address-to-integer-and-vice-versa/ + + +def ipv4_to_decimal(ipv4_address: str) -> int: + """ + Convert an IPv4 address to its decimal representation. + + Args: + ip_address: A string representing an IPv4 address (e.g., "192.168.0.1"). + + Returns: + int: The decimal representation of the IP address. + + >>> ipv4_to_decimal("192.168.0.1") + 3232235521 + >>> ipv4_to_decimal("10.0.0.255") + 167772415 + >>> ipv4_to_decimal("10.0.255") + Traceback (most recent call last): + ... + ValueError: Invalid IPv4 address format + >>> ipv4_to_decimal("10.0.0.256") + Traceback (most recent call last): + ... + ValueError: Invalid IPv4 octet 256 + """ + + octets = [int(octet) for octet in ipv4_address.split(".")] + if len(octets) != 4: + raise ValueError("Invalid IPv4 address format") + + decimal_ipv4 = 0 + for octet in octets: + if not 0 <= octet <= 255: + raise ValueError(f"Invalid IPv4 octet {octet}") # noqa: EM102 + decimal_ipv4 = (decimal_ipv4 << 8) + int(octet) + + return decimal_ipv4 + + +def alt_ipv4_to_decimal(ipv4_address: str) -> int: + """ + >>> alt_ipv4_to_decimal("192.168.0.1") + 3232235521 + >>> alt_ipv4_to_decimal("10.0.0.255") + 167772415 + """ + return int("0x" + "".join(f"{int(i):02x}" for i in ipv4_address.split(".")), 16) + + +def decimal_to_ipv4(decimal_ipv4: int) -> str: + """ + Convert a decimal representation of an IP address to its IPv4 format. + + Args: + decimal_ipv4: An integer representing the decimal IP address. + + Returns: + The IPv4 representation of the decimal IP address. + + >>> decimal_to_ipv4(3232235521) + '192.168.0.1' + >>> decimal_to_ipv4(167772415) + '10.0.0.255' + >>> decimal_to_ipv4(-1) + Traceback (most recent call last): + ... + ValueError: Invalid decimal IPv4 address + """ + + if not (0 <= decimal_ipv4 <= 4294967295): + raise ValueError("Invalid decimal IPv4 address") + + ip_parts = [] + for _ in range(4): + ip_parts.append(str(decimal_ipv4 & 255)) + decimal_ipv4 >>= 8 + + return ".".join(reversed(ip_parts)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/length_conversion.py b/conversions/length_conversion.py new file mode 100644 index 000000000000..07fa93a198c7 --- /dev/null +++ b/conversions/length_conversion.py @@ -0,0 +1,132 @@ +""" +Conversion of length units. +Available Units:- Metre,Kilometre,Feet,Inch,Centimeter,Yard,Foot,Mile,Millimeter + +USAGE : +-> Import this file into their respective project. +-> Use the function length_conversion() for conversion of length units. +-> Parameters : + -> value : The number of from units you want to convert + -> from_type : From which type you want to convert + -> to_type : To which type you want to convert + +REFERENCES : +-> Wikipedia reference: https://en.wikipedia.org/wiki/Meter +-> Wikipedia reference: https://en.wikipedia.org/wiki/Kilometer +-> Wikipedia reference: https://en.wikipedia.org/wiki/Feet +-> Wikipedia reference: https://en.wikipedia.org/wiki/Inch +-> Wikipedia reference: https://en.wikipedia.org/wiki/Centimeter +-> Wikipedia reference: https://en.wikipedia.org/wiki/Yard +-> Wikipedia reference: https://en.wikipedia.org/wiki/Foot +-> Wikipedia reference: https://en.wikipedia.org/wiki/Mile +-> Wikipedia reference: https://en.wikipedia.org/wiki/Millimeter +""" + +from typing import NamedTuple + + +class FromTo(NamedTuple): + from_factor: float + to_factor: float + + +TYPE_CONVERSION = { + "millimeter": "mm", + "centimeter": "cm", + "meter": "m", + "kilometer": "km", + "inch": "in", + "inche": "in", # Trailing 's' has been stripped off + "feet": "ft", + "foot": "ft", + "yard": "yd", + "mile": "mi", +} + +METRIC_CONVERSION = { + "mm": FromTo(0.001, 1000), + "cm": FromTo(0.01, 100), + "m": FromTo(1, 1), + "km": FromTo(1000, 0.001), + "in": FromTo(0.0254, 39.3701), + "ft": FromTo(0.3048, 3.28084), + "yd": FromTo(0.9144, 1.09361), + "mi": FromTo(1609.34, 0.000621371), +} + + +def length_conversion(value: float, from_type: str, to_type: str) -> float: + """ + Conversion between length units. + + >>> length_conversion(4, "METER", "FEET") + 13.12336 + >>> length_conversion(4, "M", "FT") + 13.12336 + >>> length_conversion(1, "meter", "kilometer") + 0.001 + >>> length_conversion(1, "kilometer", "inch") + 39370.1 + >>> length_conversion(3, "kilometer", "mile") + 1.8641130000000001 + >>> length_conversion(2, "feet", "meter") + 0.6096 + >>> length_conversion(4, "feet", "yard") + 1.333329312 + >>> length_conversion(1, "inch", "meter") + 0.0254 + >>> length_conversion(2, "inch", "mile") + 3.15656468e-05 + >>> length_conversion(2, "centimeter", "millimeter") + 20.0 + >>> length_conversion(2, "centimeter", "yard") + 0.0218722 + >>> length_conversion(4, "yard", "meter") + 3.6576 + >>> length_conversion(4, "yard", "kilometer") + 0.0036576 + >>> length_conversion(3, "foot", "meter") + 0.9144000000000001 + >>> length_conversion(3, "foot", "inch") + 36.00001944 + >>> length_conversion(4, "mile", "kilometer") + 6.43736 + >>> length_conversion(2, "miles", "InChEs") + 126719.753468 + >>> length_conversion(3, "millimeter", "centimeter") + 0.3 + >>> length_conversion(3, "mm", "in") + 0.1181103 + >>> length_conversion(4, "wrongUnit", "inch") + Traceback (most recent call last): + ... + ValueError: Invalid 'from_type' value: 'wrongUnit'. + Conversion abbreviations are: mm, cm, m, km, in, ft, yd, mi + """ + new_from = from_type.lower().rstrip("s") + new_from = TYPE_CONVERSION.get(new_from, new_from) + new_to = to_type.lower().rstrip("s") + new_to = TYPE_CONVERSION.get(new_to, new_to) + if new_from not in METRIC_CONVERSION: + msg = ( + f"Invalid 'from_type' value: {from_type!r}.\n" + f"Conversion abbreviations are: {', '.join(METRIC_CONVERSION)}" + ) + raise ValueError(msg) + if new_to not in METRIC_CONVERSION: + msg = ( + f"Invalid 'to_type' value: {to_type!r}.\n" + f"Conversion abbreviations are: {', '.join(METRIC_CONVERSION)}" + ) + raise ValueError(msg) + return ( + value + * METRIC_CONVERSION[new_from].from_factor + * METRIC_CONVERSION[new_to].to_factor + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/molecular_chemistry.py b/conversions/molecular_chemistry.py new file mode 100644 index 000000000000..69e3ae5b728f --- /dev/null +++ b/conversions/molecular_chemistry.py @@ -0,0 +1,131 @@ +""" +Functions useful for doing molecular chemistry: +* molarity_to_normality +* moles_to_pressure +* moles_to_volume +* pressure_and_volume_to_temperature +* mass_to_moles +* moles_to_molecules +""" + + +def molarity_to_normality(nfactor: int, moles: float, volume: float) -> float: + """ + Convert molarity to normality. + Volume is taken in litres. + + Wikipedia reference: https://en.wikipedia.org/wiki/Equivalent_concentration + Wikipedia reference: https://en.wikipedia.org/wiki/Molar_concentration + + >>> molarity_to_normality(2, 3.1, 0.31) + 20 + >>> molarity_to_normality(4, 11.4, 5.7) + 8 + """ + return round(float(moles / volume) * nfactor) + + +def moles_to_pressure(volume: float, moles: float, temperature: float) -> float: + """ + Convert moles to pressure. + Ideal gas laws are used. + Temperature is taken in kelvin. + Volume is taken in litres. + Pressure has atm as SI unit. + + Wikipedia reference: https://en.wikipedia.org/wiki/Gas_laws + Wikipedia reference: https://en.wikipedia.org/wiki/Pressure + Wikipedia reference: https://en.wikipedia.org/wiki/Temperature + + >>> moles_to_pressure(0.82, 3, 300) + 90 + >>> moles_to_pressure(8.2, 5, 200) + 10 + """ + return round(float((moles * 0.0821 * temperature) / (volume))) + + +def moles_to_volume(pressure: float, moles: float, temperature: float) -> float: + """ + Convert moles to volume. + Ideal gas laws are used. + Temperature is taken in kelvin. + Volume is taken in litres. + Pressure has atm as SI unit. + + Wikipedia reference: https://en.wikipedia.org/wiki/Gas_laws + Wikipedia reference: https://en.wikipedia.org/wiki/Pressure + Wikipedia reference: https://en.wikipedia.org/wiki/Temperature + + >>> moles_to_volume(0.82, 3, 300) + 90 + >>> moles_to_volume(8.2, 5, 200) + 10 + """ + return round(float((moles * 0.0821 * temperature) / (pressure))) + + +def pressure_and_volume_to_temperature( + pressure: float, moles: float, volume: float +) -> float: + """ + Convert pressure and volume to temperature. + Ideal gas laws are used. + Temperature is taken in kelvin. + Volume is taken in litres. + Pressure has atm as SI unit. + + Wikipedia reference: https://en.wikipedia.org/wiki/Gas_laws + Wikipedia reference: https://en.wikipedia.org/wiki/Pressure + Wikipedia reference: https://en.wikipedia.org/wiki/Temperature + + >>> pressure_and_volume_to_temperature(0.82, 1, 2) + 20 + >>> pressure_and_volume_to_temperature(8.2, 5, 3) + 60 + """ + return round(float((pressure * volume) / (0.0821 * moles))) + + +def mass_to_moles(mass: float, molar_mass: float) -> float: + """ + Convert mass of a substance to moles. + Mass is taken in grams. + Molar mass is taken in grams per mole (g/mol). + + Wikipedia reference: https://en.wikipedia.org/wiki/Mole_(unit) + + >>> mass_to_moles(36.03, 18.015) + 2.0 + >>> mass_to_moles(11.0, 44.01) + 0.25 + """ + if molar_mass <= 0: + raise ValueError("Molar mass must be greater than zero.") + if mass < 0: + raise ValueError("Mass cannot be negative.") + + return round(float(mass / molar_mass), 2) + + +def moles_to_molecules(moles: float) -> float: + """ + Convert moles of a substance to total molecules using Avogadro's constant. + + Wikipedia reference: https://en.wikipedia.org/wiki/Avogadro_constant + + >>> moles_to_molecules(2) + 1.2044e+24 + >>> moles_to_molecules(0.5) + 3.011e+23 + """ + if moles < 0: + raise ValueError("Moles cannot be negative.") + + return float(f"{moles * 6.022e23:.4e}") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/negative_binary_base_to_int.py b/conversions/negative_binary_base_to_int.py new file mode 100644 index 000000000000..082f63e9a358 --- /dev/null +++ b/conversions/negative_binary_base_to_int.py @@ -0,0 +1,24 @@ +def negative_binary_to_int(negative_binary: int) -> int: + """ + a conversion algorithm from negative binary + base to decimal + + https://en.wikipedia.org/wiki/Negative_base#:~:text=Binary-,Negabinary,-Ternary + + >>> negative_binary_to_int(101110) + -38 + >>> negative_binary_to_int(10111110) + -150 + >>> negative_binary_to_int(100) + 4 + >>> negative_binary_to_int(110) + 2 + """ + negative_binary_str = str(negative_binary) + r = negative_binary_str[::-1] + res = [int(r[c]) * (-2) ** c for c in range(len(r))] + return sum(res) + + +if __name__ == "__main__": + __import__("doctest").testmod() diff --git a/conversions/octal_to_binary.py b/conversions/octal_to_binary.py new file mode 100644 index 000000000000..84e1e85f33ca --- /dev/null +++ b/conversions/octal_to_binary.py @@ -0,0 +1,54 @@ +""" +* Author: Bama Charan Chhandogi (https://github.com/BamaCharanChhandogi) +* Description: Convert a Octal number to Binary. + +References for better understanding: +https://en.wikipedia.org/wiki/Binary_number +https://en.wikipedia.org/wiki/Octal +""" + + +def octal_to_binary(octal_number: str) -> str: + """ + Convert an Octal number to Binary. + + >>> octal_to_binary("17") + '001111' + >>> octal_to_binary("7") + '111' + >>> octal_to_binary("Av") + Traceback (most recent call last): + ... + ValueError: Non-octal value was passed to the function + >>> octal_to_binary("@#") + Traceback (most recent call last): + ... + ValueError: Non-octal value was passed to the function + >>> octal_to_binary("") + Traceback (most recent call last): + ... + ValueError: Empty string was passed to the function + """ + if not octal_number: + raise ValueError("Empty string was passed to the function") + + binary_number = "" + octal_digits = "01234567" + for digit in octal_number: + if digit not in octal_digits: + raise ValueError("Non-octal value was passed to the function") + + binary_digit = "" + value = int(digit) + for _ in range(3): + binary_digit = str(value % 2) + binary_digit + value //= 2 + binary_number += binary_digit + + return binary_number + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/octal_to_decimal.py b/conversions/octal_to_decimal.py new file mode 100644 index 000000000000..7f006f20e0c8 --- /dev/null +++ b/conversions/octal_to_decimal.py @@ -0,0 +1,79 @@ +def oct_to_decimal(oct_string: str) -> int: + """ + Convert a octal value to its decimal equivalent + + >>> oct_to_decimal("") + Traceback (most recent call last): + ... + ValueError: Empty string was passed to the function + >>> oct_to_decimal("-") + Traceback (most recent call last): + ... + ValueError: Non-octal value was passed to the function + >>> oct_to_decimal("e") + Traceback (most recent call last): + ... + ValueError: Non-octal value was passed to the function + >>> oct_to_decimal("8") + Traceback (most recent call last): + ... + ValueError: Non-octal value was passed to the function + >>> oct_to_decimal("-e") + Traceback (most recent call last): + ... + ValueError: Non-octal value was passed to the function + >>> oct_to_decimal("-8") + Traceback (most recent call last): + ... + ValueError: Non-octal value was passed to the function + >>> oct_to_decimal("1") + 1 + >>> oct_to_decimal("-1") + -1 + >>> oct_to_decimal("12") + 10 + >>> oct_to_decimal(" 12 ") + 10 + >>> oct_to_decimal("-45") + -37 + >>> oct_to_decimal("-") + Traceback (most recent call last): + ... + ValueError: Non-octal value was passed to the function + >>> oct_to_decimal("0") + 0 + >>> oct_to_decimal("-4055") + -2093 + >>> oct_to_decimal("2-0Fm") + Traceback (most recent call last): + ... + ValueError: Non-octal value was passed to the function + >>> oct_to_decimal("") + Traceback (most recent call last): + ... + ValueError: Empty string was passed to the function + >>> oct_to_decimal("19") + Traceback (most recent call last): + ... + ValueError: Non-octal value was passed to the function + """ + oct_string = str(oct_string).strip() + if not oct_string: + raise ValueError("Empty string was passed to the function") + is_negative = oct_string[0] == "-" + if is_negative: + oct_string = oct_string[1:] + if not oct_string.isdigit() or not all(0 <= int(char) <= 7 for char in oct_string): + raise ValueError("Non-octal value was passed to the function") + decimal_number = 0 + for char in oct_string: + decimal_number = 8 * decimal_number + int(char) + if is_negative: + decimal_number = -decimal_number + return decimal_number + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/conversions/octal_to_hexadecimal.py b/conversions/octal_to_hexadecimal.py new file mode 100644 index 000000000000..0615d79b5c53 --- /dev/null +++ b/conversions/octal_to_hexadecimal.py @@ -0,0 +1,65 @@ +def octal_to_hex(octal: str) -> str: + """ + Convert an Octal number to Hexadecimal number. + For more information: https://en.wikipedia.org/wiki/Octal + + >>> octal_to_hex("100") + '0x40' + >>> octal_to_hex("235") + '0x9D' + >>> octal_to_hex(17) + Traceback (most recent call last): + ... + TypeError: Expected a string as input + >>> octal_to_hex("Av") + Traceback (most recent call last): + ... + ValueError: Not a Valid Octal Number + >>> octal_to_hex("") + Traceback (most recent call last): + ... + ValueError: Empty string was passed to the function + """ + + if not isinstance(octal, str): + raise TypeError("Expected a string as input") + if octal.startswith("0o"): + octal = octal[2:] + if octal == "": + raise ValueError("Empty string was passed to the function") + if any(char not in "01234567" for char in octal): + raise ValueError("Not a Valid Octal Number") + + decimal = 0 + for char in octal: + decimal <<= 3 + decimal |= int(char) + + hex_char = "0123456789ABCDEF" + + revhex = "" + while decimal: + revhex += hex_char[decimal & 15] + decimal >>= 4 + + return "0x" + revhex[::-1] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + nums = ["030", "100", "247", "235", "007"] + + ## Main Tests + + for num in nums: + hexadecimal = octal_to_hex(num) + expected = "0x" + hex(int(num, 8))[2:].upper() + + assert hexadecimal == expected + + print(f"Hex of '0o{num}' is: {hexadecimal}") + print(f"Expected was: {expected}") + print("---") diff --git a/conversions/prefix_conversions.py b/conversions/prefix_conversions.py new file mode 100644 index 000000000000..714677f3b242 --- /dev/null +++ b/conversions/prefix_conversions.py @@ -0,0 +1,104 @@ +""" +Convert International System of Units (SI) and Binary prefixes +""" + +from __future__ import annotations + +from enum import Enum + + +class SIUnit(Enum): + yotta = 24 + zetta = 21 + exa = 18 + peta = 15 + tera = 12 + giga = 9 + mega = 6 + kilo = 3 + hecto = 2 + deca = 1 + deci = -1 + centi = -2 + milli = -3 + micro = -6 + nano = -9 + pico = -12 + femto = -15 + atto = -18 + zepto = -21 + yocto = -24 + + +class BinaryUnit(Enum): + yotta = 8 + zetta = 7 + exa = 6 + peta = 5 + tera = 4 + giga = 3 + mega = 2 + kilo = 1 + + +def convert_si_prefix( + known_amount: float, + known_prefix: str | SIUnit, + unknown_prefix: str | SIUnit, +) -> float: + """ + Wikipedia reference: https://en.wikipedia.org/wiki/Binary_prefix + Wikipedia reference: https://en.wikipedia.org/wiki/International_System_of_Units + >>> convert_si_prefix(1, SIUnit.giga, SIUnit.mega) + 1000 + >>> convert_si_prefix(1, SIUnit.mega, SIUnit.giga) + 0.001 + >>> convert_si_prefix(1, SIUnit.kilo, SIUnit.kilo) + 1 + >>> convert_si_prefix(1, 'giga', 'mega') + 1000 + >>> convert_si_prefix(1, 'gIGa', 'mEGa') + 1000 + """ + if isinstance(known_prefix, str): + known_prefix = SIUnit[known_prefix.lower()] + if isinstance(unknown_prefix, str): + unknown_prefix = SIUnit[unknown_prefix.lower()] + unknown_amount: float = known_amount * ( + 10 ** (known_prefix.value - unknown_prefix.value) + ) + return unknown_amount + + +def convert_binary_prefix( + known_amount: float, + known_prefix: str | BinaryUnit, + unknown_prefix: str | BinaryUnit, +) -> float: + """ + Wikipedia reference: https://en.wikipedia.org/wiki/Metric_prefix + >>> convert_binary_prefix(1, BinaryUnit.giga, BinaryUnit.mega) + 1024 + >>> convert_binary_prefix(1, BinaryUnit.mega, BinaryUnit.giga) + 0.0009765625 + >>> convert_binary_prefix(1, BinaryUnit.kilo, BinaryUnit.kilo) + 1 + >>> convert_binary_prefix(1, 'giga', 'mega') + 1024 + >>> convert_binary_prefix(1, 'gIGa', 'mEGa') + 1024 + """ + if isinstance(known_prefix, str): + known_prefix = BinaryUnit[known_prefix.lower()] + if isinstance(unknown_prefix, str): + unknown_prefix = BinaryUnit[unknown_prefix.lower()] + unknown_amount: float = known_amount * ( + 2 ** ((known_prefix.value - unknown_prefix.value) * 10) + ) + return unknown_amount + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/prefix_conversions_string.py b/conversions/prefix_conversions_string.py new file mode 100644 index 000000000000..c5fef49874ca --- /dev/null +++ b/conversions/prefix_conversions_string.py @@ -0,0 +1,121 @@ +""" +* Author: Manuel Di Lullo (https://github.com/manueldilullo) +* Description: Convert a number to use the correct SI or Binary unit prefix. + +Inspired by prefix_conversion.py file in this repository by lance-pyles + +URL: https://en.wikipedia.org/wiki/Metric_prefix#List_of_SI_prefixes +URL: https://en.wikipedia.org/wiki/Binary_prefix +""" + +from __future__ import annotations + +from enum import Enum, unique +from typing import TypeVar + +# Create a generic variable that can be 'Enum', or any subclass. +T = TypeVar("T", bound="Enum") + + +@unique +class BinaryUnit(Enum): + yotta = 80 + zetta = 70 + exa = 60 + peta = 50 + tera = 40 + giga = 30 + mega = 20 + kilo = 10 + + +@unique +class SIUnit(Enum): + yotta = 24 + zetta = 21 + exa = 18 + peta = 15 + tera = 12 + giga = 9 + mega = 6 + kilo = 3 + hecto = 2 + deca = 1 + deci = -1 + centi = -2 + milli = -3 + micro = -6 + nano = -9 + pico = -12 + femto = -15 + atto = -18 + zepto = -21 + yocto = -24 + + @classmethod + def get_positive(cls) -> dict: + """ + Returns a dictionary with only the elements of this enum + that has a positive value + >>> from itertools import islice + >>> positive = SIUnit.get_positive() + >>> inc = iter(positive.items()) + >>> dict(islice(inc, len(positive) // 2)) + {'yotta': 24, 'zetta': 21, 'exa': 18, 'peta': 15, 'tera': 12} + >>> dict(inc) + {'giga': 9, 'mega': 6, 'kilo': 3, 'hecto': 2, 'deca': 1} + """ + return {unit.name: unit.value for unit in cls if unit.value > 0} + + @classmethod + def get_negative(cls) -> dict: + """ + Returns a dictionary with only the elements of this enum + that has a negative value + @example + >>> from itertools import islice + >>> negative = SIUnit.get_negative() + >>> inc = iter(negative.items()) + >>> dict(islice(inc, len(negative) // 2)) + {'deci': -1, 'centi': -2, 'milli': -3, 'micro': -6, 'nano': -9} + >>> dict(inc) + {'pico': -12, 'femto': -15, 'atto': -18, 'zepto': -21, 'yocto': -24} + """ + return {unit.name: unit.value for unit in cls if unit.value < 0} + + +def add_si_prefix(value: float) -> str: + """ + Function that converts a number to his version with SI prefix + @input value (an integer) + @example: + >>> add_si_prefix(10000) + '10.0 kilo' + """ + prefixes = SIUnit.get_positive() if value > 0 else SIUnit.get_negative() + for name_prefix, value_prefix in prefixes.items(): + numerical_part = value / (10**value_prefix) + if numerical_part > 1: + return f"{numerical_part!s} {name_prefix}" + return str(value) + + +def add_binary_prefix(value: float) -> str: + """ + Function that converts a number to his version with Binary prefix + @input value (an integer) + @example: + >>> add_binary_prefix(65536) + '64.0 kilo' + """ + for prefix in BinaryUnit: + numerical_part = value / (2**prefix.value) + if numerical_part > 1: + return f"{numerical_part!s} {prefix.name}" + return str(value) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/pressure_conversions.py b/conversions/pressure_conversions.py new file mode 100644 index 000000000000..fe78b1382677 --- /dev/null +++ b/conversions/pressure_conversions.py @@ -0,0 +1,87 @@ +""" +Conversion of pressure units. +Available Units:- Pascal,Bar,Kilopascal,Megapascal,psi(pound per square inch), +inHg(in mercury column),torr,atm +USAGE : +-> Import this file into their respective project. +-> Use the function pressure_conversion() for conversion of pressure units. +-> Parameters : + -> value : The number of from units you want to convert + -> from_type : From which type you want to convert + -> to_type : To which type you want to convert +REFERENCES : +-> Wikipedia reference: https://en.wikipedia.org/wiki/Pascal_(unit) +-> Wikipedia reference: https://en.wikipedia.org/wiki/Pound_per_square_inch +-> Wikipedia reference: https://en.wikipedia.org/wiki/Inch_of_mercury +-> Wikipedia reference: https://en.wikipedia.org/wiki/Torr +-> https://en.wikipedia.org/wiki/Standard_atmosphere_(unit) +-> https://msestudent.com/what-are-the-units-of-pressure/ +-> https://www.unitconverters.net/pressure-converter.html +""" + +from typing import NamedTuple + + +class FromTo(NamedTuple): + from_factor: float + to_factor: float + + +PRESSURE_CONVERSION = { + "atm": FromTo(1, 1), + "pascal": FromTo(0.0000098, 101325), + "bar": FromTo(0.986923, 1.01325), + "kilopascal": FromTo(0.00986923, 101.325), + "megapascal": FromTo(9.86923, 0.101325), + "psi": FromTo(0.068046, 14.6959), + "inHg": FromTo(0.0334211, 29.9213), + "torr": FromTo(0.00131579, 760), +} + + +def pressure_conversion(value: float, from_type: str, to_type: str) -> float: + """ + Conversion between pressure units. + >>> pressure_conversion(4, "atm", "pascal") + 405300 + >>> pressure_conversion(1, "pascal", "psi") + 0.00014401981999999998 + >>> pressure_conversion(1, "bar", "atm") + 0.986923 + >>> pressure_conversion(3, "kilopascal", "bar") + 0.029999991892499998 + >>> pressure_conversion(2, "megapascal", "psi") + 290.074434314 + >>> pressure_conversion(4, "psi", "torr") + 206.85984 + >>> pressure_conversion(1, "inHg", "atm") + 0.0334211 + >>> pressure_conversion(1, "torr", "psi") + 0.019336718261000002 + >>> pressure_conversion(4, "wrongUnit", "atm") + Traceback (most recent call last): + ... + ValueError: Invalid 'from_type' value: 'wrongUnit' Supported values are: + atm, pascal, bar, kilopascal, megapascal, psi, inHg, torr + """ + if from_type not in PRESSURE_CONVERSION: + raise ValueError( + f"Invalid 'from_type' value: {from_type!r} Supported values are:\n" + + ", ".join(PRESSURE_CONVERSION) + ) + if to_type not in PRESSURE_CONVERSION: + raise ValueError( + f"Invalid 'to_type' value: {to_type!r}. Supported values are:\n" + + ", ".join(PRESSURE_CONVERSION) + ) + return ( + value + * PRESSURE_CONVERSION[from_type].from_factor + * PRESSURE_CONVERSION[to_type].to_factor + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/rectangular_to_polar.py b/conversions/rectangular_to_polar.py new file mode 100644 index 000000000000..bed97d7410ec --- /dev/null +++ b/conversions/rectangular_to_polar.py @@ -0,0 +1,32 @@ +import math + + +def rectangular_to_polar(real: float, img: float) -> tuple[float, float]: + """ + https://en.wikipedia.org/wiki/Polar_coordinate_system + + >>> rectangular_to_polar(5,-5) + (7.07, -45.0) + >>> rectangular_to_polar(-1,1) + (1.41, 135.0) + >>> rectangular_to_polar(-1,-1) + (1.41, -135.0) + >>> rectangular_to_polar(1e-10,1e-10) + (0.0, 45.0) + >>> rectangular_to_polar(-1e-10,1e-10) + (0.0, 135.0) + >>> rectangular_to_polar(9.75,5.93) + (11.41, 31.31) + >>> rectangular_to_polar(10000,99999) + (100497.76, 84.29) + """ + + mod = round(math.sqrt((real**2) + (img**2)), 2) + ang = round(math.degrees(math.atan2(img, real)), 2) + return (mod, ang) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/rgb_cmyk_conversion.py b/conversions/rgb_cmyk_conversion.py new file mode 100644 index 000000000000..07d65b704c44 --- /dev/null +++ b/conversions/rgb_cmyk_conversion.py @@ -0,0 +1,71 @@ +def rgb_to_cmyk(r_input: int, g_input: int, b_input: int) -> tuple[int, int, int, int]: + """ + Simple RGB to CMYK conversion. Returns percentages of CMYK paint. + https://www.programmingalgorithms.com/algorithm/rgb-to-cmyk/ + + Note: this is a very popular algorithm that converts colors linearly and gives + only approximate results. Actual preparation for printing requires advanced color + conversion considering the color profiles and parameters of the target device. + + >>> rgb_to_cmyk(255, 200, "a") + Traceback (most recent call last): + ... + ValueError: Expected int, found (, , ) + + >>> rgb_to_cmyk(255, 255, 999) + Traceback (most recent call last): + ... + ValueError: Expected int of the range 0..255 + + >>> rgb_to_cmyk(255, 255, 255) # white + (0, 0, 0, 0) + + >>> rgb_to_cmyk(128, 128, 128) # gray + (0, 0, 0, 50) + + >>> rgb_to_cmyk(0, 0, 0) # black + (0, 0, 0, 100) + + >>> rgb_to_cmyk(255, 0, 0) # red + (0, 100, 100, 0) + + >>> rgb_to_cmyk(0, 255, 0) # green + (100, 0, 100, 0) + + >>> rgb_to_cmyk(0, 0, 255) # blue + (100, 100, 0, 0) + """ + + if ( + not isinstance(r_input, int) + or not isinstance(g_input, int) + or not isinstance(b_input, int) + ): + msg = f"Expected int, found {type(r_input), type(g_input), type(b_input)}" + raise ValueError(msg) + + if not 0 <= r_input < 256 or not 0 <= g_input < 256 or not 0 <= b_input < 256: + raise ValueError("Expected int of the range 0..255") + + # changing range from 0..255 to 0..1 + r = r_input / 255 + g = g_input / 255 + b = b_input / 255 + + k = 1 - max(r, g, b) + + if k == 1: # pure black + return 0, 0, 0, 100 + + c = round(100 * (1 - r - k) / (1 - k)) + m = round(100 * (1 - g - k) / (1 - k)) + y = round(100 * (1 - b - k) / (1 - k)) + k = round(100 * k) + + return c, m, y, k + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/conversions/rgb_hsv_conversion.py b/conversions/rgb_hsv_conversion.py new file mode 100644 index 000000000000..74b3d33e49e7 --- /dev/null +++ b/conversions/rgb_hsv_conversion.py @@ -0,0 +1,159 @@ +""" +The RGB color model is an additive color model in which red, green, and blue light +are added together in various ways to reproduce a broad array of colors. The name +of the model comes from the initials of the three additive primary colors, red, +green, and blue. Meanwhile, the HSV representation models how colors appear under +light. In it, colors are represented using three components: hue, saturation and +(brightness-)value. This file provides functions for converting colors from one +representation to the other. + +(description adapted from https://en.wikipedia.org/wiki/RGB_color_model and +https://en.wikipedia.org/wiki/HSL_and_HSV). +""" + + +def hsv_to_rgb(hue: float, saturation: float, value: float) -> list[int]: + """ + Conversion from the HSV-representation to the RGB-representation. + Expected RGB-values taken from + https://www.rapidtables.com/convert/color/hsv-to-rgb.html + + >>> hsv_to_rgb(0, 0, 0) + [0, 0, 0] + >>> hsv_to_rgb(0, 0, 1) + [255, 255, 255] + >>> hsv_to_rgb(0, 1, 1) + [255, 0, 0] + >>> hsv_to_rgb(60, 1, 1) + [255, 255, 0] + >>> hsv_to_rgb(120, 1, 1) + [0, 255, 0] + >>> hsv_to_rgb(240, 1, 1) + [0, 0, 255] + >>> hsv_to_rgb(300, 1, 1) + [255, 0, 255] + >>> hsv_to_rgb(180, 0.5, 0.5) + [64, 128, 128] + >>> hsv_to_rgb(234, 0.14, 0.88) + [193, 196, 224] + >>> hsv_to_rgb(330, 0.75, 0.5) + [128, 32, 80] + """ + if hue < 0 or hue > 360: + raise Exception("hue should be between 0 and 360") + + if saturation < 0 or saturation > 1: + raise Exception("saturation should be between 0 and 1") + + if value < 0 or value > 1: + raise Exception("value should be between 0 and 1") + + chroma = value * saturation + hue_section = hue / 60 + second_largest_component = chroma * (1 - abs(hue_section % 2 - 1)) + match_value = value - chroma + + if hue_section >= 0 and hue_section <= 1: + red = round(255 * (chroma + match_value)) + green = round(255 * (second_largest_component + match_value)) + blue = round(255 * (match_value)) + elif hue_section > 1 and hue_section <= 2: + red = round(255 * (second_largest_component + match_value)) + green = round(255 * (chroma + match_value)) + blue = round(255 * (match_value)) + elif hue_section > 2 and hue_section <= 3: + red = round(255 * (match_value)) + green = round(255 * (chroma + match_value)) + blue = round(255 * (second_largest_component + match_value)) + elif hue_section > 3 and hue_section <= 4: + red = round(255 * (match_value)) + green = round(255 * (second_largest_component + match_value)) + blue = round(255 * (chroma + match_value)) + elif hue_section > 4 and hue_section <= 5: + red = round(255 * (second_largest_component + match_value)) + green = round(255 * (match_value)) + blue = round(255 * (chroma + match_value)) + else: + red = round(255 * (chroma + match_value)) + green = round(255 * (match_value)) + blue = round(255 * (second_largest_component + match_value)) + + return [red, green, blue] + + +def rgb_to_hsv(red: int, green: int, blue: int) -> list[float]: + """ + Conversion from the RGB-representation to the HSV-representation. + The tested values are the reverse values from the hsv_to_rgb-doctests. + Function "approximately_equal_hsv" is needed because of small deviations due to + rounding for the RGB-values. + + >>> approximately_equal_hsv(rgb_to_hsv(0, 0, 0), [0, 0, 0]) + True + >>> approximately_equal_hsv(rgb_to_hsv(255, 255, 255), [0, 0, 1]) + True + >>> approximately_equal_hsv(rgb_to_hsv(255, 0, 0), [0, 1, 1]) + True + >>> approximately_equal_hsv(rgb_to_hsv(255, 255, 0), [60, 1, 1]) + True + >>> approximately_equal_hsv(rgb_to_hsv(0, 255, 0), [120, 1, 1]) + True + >>> approximately_equal_hsv(rgb_to_hsv(0, 0, 255), [240, 1, 1]) + True + >>> approximately_equal_hsv(rgb_to_hsv(255, 0, 255), [300, 1, 1]) + True + >>> approximately_equal_hsv(rgb_to_hsv(64, 128, 128), [180, 0.5, 0.5]) + True + >>> approximately_equal_hsv(rgb_to_hsv(193, 196, 224), [234, 0.14, 0.88]) + True + >>> approximately_equal_hsv(rgb_to_hsv(128, 32, 80), [330, 0.75, 0.5]) + True + """ + if red < 0 or red > 255: + raise Exception("red should be between 0 and 255") + + if green < 0 or green > 255: + raise Exception("green should be between 0 and 255") + + if blue < 0 or blue > 255: + raise Exception("blue should be between 0 and 255") + + float_red = red / 255 + float_green = green / 255 + float_blue = blue / 255 + value = max(float_red, float_green, float_blue) + chroma = value - min(float_red, float_green, float_blue) + saturation = 0 if value == 0 else chroma / value + + if chroma == 0: + hue = 0.0 + elif value == float_red: + hue = 60 * (0 + (float_green - float_blue) / chroma) + elif value == float_green: + hue = 60 * (2 + (float_blue - float_red) / chroma) + else: + hue = 60 * (4 + (float_red - float_green) / chroma) + + hue = (hue + 360) % 360 + + return [hue, saturation, value] + + +def approximately_equal_hsv(hsv_1: list[float], hsv_2: list[float]) -> bool: + """ + Utility-function to check that two hsv-colors are approximately equal + + >>> approximately_equal_hsv([0, 0, 0], [0, 0, 0]) + True + >>> approximately_equal_hsv([180, 0.5, 0.3], [179.9999, 0.500001, 0.30001]) + True + >>> approximately_equal_hsv([0, 0, 0], [1, 0, 0]) + False + >>> approximately_equal_hsv([180, 0.5, 0.3], [179.9999, 0.6, 0.30001]) + False + """ + check_hue = abs(hsv_1[0] - hsv_2[0]) < 0.2 + check_saturation = abs(hsv_1[1] - hsv_2[1]) < 0.002 + check_value = abs(hsv_1[2] - hsv_2[2]) < 0.002 + + return check_hue and check_saturation and check_value diff --git a/conversions/roman_numerals.py b/conversions/roman_numerals.py new file mode 100644 index 000000000000..7764ee0d8de5 --- /dev/null +++ b/conversions/roman_numerals.py @@ -0,0 +1,84 @@ +ROMAN = [ + (1000, "M"), + (900, "CM"), + (500, "D"), + (400, "CD"), + (100, "C"), + (90, "XC"), + (50, "L"), + (40, "XL"), + (10, "X"), + (9, "IX"), + (5, "V"), + (4, "IV"), + (1, "I"), +] + + +def roman_to_int(roman: str) -> int: + """ + LeetCode No. 13 Roman to Integer + Given a roman numeral, convert it to an integer. + Input is guaranteed to be within the range from 1 to 3999. + https://en.wikipedia.org/wiki/Roman_numerals + >>> tests = {"III": 3, "CLIV": 154, "MIX": 1009, "MMD": 2500, "MMMCMXCIX": 3999} + >>> all(roman_to_int(key) == value for key, value in tests.items()) + True + """ + vals = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000} + total = 0 + place = 0 + while place < len(roman): + if (place + 1 < len(roman)) and (vals[roman[place]] < vals[roman[place + 1]]): + total += vals[roman[place + 1]] - vals[roman[place]] + place += 2 + else: + total += vals[roman[place]] + place += 1 + return total + + +def int_to_roman(number: int) -> str: + """ + Given an integer, convert it to a Roman numeral. + Input must be an integer in the range 1 to 3999. + https://en.wikipedia.org/wiki/Roman_numerals + >>> tests = {"III": 3, "CLIV": 154, "MIX": 1009, "MMD": 2500, "MMMCMXCIX": 3999} + >>> all(int_to_roman(value) == key for key, value in tests.items()) + True + >>> int_to_roman(0) + Traceback (most recent call last): + ... + ValueError: int_to_roman only accepts integers in the range 1 to 3999, got 0 + >>> int_to_roman(-1) + Traceback (most recent call last): + ... + ValueError: int_to_roman only accepts integers in the range 1 to 3999, got -1 + >>> int_to_roman(4000) + Traceback (most recent call last): + ... + ValueError: int_to_roman only accepts integers in the range 1 to 3999, got 4000 + >>> int_to_roman(True) + Traceback (most recent call last): + ... + TypeError: int_to_roman only accepts integers, got bool + """ + if isinstance(number, bool) or not isinstance(number, int): + msg = f"int_to_roman only accepts integers, got {type(number).__name__}" + raise TypeError(msg) + if not 1 <= number <= 3999: + msg = f"int_to_roman only accepts integers in the range 1 to 3999, got {number}" + raise ValueError(msg) + result = [] + for arabic, roman in ROMAN: + (factor, number) = divmod(number, arabic) + result.append(roman * factor) + if number == 0: + break + return "".join(result) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/speed_conversions.py b/conversions/speed_conversions.py new file mode 100644 index 000000000000..ba497119d3f5 --- /dev/null +++ b/conversions/speed_conversions.py @@ -0,0 +1,71 @@ +""" +Convert speed units + +https://en.wikipedia.org/wiki/Kilometres_per_hour +https://en.wikipedia.org/wiki/Miles_per_hour +https://en.wikipedia.org/wiki/Knot_(unit) +https://en.wikipedia.org/wiki/Metre_per_second +""" + +speed_chart: dict[str, float] = { + "km/h": 1.0, + "m/s": 3.6, + "mph": 1.609344, + "knot": 1.852, +} + +speed_chart_inverse: dict[str, float] = { + "km/h": 1.0, + "m/s": 0.277777778, + "mph": 0.621371192, + "knot": 0.539956803, +} + + +def convert_speed(speed: float, unit_from: str, unit_to: str) -> float: + """ + Convert speed from one unit to another using the speed_chart above. + + "km/h": 1.0, + "m/s": 3.6, + "mph": 1.609344, + "knot": 1.852, + + >>> convert_speed(100, "km/h", "m/s") + 27.778 + >>> convert_speed(100, "km/h", "mph") + 62.137 + >>> convert_speed(100, "km/h", "knot") + 53.996 + >>> convert_speed(100, "m/s", "km/h") + 360.0 + >>> convert_speed(100, "m/s", "mph") + 223.694 + >>> convert_speed(100, "m/s", "knot") + 194.384 + >>> convert_speed(100, "mph", "km/h") + 160.934 + >>> convert_speed(100, "mph", "m/s") + 44.704 + >>> convert_speed(100, "mph", "knot") + 86.898 + >>> convert_speed(100, "knot", "km/h") + 185.2 + >>> convert_speed(100, "knot", "m/s") + 51.444 + >>> convert_speed(100, "knot", "mph") + 115.078 + """ + if unit_to not in speed_chart or unit_from not in speed_chart_inverse: + msg = ( + f"Incorrect 'from_type' or 'to_type' value: {unit_from!r}, {unit_to!r}\n" + f"Valid values are: {', '.join(speed_chart_inverse)}" + ) + raise ValueError(msg) + return round(speed * speed_chart[unit_from] * speed_chart_inverse[unit_to], 3) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/temperature_conversions.py b/conversions/temperature_conversions.py new file mode 100644 index 000000000000..dde1d2f0f166 --- /dev/null +++ b/conversions/temperature_conversions.py @@ -0,0 +1,385 @@ +"""Convert between different units of temperature""" + + +def celsius_to_fahrenheit(celsius: float, ndigits: int = 2) -> float: + """ + Convert a given value from Celsius to Fahrenheit and round it to 2 decimal places. + Wikipedia reference: https://en.wikipedia.org/wiki/Celsius + Wikipedia reference: https://en.wikipedia.org/wiki/Fahrenheit + + >>> celsius_to_fahrenheit(273.354, 3) + 524.037 + >>> celsius_to_fahrenheit(273.354, 0) + 524.0 + >>> celsius_to_fahrenheit(-40.0) + -40.0 + >>> celsius_to_fahrenheit(-20.0) + -4.0 + >>> celsius_to_fahrenheit(0) + 32.0 + >>> celsius_to_fahrenheit(20) + 68.0 + >>> celsius_to_fahrenheit("40") + 104.0 + >>> celsius_to_fahrenheit("celsius") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'celsius' + """ + return round((float(celsius) * 9 / 5) + 32, ndigits) + + +def celsius_to_kelvin(celsius: float, ndigits: int = 2) -> float: + """ + Convert a given value from Celsius to Kelvin and round it to 2 decimal places. + Wikipedia reference: https://en.wikipedia.org/wiki/Celsius + Wikipedia reference: https://en.wikipedia.org/wiki/Kelvin + + >>> celsius_to_kelvin(273.354, 3) + 546.504 + >>> celsius_to_kelvin(273.354, 0) + 547.0 + >>> celsius_to_kelvin(0) + 273.15 + >>> celsius_to_kelvin(20.0) + 293.15 + >>> celsius_to_kelvin("40") + 313.15 + >>> celsius_to_kelvin("celsius") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'celsius' + """ + return round(float(celsius) + 273.15, ndigits) + + +def celsius_to_rankine(celsius: float, ndigits: int = 2) -> float: + """ + Convert a given value from Celsius to Rankine and round it to 2 decimal places. + Wikipedia reference: https://en.wikipedia.org/wiki/Celsius + Wikipedia reference: https://en.wikipedia.org/wiki/Rankine_scale + + >>> celsius_to_rankine(273.354, 3) + 983.707 + >>> celsius_to_rankine(273.354, 0) + 984.0 + >>> celsius_to_rankine(0) + 491.67 + >>> celsius_to_rankine(20.0) + 527.67 + >>> celsius_to_rankine("40") + 563.67 + >>> celsius_to_rankine("celsius") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'celsius' + """ + return round((float(celsius) * 9 / 5) + 491.67, ndigits) + + +def fahrenheit_to_celsius(fahrenheit: float, ndigits: int = 2) -> float: + """ + Convert a given value from Fahrenheit to Celsius and round it to 2 decimal places. + Wikipedia reference: https://en.wikipedia.org/wiki/Fahrenheit + Wikipedia reference: https://en.wikipedia.org/wiki/Celsius + + >>> fahrenheit_to_celsius(273.354, 3) + 134.086 + >>> fahrenheit_to_celsius(273.354, 0) + 134.0 + >>> fahrenheit_to_celsius(0) + -17.78 + >>> fahrenheit_to_celsius(20.0) + -6.67 + >>> fahrenheit_to_celsius(40.0) + 4.44 + >>> fahrenheit_to_celsius(60) + 15.56 + >>> fahrenheit_to_celsius(80) + 26.67 + >>> fahrenheit_to_celsius("100") + 37.78 + >>> fahrenheit_to_celsius("fahrenheit") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'fahrenheit' + """ + return round((float(fahrenheit) - 32) * 5 / 9, ndigits) + + +def fahrenheit_to_kelvin(fahrenheit: float, ndigits: int = 2) -> float: + """ + Convert a given value from Fahrenheit to Kelvin and round it to 2 decimal places. + Wikipedia reference: https://en.wikipedia.org/wiki/Fahrenheit + Wikipedia reference: https://en.wikipedia.org/wiki/Kelvin + + >>> fahrenheit_to_kelvin(273.354, 3) + 407.236 + >>> fahrenheit_to_kelvin(273.354, 0) + 407.0 + >>> fahrenheit_to_kelvin(0) + 255.37 + >>> fahrenheit_to_kelvin(20.0) + 266.48 + >>> fahrenheit_to_kelvin(40.0) + 277.59 + >>> fahrenheit_to_kelvin(60) + 288.71 + >>> fahrenheit_to_kelvin(80) + 299.82 + >>> fahrenheit_to_kelvin("100") + 310.93 + >>> fahrenheit_to_kelvin("fahrenheit") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'fahrenheit' + """ + return round(((float(fahrenheit) - 32) * 5 / 9) + 273.15, ndigits) + + +def fahrenheit_to_rankine(fahrenheit: float, ndigits: int = 2) -> float: + """ + Convert a given value from Fahrenheit to Rankine and round it to 2 decimal places. + Wikipedia reference: https://en.wikipedia.org/wiki/Fahrenheit + Wikipedia reference: https://en.wikipedia.org/wiki/Rankine_scale + + >>> fahrenheit_to_rankine(273.354, 3) + 733.024 + >>> fahrenheit_to_rankine(273.354, 0) + 733.0 + >>> fahrenheit_to_rankine(0) + 459.67 + >>> fahrenheit_to_rankine(20.0) + 479.67 + >>> fahrenheit_to_rankine(40.0) + 499.67 + >>> fahrenheit_to_rankine(60) + 519.67 + >>> fahrenheit_to_rankine(80) + 539.67 + >>> fahrenheit_to_rankine("100") + 559.67 + >>> fahrenheit_to_rankine("fahrenheit") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'fahrenheit' + """ + return round(float(fahrenheit) + 459.67, ndigits) + + +def kelvin_to_celsius(kelvin: float, ndigits: int = 2) -> float: + """ + Convert a given value from Kelvin to Celsius and round it to 2 decimal places. + Wikipedia reference: https://en.wikipedia.org/wiki/Kelvin + Wikipedia reference: https://en.wikipedia.org/wiki/Celsius + + >>> kelvin_to_celsius(273.354, 3) + 0.204 + >>> kelvin_to_celsius(273.354, 0) + 0.0 + >>> kelvin_to_celsius(273.15) + 0.0 + >>> kelvin_to_celsius(300) + 26.85 + >>> kelvin_to_celsius("315.5") + 42.35 + >>> kelvin_to_celsius("kelvin") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'kelvin' + """ + return round(float(kelvin) - 273.15, ndigits) + + +def kelvin_to_fahrenheit(kelvin: float, ndigits: int = 2) -> float: + """ + Convert a given value from Kelvin to Fahrenheit and round it to 2 decimal places. + Wikipedia reference: https://en.wikipedia.org/wiki/Kelvin + Wikipedia reference: https://en.wikipedia.org/wiki/Fahrenheit + + >>> kelvin_to_fahrenheit(273.354, 3) + 32.367 + >>> kelvin_to_fahrenheit(273.354, 0) + 32.0 + >>> kelvin_to_fahrenheit(273.15) + 32.0 + >>> kelvin_to_fahrenheit(300) + 80.33 + >>> kelvin_to_fahrenheit("315.5") + 108.23 + >>> kelvin_to_fahrenheit("kelvin") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'kelvin' + """ + return round(((float(kelvin) - 273.15) * 9 / 5) + 32, ndigits) + + +def kelvin_to_rankine(kelvin: float, ndigits: int = 2) -> float: + """ + Convert a given value from Kelvin to Rankine and round it to 2 decimal places. + Wikipedia reference: https://en.wikipedia.org/wiki/Kelvin + Wikipedia reference: https://en.wikipedia.org/wiki/Rankine_scale + + >>> kelvin_to_rankine(273.354, 3) + 492.037 + >>> kelvin_to_rankine(273.354, 0) + 492.0 + >>> kelvin_to_rankine(0) + 0.0 + >>> kelvin_to_rankine(20.0) + 36.0 + >>> kelvin_to_rankine("40") + 72.0 + >>> kelvin_to_rankine("kelvin") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'kelvin' + """ + return round((float(kelvin) * 9 / 5), ndigits) + + +def rankine_to_celsius(rankine: float, ndigits: int = 2) -> float: + """ + Convert a given value from Rankine to Celsius and round it to 2 decimal places. + Wikipedia reference: https://en.wikipedia.org/wiki/Rankine_scale + Wikipedia reference: https://en.wikipedia.org/wiki/Celsius + + >>> rankine_to_celsius(273.354, 3) + -121.287 + >>> rankine_to_celsius(273.354, 0) + -121.0 + >>> rankine_to_celsius(273.15) + -121.4 + >>> rankine_to_celsius(300) + -106.48 + >>> rankine_to_celsius("315.5") + -97.87 + >>> rankine_to_celsius("rankine") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'rankine' + """ + return round((float(rankine) - 491.67) * 5 / 9, ndigits) + + +def rankine_to_fahrenheit(rankine: float, ndigits: int = 2) -> float: + """ + Convert a given value from Rankine to Fahrenheit and round it to 2 decimal places. + Wikipedia reference: https://en.wikipedia.org/wiki/Rankine_scale + Wikipedia reference: https://en.wikipedia.org/wiki/Fahrenheit + + >>> rankine_to_fahrenheit(273.15) + -186.52 + >>> rankine_to_fahrenheit(300) + -159.67 + >>> rankine_to_fahrenheit("315.5") + -144.17 + >>> rankine_to_fahrenheit("rankine") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'rankine' + """ + return round(float(rankine) - 459.67, ndigits) + + +def rankine_to_kelvin(rankine: float, ndigits: int = 2) -> float: + """ + Convert a given value from Rankine to Kelvin and round it to 2 decimal places. + Wikipedia reference: https://en.wikipedia.org/wiki/Rankine_scale + Wikipedia reference: https://en.wikipedia.org/wiki/Kelvin + + >>> rankine_to_kelvin(0) + 0.0 + >>> rankine_to_kelvin(20.0) + 11.11 + >>> rankine_to_kelvin("40") + 22.22 + >>> rankine_to_kelvin("rankine") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'rankine' + """ + return round((float(rankine) * 5 / 9), ndigits) + + +def reaumur_to_kelvin(reaumur: float, ndigits: int = 2) -> float: + """ + Convert a given value from reaumur to Kelvin and round it to 2 decimal places. + Reference:- http://www.csgnetwork.com/temp2conv.html + + >>> reaumur_to_kelvin(0) + 273.15 + >>> reaumur_to_kelvin(20.0) + 298.15 + >>> reaumur_to_kelvin(40) + 323.15 + >>> reaumur_to_kelvin("reaumur") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'reaumur' + """ + return round((float(reaumur) * 1.25 + 273.15), ndigits) + + +def reaumur_to_fahrenheit(reaumur: float, ndigits: int = 2) -> float: + """ + Convert a given value from reaumur to fahrenheit and round it to 2 decimal places. + Reference:- http://www.csgnetwork.com/temp2conv.html + + >>> reaumur_to_fahrenheit(0) + 32.0 + >>> reaumur_to_fahrenheit(20.0) + 77.0 + >>> reaumur_to_fahrenheit(40) + 122.0 + >>> reaumur_to_fahrenheit("reaumur") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'reaumur' + """ + return round((float(reaumur) * 2.25 + 32), ndigits) + + +def reaumur_to_celsius(reaumur: float, ndigits: int = 2) -> float: + """ + Convert a given value from reaumur to celsius and round it to 2 decimal places. + Reference:- http://www.csgnetwork.com/temp2conv.html + + >>> reaumur_to_celsius(0) + 0.0 + >>> reaumur_to_celsius(20.0) + 25.0 + >>> reaumur_to_celsius(40) + 50.0 + >>> reaumur_to_celsius("reaumur") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'reaumur' + """ + return round((float(reaumur) * 1.25), ndigits) + + +def reaumur_to_rankine(reaumur: float, ndigits: int = 2) -> float: + """ + Convert a given value from reaumur to rankine and round it to 2 decimal places. + Reference:- http://www.csgnetwork.com/temp2conv.html + + >>> reaumur_to_rankine(0) + 491.67 + >>> reaumur_to_rankine(20.0) + 536.67 + >>> reaumur_to_rankine(40) + 581.67 + >>> reaumur_to_rankine("reaumur") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'reaumur' + """ + return round((float(reaumur) * 2.25 + 32 + 459.67), ndigits) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/time_conversions.py b/conversions/time_conversions.py new file mode 100644 index 000000000000..8c30f5bc4a45 --- /dev/null +++ b/conversions/time_conversions.py @@ -0,0 +1,86 @@ +""" +A unit of time is any particular time interval, used as a standard way of measuring or +expressing duration. The base unit of time in the International System of Units (SI), +and by extension most of the Western world, is the second, defined as about 9 billion +oscillations of the caesium atom. + +https://en.wikipedia.org/wiki/Unit_of_time +""" + +time_chart: dict[str, float] = { + "seconds": 1.0, + "minutes": 60.0, # 1 minute = 60 sec + "hours": 3600.0, # 1 hour = 60 minutes = 3600 seconds + "days": 86400.0, # 1 day = 24 hours = 1440 min = 86400 sec + "weeks": 604800.0, # 1 week=7d=168hr=10080min = 604800 sec + "months": 2629800.0, # Approximate value for a month in seconds + "years": 31557600.0, # Approximate value for a year in seconds +} + +time_chart_inverse: dict[str, float] = { + key: 1 / value for key, value in time_chart.items() +} + + +def convert_time(time_value: float, unit_from: str, unit_to: str) -> float: + """ + Convert time from one unit to another using the time_chart above. + + >>> convert_time(3600, "seconds", "hours") + 1.0 + >>> convert_time(3500, "Seconds", "Hours") + 0.972 + >>> convert_time(1, "DaYs", "hours") + 24.0 + >>> convert_time(120, "minutes", "SeCoNdS") + 7200.0 + >>> convert_time(2, "WEEKS", "days") + 14.0 + >>> convert_time(0.5, "hours", "MINUTES") + 30.0 + >>> convert_time(-3600, "seconds", "hours") + Traceback (most recent call last): + ... + ValueError: 'time_value' must be a non-negative number. + >>> convert_time("Hello", "hours", "minutes") + Traceback (most recent call last): + ... + ValueError: 'time_value' must be a non-negative number. + >>> convert_time([0, 1, 2], "weeks", "days") + Traceback (most recent call last): + ... + ValueError: 'time_value' must be a non-negative number. + >>> convert_time(1, "cool", "century") # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Invalid unit cool is not in seconds, minutes, hours, days, weeks, ... + >>> convert_time(1, "seconds", "hot") # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Invalid unit hot is not in seconds, minutes, hours, days, weeks, ... + """ + if not isinstance(time_value, (int, float)) or time_value < 0: + msg = "'time_value' must be a non-negative number." + raise ValueError(msg) + + unit_from = unit_from.lower() + unit_to = unit_to.lower() + if unit_from not in time_chart or unit_to not in time_chart: + invalid_unit = unit_from if unit_from not in time_chart else unit_to + msg = f"Invalid unit {invalid_unit} is not in {', '.join(time_chart)}." + raise ValueError(msg) + + return round( + time_value * time_chart[unit_from] * time_chart_inverse[unit_to], + 3, + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + print(f"{convert_time(3600,'seconds', 'hours') = :,}") + print(f"{convert_time(360, 'days', 'months') = :,}") + print(f"{convert_time(360, 'months', 'years') = :,}") + print(f"{convert_time(1, 'years', 'seconds') = :,}") diff --git a/conversions/volume_conversions.py b/conversions/volume_conversions.py new file mode 100644 index 000000000000..cb240380534b --- /dev/null +++ b/conversions/volume_conversions.py @@ -0,0 +1,83 @@ +""" +Conversion of volume units. +Available Units:- Cubic metre,Litre,KiloLitre,Gallon,Cubic yard,Cubic foot,cup +USAGE : +-> Import this file into their respective project. +-> Use the function length_conversion() for conversion of volume units. +-> Parameters : + -> value : The number of from units you want to convert + -> from_type : From which type you want to convert + -> to_type : To which type you want to convert +REFERENCES : +-> Wikipedia reference: https://en.wikipedia.org/wiki/Cubic_metre +-> Wikipedia reference: https://en.wikipedia.org/wiki/Litre +-> Wikipedia reference: https://en.wiktionary.org/wiki/kilolitre +-> Wikipedia reference: https://en.wikipedia.org/wiki/Gallon +-> Wikipedia reference: https://en.wikipedia.org/wiki/Cubic_yard +-> Wikipedia reference: https://en.wikipedia.org/wiki/Cubic_foot +-> Wikipedia reference: https://en.wikipedia.org/wiki/Cup_(unit) +""" + +from typing import NamedTuple + + +class FromTo(NamedTuple): + from_factor: float + to_factor: float + + +METRIC_CONVERSION = { + "cubic meter": FromTo(1, 1), + "litre": FromTo(0.001, 1000), + "kilolitre": FromTo(1, 1), + "gallon": FromTo(0.00454, 264.172), + "cubic yard": FromTo(0.76455, 1.30795), + "cubic foot": FromTo(0.028, 35.3147), + "cup": FromTo(0.000236588, 4226.75), +} + + +def volume_conversion(value: float, from_type: str, to_type: str) -> float: + """ + Conversion between volume units. + >>> volume_conversion(4, "cubic meter", "litre") + 4000 + >>> volume_conversion(1, "litre", "gallon") + 0.264172 + >>> volume_conversion(1, "kilolitre", "cubic meter") + 1 + >>> volume_conversion(3, "gallon", "cubic yard") + 0.017814279 + >>> volume_conversion(2, "cubic yard", "litre") + 1529.1 + >>> volume_conversion(4, "cubic foot", "cup") + 473.396 + >>> volume_conversion(1, "cup", "kilolitre") + 0.000236588 + >>> volume_conversion(4, "wrongUnit", "litre") + Traceback (most recent call last): + ... + ValueError: Invalid 'from_type' value: 'wrongUnit' Supported values are: + cubic meter, litre, kilolitre, gallon, cubic yard, cubic foot, cup + """ + if from_type not in METRIC_CONVERSION: + raise ValueError( + f"Invalid 'from_type' value: {from_type!r} Supported values are:\n" + + ", ".join(METRIC_CONVERSION) + ) + if to_type not in METRIC_CONVERSION: + raise ValueError( + f"Invalid 'to_type' value: {to_type!r}. Supported values are:\n" + + ", ".join(METRIC_CONVERSION) + ) + return ( + value + * METRIC_CONVERSION[from_type].from_factor + * METRIC_CONVERSION[to_type].to_factor + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/weight_conversion.py b/conversions/weight_conversion.py new file mode 100644 index 000000000000..0777aead9f02 --- /dev/null +++ b/conversions/weight_conversion.py @@ -0,0 +1,319 @@ +""" +Conversion of weight units. + +__author__ = "Anubhav Solanki" +__license__ = "MIT" +__version__ = "1.1.0" +__maintainer__ = "Anubhav Solanki" +__email__ = "anubhavsolanki0@gmail.com" + +USAGE : +-> Import this file into their respective project. +-> Use the function weight_conversion() for conversion of weight units. +-> Parameters : + -> from_type : From which type you want to convert + -> to_type : To which type you want to convert + -> value : the value which you want to convert + +REFERENCES : + +-> Wikipedia reference: https://en.wikipedia.org/wiki/Kilogram +-> Wikipedia reference: https://en.wikipedia.org/wiki/Gram +-> Wikipedia reference: https://en.wikipedia.org/wiki/Millimetre +-> Wikipedia reference: https://en.wikipedia.org/wiki/Tonne +-> Wikipedia reference: https://en.wikipedia.org/wiki/Long_ton +-> Wikipedia reference: https://en.wikipedia.org/wiki/Short_ton +-> Wikipedia reference: https://en.wikipedia.org/wiki/Pound +-> Wikipedia reference: https://en.wikipedia.org/wiki/Ounce +-> Wikipedia reference: https://en.wikipedia.org/wiki/Fineness#Karat +-> Wikipedia reference: https://en.wikipedia.org/wiki/Dalton_(unit) +-> Wikipedia reference: https://en.wikipedia.org/wiki/Stone_(unit) +""" + +KILOGRAM_CHART: dict[str, float] = { + "kilogram": 1, + "gram": pow(10, 3), + "milligram": pow(10, 6), + "metric-ton": pow(10, -3), + "long-ton": 0.0009842073, + "short-ton": 0.0011023122, + "pound": 2.2046244202, + "stone": 0.1574731728, + "ounce": 35.273990723, + "carrat": 5000, + "atomic-mass-unit": 6.022136652e26, +} + +WEIGHT_TYPE_CHART: dict[str, float] = { + "kilogram": 1, + "gram": pow(10, -3), + "milligram": pow(10, -6), + "metric-ton": pow(10, 3), + "long-ton": 1016.04608, + "short-ton": 907.184, + "pound": 0.453592, + "stone": 6.35029, + "ounce": 0.0283495, + "carrat": 0.0002, + "atomic-mass-unit": 1.660540199e-27, +} + + +def weight_conversion(from_type: str, to_type: str, value: float) -> float: + """ + Conversion of weight unit with the help of KILOGRAM_CHART + + "kilogram" : 1, + "gram" : pow(10, 3), + "milligram" : pow(10, 6), + "metric-ton" : pow(10, -3), + "long-ton" : 0.0009842073, + "short-ton" : 0.0011023122, + "pound" : 2.2046244202, + "stone": 0.1574731728, + "ounce" : 35.273990723, + "carrat" : 5000, + "atomic-mass-unit" : 6.022136652E+26 + + >>> weight_conversion("kilogram","kilogram",4) + 4 + >>> weight_conversion("kilogram","gram",1) + 1000 + >>> weight_conversion("kilogram","milligram",4) + 4000000 + >>> weight_conversion("kilogram","metric-ton",4) + 0.004 + >>> weight_conversion("kilogram","long-ton",3) + 0.0029526219 + >>> weight_conversion("kilogram","short-ton",1) + 0.0011023122 + >>> weight_conversion("kilogram","pound",4) + 8.8184976808 + >>> weight_conversion("kilogram","stone",5) + 0.7873658640000001 + >>> weight_conversion("kilogram","ounce",4) + 141.095962892 + >>> weight_conversion("kilogram","carrat",3) + 15000 + >>> weight_conversion("kilogram","atomic-mass-unit",1) + 6.022136652e+26 + >>> weight_conversion("gram","kilogram",1) + 0.001 + >>> weight_conversion("gram","gram",3) + 3.0 + >>> weight_conversion("gram","milligram",2) + 2000.0 + >>> weight_conversion("gram","metric-ton",4) + 4e-06 + >>> weight_conversion("gram","long-ton",3) + 2.9526219e-06 + >>> weight_conversion("gram","short-ton",3) + 3.3069366000000003e-06 + >>> weight_conversion("gram","pound",3) + 0.0066138732606 + >>> weight_conversion("gram","stone",4) + 0.0006298926912000001 + >>> weight_conversion("gram","ounce",1) + 0.035273990723 + >>> weight_conversion("gram","carrat",2) + 10.0 + >>> weight_conversion("gram","atomic-mass-unit",1) + 6.022136652e+23 + >>> weight_conversion("milligram","kilogram",1) + 1e-06 + >>> weight_conversion("milligram","gram",2) + 0.002 + >>> weight_conversion("milligram","milligram",3) + 3.0 + >>> weight_conversion("milligram","metric-ton",3) + 3e-09 + >>> weight_conversion("milligram","long-ton",3) + 2.9526219e-09 + >>> weight_conversion("milligram","short-ton",1) + 1.1023122e-09 + >>> weight_conversion("milligram","pound",3) + 6.6138732605999995e-06 + >>> weight_conversion("milligram","ounce",2) + 7.054798144599999e-05 + >>> weight_conversion("milligram","carrat",1) + 0.005 + >>> weight_conversion("milligram","atomic-mass-unit",1) + 6.022136652e+20 + >>> weight_conversion("metric-ton","kilogram",2) + 2000 + >>> weight_conversion("metric-ton","gram",2) + 2000000 + >>> weight_conversion("metric-ton","milligram",3) + 3000000000 + >>> weight_conversion("metric-ton","metric-ton",2) + 2.0 + >>> weight_conversion("metric-ton","long-ton",3) + 2.9526219 + >>> weight_conversion("metric-ton","short-ton",2) + 2.2046244 + >>> weight_conversion("metric-ton","pound",3) + 6613.8732606 + >>> weight_conversion("metric-ton","ounce",4) + 141095.96289199998 + >>> weight_conversion("metric-ton","carrat",4) + 20000000 + >>> weight_conversion("metric-ton","atomic-mass-unit",1) + 6.022136652e+29 + >>> weight_conversion("long-ton","kilogram",4) + 4064.18432 + >>> weight_conversion("long-ton","gram",4) + 4064184.32 + >>> weight_conversion("long-ton","milligram",3) + 3048138240.0 + >>> weight_conversion("long-ton","metric-ton",4) + 4.06418432 + >>> weight_conversion("long-ton","long-ton",3) + 2.999999907217152 + >>> weight_conversion("long-ton","short-ton",1) + 1.119999989746176 + >>> weight_conversion("long-ton","pound",3) + 6720.000000049448 + >>> weight_conversion("long-ton","ounce",1) + 35840.000000060514 + >>> weight_conversion("long-ton","carrat",4) + 20320921.599999998 + >>> weight_conversion("long-ton","atomic-mass-unit",4) + 2.4475073353955697e+30 + >>> weight_conversion("short-ton","kilogram",3) + 2721.5519999999997 + >>> weight_conversion("short-ton","gram",3) + 2721552.0 + >>> weight_conversion("short-ton","milligram",1) + 907184000.0 + >>> weight_conversion("short-ton","metric-ton",4) + 3.628736 + >>> weight_conversion("short-ton","long-ton",3) + 2.6785713457296 + >>> weight_conversion("short-ton","short-ton",3) + 2.9999999725344 + >>> weight_conversion("short-ton","pound",2) + 4000.0000000294335 + >>> weight_conversion("short-ton","ounce",4) + 128000.00000021611 + >>> weight_conversion("short-ton","carrat",4) + 18143680.0 + >>> weight_conversion("short-ton","atomic-mass-unit",1) + 5.463186016507968e+29 + >>> weight_conversion("pound","kilogram",4) + 1.814368 + >>> weight_conversion("pound","gram",2) + 907.184 + >>> weight_conversion("pound","milligram",3) + 1360776.0 + >>> weight_conversion("pound","metric-ton",3) + 0.001360776 + >>> weight_conversion("pound","long-ton",2) + 0.0008928571152432 + >>> weight_conversion("pound","short-ton",1) + 0.0004999999954224 + >>> weight_conversion("pound","pound",3) + 3.0000000000220752 + >>> weight_conversion("pound","ounce",1) + 16.000000000027015 + >>> weight_conversion("pound","carrat",1) + 2267.96 + >>> weight_conversion("pound","atomic-mass-unit",4) + 1.0926372033015936e+27 + >>> weight_conversion("stone","kilogram",5) + 31.751450000000002 + >>> weight_conversion("stone","gram",2) + 12700.58 + >>> weight_conversion("stone","milligram",3) + 19050870.0 + >>> weight_conversion("stone","metric-ton",3) + 0.01905087 + >>> weight_conversion("stone","long-ton",3) + 0.018750005325351003 + >>> weight_conversion("stone","short-ton",3) + 0.021000006421614002 + >>> weight_conversion("stone","pound",2) + 28.00000881870372 + >>> weight_conversion("stone","ounce",1) + 224.00007054835967 + >>> weight_conversion("stone","carrat",2) + 63502.9 + >>> weight_conversion("ounce","kilogram",3) + 0.0850485 + >>> weight_conversion("ounce","gram",3) + 85.0485 + >>> weight_conversion("ounce","milligram",4) + 113398.0 + >>> weight_conversion("ounce","metric-ton",4) + 0.000113398 + >>> weight_conversion("ounce","long-ton",4) + 0.0001116071394054 + >>> weight_conversion("ounce","short-ton",4) + 0.0001249999988556 + >>> weight_conversion("ounce","pound",1) + 0.0625000000004599 + >>> weight_conversion("ounce","ounce",2) + 2.000000000003377 + >>> weight_conversion("ounce","carrat",1) + 141.7475 + >>> weight_conversion("ounce","atomic-mass-unit",1) + 1.70724563015874e+25 + >>> weight_conversion("carrat","kilogram",1) + 0.0002 + >>> weight_conversion("carrat","gram",4) + 0.8 + >>> weight_conversion("carrat","milligram",2) + 400.0 + >>> weight_conversion("carrat","metric-ton",2) + 4.0000000000000003e-07 + >>> weight_conversion("carrat","long-ton",3) + 5.9052438e-07 + >>> weight_conversion("carrat","short-ton",4) + 8.818497600000002e-07 + >>> weight_conversion("carrat","pound",1) + 0.00044092488404000004 + >>> weight_conversion("carrat","ounce",2) + 0.0141095962892 + >>> weight_conversion("carrat","carrat",4) + 4.0 + >>> weight_conversion("carrat","atomic-mass-unit",4) + 4.8177093216e+23 + >>> weight_conversion("atomic-mass-unit","kilogram",4) + 6.642160796e-27 + >>> weight_conversion("atomic-mass-unit","gram",2) + 3.321080398e-24 + >>> weight_conversion("atomic-mass-unit","milligram",2) + 3.3210803980000002e-21 + >>> weight_conversion("atomic-mass-unit","metric-ton",3) + 4.9816205970000004e-30 + >>> weight_conversion("atomic-mass-unit","long-ton",3) + 4.9029473573977584e-30 + >>> weight_conversion("atomic-mass-unit","short-ton",1) + 1.830433719948128e-30 + >>> weight_conversion("atomic-mass-unit","pound",3) + 1.0982602420317504e-26 + >>> weight_conversion("atomic-mass-unit","ounce",2) + 1.1714775914938915e-25 + >>> weight_conversion("atomic-mass-unit","carrat",2) + 1.660540199e-23 + >>> weight_conversion("atomic-mass-unit","atomic-mass-unit",2) + 1.999999998903455 + >>> weight_conversion("slug", "kilogram", 1) + Traceback (most recent call last): + ... + ValueError: Invalid 'from_type' or 'to_type' value: 'slug', 'kilogram' + Supported values are: kilogram, gram, milligram, metric-ton, long-ton, short-ton, \ +pound, stone, ounce, carrat, atomic-mass-unit + """ + if to_type not in KILOGRAM_CHART or from_type not in WEIGHT_TYPE_CHART: + msg = ( + f"Invalid 'from_type' or 'to_type' value: {from_type!r}, {to_type!r}\n" + f"Supported values are: {', '.join(WEIGHT_TYPE_CHART)}" + ) + raise ValueError(msg) + return value * KILOGRAM_CHART[to_type] * WEIGHT_TYPE_CHART[from_type] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_compression/README.md b/data_compression/README.md new file mode 100644 index 000000000000..bad7ae1a2f76 --- /dev/null +++ b/data_compression/README.md @@ -0,0 +1,10 @@ +# Compression + +Data compression is everywhere, you need it to store data without taking too much space. +Either the compression loses some data (then we talk about lossy compression, such as .jpg) or it does not (and then it is lossless compression, such as .png) + +Lossless compression is mainly used for archive purpose as it allows storing data without losing information about the file archived. On the other hand, lossy compression is used for transfer of file where quality isn't necessarily what is required (i.e: images on Twitter). + +* +* +* diff --git a/data_compression/__init__.py b/data_compression/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_compression/burrows_wheeler.py b/data_compression/burrows_wheeler.py new file mode 100644 index 000000000000..857d677c904e --- /dev/null +++ b/data_compression/burrows_wheeler.py @@ -0,0 +1,177 @@ +""" +https://en.wikipedia.org/wiki/Burrows%E2%80%93Wheeler_transform + +The Burrows-Wheeler transform (BWT, also called block-sorting compression) +rearranges a character string into runs of similar characters. This is useful +for compression, since it tends to be easy to compress a string that has runs +of repeated characters by techniques such as move-to-front transform and +run-length encoding. More importantly, the transformation is reversible, +without needing to store any additional data except the position of the first +original character. The BWT is thus a "free" method of improving the efficiency +of text compression algorithms, costing only some extra computation. +""" + +from __future__ import annotations + +from typing import TypedDict + + +class BWTTransformDict(TypedDict): + bwt_string: str + idx_original_string: int + + +def all_rotations(s: str) -> list[str]: + """ + :param s: The string that will be rotated len(s) times. + :return: A list with the rotations. + :raises TypeError: If s is not an instance of str. + Examples: + + >>> all_rotations("^BANANA|") # doctest: +NORMALIZE_WHITESPACE + ['^BANANA|', 'BANANA|^', 'ANANA|^B', 'NANA|^BA', 'ANA|^BAN', 'NA|^BANA', + 'A|^BANAN', '|^BANANA'] + >>> all_rotations("a_asa_da_casa") # doctest: +NORMALIZE_WHITESPACE + ['a_asa_da_casa', '_asa_da_casaa', 'asa_da_casaa_', 'sa_da_casaa_a', + 'a_da_casaa_as', '_da_casaa_asa', 'da_casaa_asa_', 'a_casaa_asa_d', + '_casaa_asa_da', 'casaa_asa_da_', 'asaa_asa_da_c', 'saa_asa_da_ca', + 'aa_asa_da_cas'] + >>> all_rotations("panamabanana") # doctest: +NORMALIZE_WHITESPACE + ['panamabanana', 'anamabananap', 'namabananapa', 'amabananapan', + 'mabananapana', 'abananapanam', 'bananapanama', 'ananapanamab', + 'nanapanamaba', 'anapanamaban', 'napanamabana', 'apanamabanan'] + >>> all_rotations(5) + Traceback (most recent call last): + ... + TypeError: The parameter s type must be str. + """ + if not isinstance(s, str): + raise TypeError("The parameter s type must be str.") + + return [s[i:] + s[:i] for i in range(len(s))] + + +def bwt_transform(s: str) -> BWTTransformDict: + """ + :param s: The string that will be used at bwt algorithm + :return: the string composed of the last char of each row of the ordered + rotations and the index of the original string at ordered rotations list + :raises TypeError: If the s parameter type is not str + :raises ValueError: If the s parameter is empty + Examples: + + >>> bwt_transform("^BANANA") + {'bwt_string': 'BNN^AAA', 'idx_original_string': 6} + >>> bwt_transform("a_asa_da_casa") + {'bwt_string': 'aaaadss_c__aa', 'idx_original_string': 3} + >>> bwt_transform("panamabanana") + {'bwt_string': 'mnpbnnaaaaaa', 'idx_original_string': 11} + >>> bwt_transform(4) + Traceback (most recent call last): + ... + TypeError: The parameter s type must be str. + >>> bwt_transform('') + Traceback (most recent call last): + ... + ValueError: The parameter s must not be empty. + """ + if not isinstance(s, str): + raise TypeError("The parameter s type must be str.") + if not s: + raise ValueError("The parameter s must not be empty.") + + rotations = all_rotations(s) + rotations.sort() # sort the list of rotations in alphabetically order + # make a string composed of the last char of each rotation + response: BWTTransformDict = { + "bwt_string": "".join([word[-1] for word in rotations]), + "idx_original_string": rotations.index(s), + } + return response + + +def reverse_bwt(bwt_string: str, idx_original_string: int) -> str: + """ + :param bwt_string: The string returned from bwt algorithm execution + :param idx_original_string: A 0-based index of the string that was used to + generate bwt_string at ordered rotations list + :return: The string used to generate bwt_string when bwt was executed + :raises TypeError: If the bwt_string parameter type is not str + :raises ValueError: If the bwt_string parameter is empty + :raises TypeError: If the idx_original_string type is not int or if not + possible to cast it to int + :raises ValueError: If the idx_original_string value is lower than 0 or + greater than len(bwt_string) - 1 + + >>> reverse_bwt("BNN^AAA", 6) + '^BANANA' + >>> reverse_bwt("aaaadss_c__aa", 3) + 'a_asa_da_casa' + >>> reverse_bwt("mnpbnnaaaaaa", 11) + 'panamabanana' + >>> reverse_bwt(4, 11) + Traceback (most recent call last): + ... + TypeError: The parameter bwt_string type must be str. + >>> reverse_bwt("", 11) + Traceback (most recent call last): + ... + ValueError: The parameter bwt_string must not be empty. + >>> reverse_bwt("mnpbnnaaaaaa", "asd") # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + ... + TypeError: The parameter idx_original_string type must be int or passive + of cast to int. + >>> reverse_bwt("mnpbnnaaaaaa", -1) + Traceback (most recent call last): + ... + ValueError: The parameter idx_original_string must not be lower than 0. + >>> reverse_bwt("mnpbnnaaaaaa", 12) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + ... + ValueError: The parameter idx_original_string must be lower than + len(bwt_string). + >>> reverse_bwt("mnpbnnaaaaaa", 11.0) + 'panamabanana' + >>> reverse_bwt("mnpbnnaaaaaa", 11.4) + 'panamabanana' + """ + if not isinstance(bwt_string, str): + raise TypeError("The parameter bwt_string type must be str.") + if not bwt_string: + raise ValueError("The parameter bwt_string must not be empty.") + try: + idx_original_string = int(idx_original_string) + except ValueError: + raise TypeError( + "The parameter idx_original_string type must be int or passive" + " of cast to int." + ) + if idx_original_string < 0: + raise ValueError("The parameter idx_original_string must not be lower than 0.") + if idx_original_string >= len(bwt_string): + raise ValueError( + "The parameter idx_original_string must be lower than len(bwt_string)." + ) + + ordered_rotations = [""] * len(bwt_string) + for _ in range(len(bwt_string)): + for i in range(len(bwt_string)): + ordered_rotations[i] = bwt_string[i] + ordered_rotations[i] + ordered_rotations.sort() + return ordered_rotations[idx_original_string] + + +if __name__ == "__main__": + entry_msg = "Provide a string that I will generate its BWT transform: " + s = input(entry_msg).strip() + result = bwt_transform(s) + print( + f"Burrows Wheeler transform for string '{s}' results " + f"in '{result['bwt_string']}'" + ) + original_string = reverse_bwt(result["bwt_string"], result["idx_original_string"]) + print( + f"Reversing Burrows Wheeler transform for entry '{result['bwt_string']}' " + f"we get original string '{original_string}'" + ) diff --git a/data_compression/coordinate_compression.py b/data_compression/coordinate_compression.py new file mode 100644 index 000000000000..bdb5f204536b --- /dev/null +++ b/data_compression/coordinate_compression.py @@ -0,0 +1,145 @@ +""" +Assumption: + - The values to compress are assumed to be comparable, + values can be sorted and compared with '<' and '>' operators. +""" + + +class CoordinateCompressor: + """ + A class for coordinate compression. + + This class allows you to compress and decompress a list of values. + + Mapping: + In addition to compression and decompression, this class maintains a mapping + between original values and their compressed counterparts using two data + structures: a dictionary `coordinate_map` and a list `reverse_map`: + - `coordinate_map`: A dictionary that maps original values to their compressed + coordinates. Keys are original values, and values are compressed coordinates. + - `reverse_map`: A list used for reverse mapping, where each index corresponds + to a compressed coordinate, and the value at that index is the original value. + + Example of mapping: + Original: 10, Compressed: 0 + Original: 52, Compressed: 1 + Original: 83, Compressed: 2 + Original: 100, Compressed: 3 + + This mapping allows for efficient compression and decompression of values within + the list. + """ + + def __init__(self, arr: list[int | float | str]) -> None: + """ + Initialize the CoordinateCompressor with a list. + + Args: + arr: The list of values to be compressed. + + >>> arr = [100, 10, 52, 83] + >>> cc = CoordinateCompressor(arr) + >>> cc.compress(100) + 3 + >>> cc.compress(52) + 1 + >>> cc.decompress(1) + 52 + """ + + # A dictionary to store compressed coordinates + self.coordinate_map: dict[int | float | str, int] = {} + + # A list to store reverse mapping + self.reverse_map: list[int | float | str] = [-1] * len(arr) + + self.arr = sorted(arr) # The input list + self.n = len(arr) # The length of the input list + self.compress_coordinates() + + def compress_coordinates(self) -> None: + """ + Compress the coordinates in the input list. + + >>> arr = [100, 10, 52, 83] + >>> cc = CoordinateCompressor(arr) + >>> cc.coordinate_map[83] + 2 + >>> cc.coordinate_map[80] # Value not in the original list + Traceback (most recent call last): + ... + KeyError: 80 + >>> cc.reverse_map[2] + 83 + """ + key = 0 + for val in self.arr: + if val not in self.coordinate_map: + self.coordinate_map[val] = key + self.reverse_map[key] = val + key += 1 + + def compress(self, original: float | str) -> int: + """ + Compress a single value. + + Args: + original: The value to compress. + + Returns: + The compressed integer. + + Raises: + KeyError: If ``original`` was not part of the input list. + + >>> arr = [100, 10, 52, 83] + >>> cc = CoordinateCompressor(arr) + >>> cc.compress(100) + 3 + >>> cc.compress(7) # Value not in the original list + Traceback (most recent call last): + ... + KeyError: 7 + """ + return self.coordinate_map[original] + + def decompress(self, num: int) -> int | float | str: + """ + Decompress a single integer. + + Args: + num: The compressed integer to decompress. + + Returns: + The original value. + + Raises: + IndexError: If ``num`` is not a valid compressed coordinate. + + >>> arr = [100, 10, 52, 83] + >>> cc = CoordinateCompressor(arr) + >>> cc.decompress(0) + 10 + >>> cc.decompress(5) # Compressed coordinate out of range + Traceback (most recent call last): + ... + IndexError: compressed coordinate 5 is out of range + """ + if not 0 <= num < len(self.reverse_map): + msg = f"compressed coordinate {num} is out of range" + raise IndexError(msg) + return self.reverse_map[num] + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + + arr: list[int | float | str] = [100, 10, 52, 83] + cc = CoordinateCompressor(arr) + + for original in arr: + compressed = cc.compress(original) + decompressed = cc.decompress(compressed) + print(f"Original: {decompressed}, Compressed: {compressed}") diff --git a/data_compression/huffman.py b/data_compression/huffman.py new file mode 100644 index 000000000000..15bd311e4fc6 --- /dev/null +++ b/data_compression/huffman.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import sys + + +class Letter: + def __init__(self, letter: str, freq: int) -> None: + self.letter: str = letter + self.freq: int = freq + self.bitstring: dict[str, str] = {} + + def __repr__(self) -> str: + return f"{self.letter}:{self.freq}" + + +class TreeNode: + def __init__( + self, freq: int, left: Letter | TreeNode, right: Letter | TreeNode + ) -> None: + self.freq: int = freq + self.left: Letter | TreeNode = left + self.right: Letter | TreeNode = right + + +def parse_file(file_path: str) -> list[Letter]: + """ + Read the file and build a dict of all letters and their + frequencies, then convert the dict into a list of Letters. + """ + chars: dict[str, int] = {} + with open(file_path) as f: + while True: + c = f.read(1) + if not c: + break + chars[c] = chars[c] + 1 if c in chars else 1 + return sorted((Letter(c, f) for c, f in chars.items()), key=lambda x: x.freq) + + +def build_tree(letters: list[Letter]) -> Letter | TreeNode: + """ + Run through the list of Letters and build the min heap + for the Huffman Tree. + """ + response: list[Letter | TreeNode] = list(letters) + while len(response) > 1: + left = response.pop(0) + right = response.pop(0) + total_freq = left.freq + right.freq + node = TreeNode(total_freq, left, right) + response.append(node) + response.sort(key=lambda x: x.freq) + return response[0] + + +def traverse_tree(root: Letter | TreeNode, bitstring: str) -> list[Letter]: + """ + Recursively traverse the Huffman Tree to set each + Letter's bitstring dictionary, and return the list of Letters + """ + if isinstance(root, Letter): + root.bitstring[root.letter] = bitstring + return [root] + treenode: TreeNode = root + letters = [] + letters += traverse_tree(treenode.left, bitstring + "0") + letters += traverse_tree(treenode.right, bitstring + "1") + return letters + + +def huffman(file_path: str) -> None: + """ + Parse the file, build the tree, then run through the file + again, using the letters dictionary to find and print out the + bitstring for each letter. + """ + letters_list = parse_file(file_path) + root = build_tree(letters_list) + letters = { + k: v for letter in traverse_tree(root, "") for k, v in letter.bitstring.items() + } + print(f"Huffman Coding of {file_path}: ") + with open(file_path) as f: + while True: + c = f.read(1) + if not c: + break + print(letters[c], end=" ") + print() + + +if __name__ == "__main__": + # pass the file path to the huffman function + huffman(sys.argv[1]) diff --git a/data_compression/image_data/PSNR-example-base.png b/data_compression/image_data/PSNR-example-base.png new file mode 100644 index 000000000000..bf12a5450b96 Binary files /dev/null and b/data_compression/image_data/PSNR-example-base.png differ diff --git a/data_compression/image_data/PSNR-example-comp-10.jpg b/data_compression/image_data/PSNR-example-comp-10.jpg new file mode 100644 index 000000000000..a137ebb1100f Binary files /dev/null and b/data_compression/image_data/PSNR-example-comp-10.jpg differ diff --git a/data_compression/image_data/compressed_image.png b/data_compression/image_data/compressed_image.png new file mode 100644 index 000000000000..75c41c21c7b5 Binary files /dev/null and b/data_compression/image_data/compressed_image.png differ diff --git a/data_compression/image_data/example_image.jpg b/data_compression/image_data/example_image.jpg new file mode 100644 index 000000000000..3a58ba3a8c33 Binary files /dev/null and b/data_compression/image_data/example_image.jpg differ diff --git a/data_compression/image_data/example_wikipedia_image.jpg b/data_compression/image_data/example_wikipedia_image.jpg new file mode 100644 index 000000000000..1e983e845651 Binary files /dev/null and b/data_compression/image_data/example_wikipedia_image.jpg differ diff --git a/data_compression/image_data/original_image.png b/data_compression/image_data/original_image.png new file mode 100644 index 000000000000..09a76ffa8ce1 Binary files /dev/null and b/data_compression/image_data/original_image.png differ diff --git a/data_compression/lempel_ziv.py b/data_compression/lempel_ziv.py new file mode 100644 index 000000000000..648b029471bd --- /dev/null +++ b/data_compression/lempel_ziv.py @@ -0,0 +1,125 @@ +""" +One of the several implementations of Lempel-Ziv-Welch compression algorithm +https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch +""" + +import math +import os +import sys + + +def read_file_binary(file_path: str) -> str: + """ + Reads given file as bytes and returns them as a long string + """ + result = "" + try: + with open(file_path, "rb") as binary_file: + data = binary_file.read() + for dat in data: + curr_byte = f"{dat:08b}" + result += curr_byte + return result + except OSError: + print("File not accessible") + sys.exit() + + +def add_key_to_lexicon( + lexicon: dict[str, str], curr_string: str, index: int, last_match_id: str +) -> None: + """ + Adds new strings (curr_string + "0", curr_string + "1") to the lexicon + """ + lexicon.pop(curr_string) + lexicon[curr_string + "0"] = last_match_id + + if math.log2(index).is_integer(): + for curr_key, value in lexicon.items(): + lexicon[curr_key] = f"0{value}" + + lexicon[curr_string + "1"] = bin(index)[2:] + + +def compress_data(data_bits: str) -> str: + """ + Compresses given data_bits using Lempel-Ziv-Welch compression algorithm + and returns the result as a string + """ + lexicon = {"0": "0", "1": "1"} + result, curr_string = "", "" + index = len(lexicon) + + for i in range(len(data_bits)): + curr_string += data_bits[i] + if curr_string not in lexicon: + continue + + last_match_id = lexicon[curr_string] + result += last_match_id + add_key_to_lexicon(lexicon, curr_string, index, last_match_id) + index += 1 + curr_string = "" + + while curr_string != "" and curr_string not in lexicon: + curr_string += "0" + + if curr_string != "": + last_match_id = lexicon[curr_string] + result += last_match_id + + return result + + +def add_file_length(source_path: str, compressed: str) -> str: + """ + Adds given file's length in front (using Elias gamma coding) of the compressed + string + """ + file_length = os.path.getsize(source_path) + file_length_binary = bin(file_length)[2:] + length_length = len(file_length_binary) + + return "0" * (length_length - 1) + file_length_binary + compressed + + +def write_file_binary(file_path: str, to_write: str) -> None: + """ + Writes given to_write string (should only consist of 0's and 1's) as bytes in the + file + """ + byte_length = 8 + try: + with open(file_path, "wb") as opened_file: + result_byte_array = [ + to_write[i : i + byte_length] + for i in range(0, len(to_write), byte_length) + ] + + if len(result_byte_array[-1]) % byte_length == 0: + result_byte_array.append("10000000") + else: + result_byte_array[-1] += "1" + "0" * ( + byte_length - len(result_byte_array[-1]) - 1 + ) + + for elem in result_byte_array: + opened_file.write(int(elem, 2).to_bytes(1, byteorder="big")) + except OSError: + print("File not accessible") + sys.exit() + + +def compress(source_path: str, destination_path: str) -> None: + """ + Reads source file, compresses it and writes the compressed result in destination + file + """ + data_bits = read_file_binary(source_path) + compressed = compress_data(data_bits) + compressed = add_file_length(source_path, compressed) + write_file_binary(destination_path, compressed) + + +if __name__ == "__main__": + compress(sys.argv[1], sys.argv[2]) diff --git a/data_compression/lempel_ziv_decompress.py b/data_compression/lempel_ziv_decompress.py new file mode 100644 index 000000000000..225e96236c2c --- /dev/null +++ b/data_compression/lempel_ziv_decompress.py @@ -0,0 +1,111 @@ +""" +One of the several implementations of Lempel-Ziv-Welch decompression algorithm +https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch +""" + +import math +import sys + + +def read_file_binary(file_path: str) -> str: + """ + Reads given file as bytes and returns them as a long string + """ + result = "" + try: + with open(file_path, "rb") as binary_file: + data = binary_file.read() + for dat in data: + curr_byte = f"{dat:08b}" + result += curr_byte + return result + except OSError: + print("File not accessible") + sys.exit() + + +def decompress_data(data_bits: str) -> str: + """ + Decompresses given data_bits using Lempel-Ziv-Welch compression algorithm + and returns the result as a string + """ + lexicon = {"0": "0", "1": "1"} + result, curr_string = "", "" + index = len(lexicon) + + for i in range(len(data_bits)): + curr_string += data_bits[i] + if curr_string not in lexicon: + continue + + last_match_id = lexicon[curr_string] + result += last_match_id + lexicon[curr_string] = last_match_id + "0" + + if math.log2(index).is_integer(): + new_lex = {} + for curr_key in list(lexicon): + new_lex["0" + curr_key] = lexicon.pop(curr_key) + lexicon = new_lex + + lexicon[bin(index)[2:]] = last_match_id + "1" + index += 1 + curr_string = "" + return result + + +def write_file_binary(file_path: str, to_write: str) -> None: + """ + Writes given to_write string (should only consist of 0's and 1's) as bytes in the + file + """ + byte_length = 8 + try: + with open(file_path, "wb") as opened_file: + result_byte_array = [ + to_write[i : i + byte_length] + for i in range(0, len(to_write), byte_length) + ] + + if len(result_byte_array[-1]) % byte_length == 0: + result_byte_array.append("10000000") + else: + result_byte_array[-1] += "1" + "0" * ( + byte_length - len(result_byte_array[-1]) - 1 + ) + + for elem in result_byte_array[:-1]: + opened_file.write(int(elem, 2).to_bytes(1, byteorder="big")) + except OSError: + print("File not accessible") + sys.exit() + + +def remove_prefix(data_bits: str) -> str: + """ + Removes size prefix, that compressed file should have + Returns the result + """ + counter = 0 + for letter in data_bits: + if letter == "1": + break + counter += 1 + + data_bits = data_bits[counter:] + data_bits = data_bits[counter + 1 :] + return data_bits + + +def compress(source_path: str, destination_path: str) -> None: + """ + Reads source file, decompresses it and writes the result in destination file + """ + data_bits = read_file_binary(source_path) + data_bits = remove_prefix(data_bits) + decompressed = decompress_data(data_bits) + write_file_binary(destination_path, decompressed) + + +if __name__ == "__main__": + compress(sys.argv[1], sys.argv[2]) diff --git a/data_compression/lz77.py b/data_compression/lz77.py new file mode 100644 index 000000000000..09b8b021e9d5 --- /dev/null +++ b/data_compression/lz77.py @@ -0,0 +1,225 @@ +""" +LZ77 compression algorithm +- lossless data compression published in papers by Abraham Lempel and Jacob Ziv in 1977 +- also known as LZ1 or sliding-window compression +- form the basis for many variations including LZW, LZSS, LZMA and others + +It uses a “sliding windowâ€� method. Within the sliding window we have: + - search buffer + - look ahead buffer +len(sliding_window) = len(search_buffer) + len(look_ahead_buffer) + +LZ77 manages a dictionary that uses triples composed of: + - Offset into search buffer, it's the distance between the start of a phrase and + the beginning of a file. + - Length of the match, it's the number of characters that make up a phrase. + - The indicator is represented by a character that is going to be encoded next. + +As a file is parsed, the dictionary is dynamically updated to reflect the compressed +data contents and size. + +Examples: +"cabracadabrarrarrad" <-> [(0, 0, 'c'), (0, 0, 'a'), (0, 0, 'b'), (0, 0, 'r'), + (3, 1, 'c'), (2, 1, 'd'), (7, 4, 'r'), (3, 5, 'd')] +"ababcbababaa" <-> [(0, 0, 'a'), (0, 0, 'b'), (2, 2, 'c'), (4, 3, 'a'), (2, 2, 'a')] +"aacaacabcabaaac" <-> [(0, 0, 'a'), (1, 1, 'c'), (3, 4, 'b'), (3, 3, 'a'), (1, 2, 'c')] + +Sources: +en.wikipedia.org/wiki/LZ77_and_LZ78 +""" + +from dataclasses import dataclass + +__version__ = "0.1" +__author__ = "Lucia Harcekova" + + +@dataclass +class Token: + """ + Dataclass representing triplet called token consisting of length, offset + and indicator. This triplet is used during LZ77 compression. + """ + + offset: int + length: int + indicator: str + + def __repr__(self) -> str: + """ + >>> token = Token(1, 2, "c") + >>> repr(token) + '(1, 2, c)' + >>> str(token) + '(1, 2, c)' + """ + return f"({self.offset}, {self.length}, {self.indicator})" + + +class LZ77Compressor: + """ + Class containing compress and decompress methods using LZ77 compression algorithm. + """ + + def __init__(self, window_size: int = 13, lookahead_buffer_size: int = 6) -> None: + self.window_size = window_size + self.lookahead_buffer_size = lookahead_buffer_size + self.search_buffer_size = self.window_size - self.lookahead_buffer_size + + def compress(self, text: str) -> list[Token]: + """ + Compress the given string text using LZ77 compression algorithm. + + Args: + text: string to be compressed + + Returns: + output: the compressed text as a list of Tokens + + >>> lz77_compressor = LZ77Compressor() + >>> str(lz77_compressor.compress("ababcbababaa")) + '[(0, 0, a), (0, 0, b), (2, 2, c), (4, 3, a), (2, 2, a)]' + >>> str(lz77_compressor.compress("aacaacabcabaaac")) + '[(0, 0, a), (1, 1, c), (3, 4, b), (3, 3, a), (1, 2, c)]' + """ + + output = [] + search_buffer = "" + + # while there are still characters in text to compress + while text: + # find the next encoding phrase + # - triplet with offset, length, indicator (the next encoding character) + token = self._find_encoding_token(text, search_buffer) + + # update the search buffer: + # - add new characters from text into it + # - check if size exceed the max search buffer size, if so, drop the + # oldest elements + search_buffer += text[: token.length + 1] + if len(search_buffer) > self.search_buffer_size: + search_buffer = search_buffer[-self.search_buffer_size :] + + # update the text + text = text[token.length + 1 :] + + # append the token to output + output.append(token) + + return output + + def decompress(self, tokens: list[Token]) -> str: + """ + Convert the list of tokens into an output string. + + Args: + tokens: list containing triplets (offset, length, char) + + Returns: + output: decompressed text + + Tests: + >>> lz77_compressor = LZ77Compressor() + >>> lz77_compressor.decompress([Token(0, 0, 'c'), Token(0, 0, 'a'), + ... Token(0, 0, 'b'), Token(0, 0, 'r'), Token(3, 1, 'c'), + ... Token(2, 1, 'd'), Token(7, 4, 'r'), Token(3, 5, 'd')]) + 'cabracadabrarrarrad' + >>> lz77_compressor.decompress([Token(0, 0, 'a'), Token(0, 0, 'b'), + ... Token(2, 2, 'c'), Token(4, 3, 'a'), Token(2, 2, 'a')]) + 'ababcbababaa' + >>> lz77_compressor.decompress([Token(0, 0, 'a'), Token(1, 1, 'c'), + ... Token(3, 4, 'b'), Token(3, 3, 'a'), Token(1, 2, 'c')]) + 'aacaacabcabaaac' + """ + + output = "" + + for token in tokens: + for _ in range(token.length): + output += output[-token.offset] + output += token.indicator + + return output + + def _find_encoding_token(self, text: str, search_buffer: str) -> Token: + """Finds the encoding token for the first character in the text. + + Tests: + >>> lz77_compressor = LZ77Compressor() + >>> lz77_compressor._find_encoding_token("abrarrarrad", "abracad").offset + 7 + >>> lz77_compressor._find_encoding_token("adabrarrarrad", "cabrac").length + 1 + >>> lz77_compressor._find_encoding_token("abc", "xyz").offset + 0 + >>> lz77_compressor._find_encoding_token("", "xyz").offset + Traceback (most recent call last): + ... + ValueError: We need some text to work with. + >>> lz77_compressor._find_encoding_token("abc", "").offset + 0 + """ + + if not text: + raise ValueError("We need some text to work with.") + + # Initialise result parameters to default values + length, offset = 0, 0 + + if not search_buffer: + return Token(offset, length, text[length]) + + for i, character in enumerate(search_buffer): + found_offset = len(search_buffer) - i + if character == text[0]: + found_length = self._match_length_from_index(text, search_buffer, 0, i) + # if the found length is bigger than the current or if it's equal, + # which means it's offset is smaller: update offset and length + if found_length >= length: + offset, length = found_offset, found_length + + return Token(offset, length, text[length]) + + def _match_length_from_index( + self, text: str, window: str, text_index: int, window_index: int + ) -> int: + """Calculate the longest possible match of text and window characters from + text_index in text and window_index in window. + + Args: + text: _description_ + window: sliding window + text_index: index of character in text + window_index: index of character in sliding window + + Returns: + The maximum match between text and window, from given indexes. + + Tests: + >>> lz77_compressor = LZ77Compressor(13, 6) + >>> lz77_compressor._match_length_from_index("rarrad", "adabrar", 0, 4) + 5 + >>> lz77_compressor._match_length_from_index("adabrarrarrad", + ... "cabrac", 0, 1) + 1 + """ + if not text or text[text_index] != window[window_index]: + return 0 + return 1 + self._match_length_from_index( + text, window + text[text_index], text_index + 1, window_index + 1 + ) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + # Initialize compressor class + lz77_compressor = LZ77Compressor(window_size=13, lookahead_buffer_size=6) + + # Example + TEXT = "cabracadabrarrarrad" + compressed_text = lz77_compressor.compress(TEXT) + print(lz77_compressor.compress("ababcbababaa")) + decompressed_text = lz77_compressor.decompress(compressed_text) + assert decompressed_text == TEXT, "The LZ77 algorithm returned the invalid result." diff --git a/data_compression/move_to_front.py b/data_compression/move_to_front.py new file mode 100644 index 000000000000..07bdc32f7b34 --- /dev/null +++ b/data_compression/move_to_front.py @@ -0,0 +1,99 @@ +""" +Move-to-front transform. + +The move-to-front transform encodes each symbol as its current index in an +ordered alphabet, then moves that symbol to the front of the alphabet. +It is commonly used after the Burrows-Wheeler transform in lossless +compression pipelines. + +Reference: https://en.wikipedia.org/wiki/Move-to-front_transform +""" + + +def _validated_alphabet(alphabet: str) -> list[str]: + """ + Return a mutable alphabet list after validating uniqueness. + + >>> _validated_alphabet("abc") + ['a', 'b', 'c'] + >>> _validated_alphabet("aba") + Traceback (most recent call last): + ... + ValueError: alphabet must contain unique characters + """ + if not isinstance(alphabet, str): + raise TypeError("alphabet must be a string") + if len(set(alphabet)) != len(alphabet): + raise ValueError("alphabet must contain unique characters") + return list(alphabet) + + +def move_to_front_encode(text: str, alphabet: str) -> list[int]: + """ + Encode text using the move-to-front transform. + + >>> move_to_front_encode("banana", "abcdefghijklmnopqrstuvwxyz") + [1, 1, 13, 1, 1, 1] + >>> move_to_front_encode("banana", "abn") + [1, 1, 2, 1, 1, 1] + >>> move_to_front_encode("", "abc") + [] + >>> move_to_front_encode("bad", "abc") + Traceback (most recent call last): + ... + ValueError: character 'd' is not in the alphabet + """ + if not isinstance(text, str): + raise TypeError("text must be a string") + + symbols = _validated_alphabet(alphabet) + encoded_text: list[int] = [] + + for char in text: + try: + char_index = symbols.index(char) + except ValueError: + message = f"character {char!r} is not in the alphabet" + raise ValueError(message) from None + encoded_text.append(char_index) + symbols.insert(0, symbols.pop(char_index)) + + return encoded_text + + +def move_to_front_decode(encoded_text: list[int], alphabet: str) -> str: + """ + Decode a move-to-front encoded list of indexes. + + >>> move_to_front_decode([1, 1, 13, 1, 1, 1], "abcdefghijklmnopqrstuvwxyz") + 'banana' + >>> move_to_front_decode([1, 1, 2, 1, 1, 1], "abn") + 'banana' + >>> move_to_front_decode([], "abc") + '' + >>> move_to_front_decode([3], "abc") + Traceback (most recent call last): + ... + ValueError: index 3 is not valid for alphabet size 3 + >>> move_to_front_decode([-1], "abc") + Traceback (most recent call last): + ... + ValueError: index -1 is not valid for alphabet size 3 + """ + symbols = _validated_alphabet(alphabet) + decoded_text = [] + + for index in encoded_text: + if not 0 <= index < len(symbols): + message = f"index {index} is not valid for alphabet size {len(symbols)}" + raise ValueError(message) + decoded_text.append(symbols[index]) + symbols.insert(0, symbols.pop(index)) + + return "".join(decoded_text) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_compression/peak_signal_to_noise_ratio.py b/data_compression/peak_signal_to_noise_ratio.py new file mode 100644 index 000000000000..284f2904a21d --- /dev/null +++ b/data_compression/peak_signal_to_noise_ratio.py @@ -0,0 +1,46 @@ +""" +Peak signal-to-noise ratio - PSNR + https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio +Source: +https://tutorials.techonical.com/how-to-calculate-psnr-value-of-two-images-using-python +""" + +import math +import os + +import cv2 +import numpy as np + +PIXEL_MAX = 255.0 + + +def peak_signal_to_noise_ratio(original: float, contrast: float) -> float: + mse = np.mean((original - contrast) ** 2) + if mse == 0: + return 100 + + return 20 * math.log10(PIXEL_MAX / math.sqrt(mse)) + + +def main() -> None: + dir_path = os.path.dirname(os.path.realpath(__file__)) + # Loading images (original image and compressed image) + original = cv2.imread(os.path.join(dir_path, "image_data/original_image.png")) + contrast = cv2.imread(os.path.join(dir_path, "image_data/compressed_image.png"), 1) + + original2 = cv2.imread(os.path.join(dir_path, "image_data/PSNR-example-base.png")) + contrast2 = cv2.imread( + os.path.join(dir_path, "image_data/PSNR-example-comp-10.jpg"), 1 + ) + + # Value expected: 29.73dB + print("-- First Test --") + print(f"PSNR value is {peak_signal_to_noise_ratio(original, contrast)} dB") + + # # Value expected: 31.53dB (Wikipedia Example) + print("\n-- Second Test --") + print(f"PSNR value is {peak_signal_to_noise_ratio(original2, contrast2)} dB") + + +if __name__ == "__main__": + main() diff --git a/data_compression/run_length_encoding.py b/data_compression/run_length_encoding.py new file mode 100644 index 000000000000..691e19095dc6 --- /dev/null +++ b/data_compression/run_length_encoding.py @@ -0,0 +1,48 @@ +# https://en.wikipedia.org/wiki/Run-length_encoding + + +def run_length_encode(text: str) -> list: + """ + Performs Run Length Encoding + >>> run_length_encode("AAAABBBCCDAA") + [('A', 4), ('B', 3), ('C', 2), ('D', 1), ('A', 2)] + >>> run_length_encode("A") + [('A', 1)] + >>> run_length_encode("AA") + [('A', 2)] + >>> run_length_encode("AAADDDDDDFFFCCCAAVVVV") + [('A', 3), ('D', 6), ('F', 3), ('C', 3), ('A', 2), ('V', 4)] + """ + encoded = [] + count = 1 + + for i in range(len(text)): + if i + 1 < len(text) and text[i] == text[i + 1]: + count += 1 + else: + encoded.append((text[i], count)) + count = 1 + + return encoded + + +def run_length_decode(encoded: list) -> str: + """ + Performs Run Length Decoding + >>> run_length_decode([('A', 4), ('B', 3), ('C', 2), ('D', 1), ('A', 2)]) + 'AAAABBBCCDAA' + >>> run_length_decode([('A', 1)]) + 'A' + >>> run_length_decode([('A', 2)]) + 'AA' + >>> run_length_decode([('A', 3), ('D', 6), ('F', 3), ('C', 3), ('A', 2), ('V', 4)]) + 'AAADDDDDDFFFCCCAAVVVV' + """ + return "".join(char * length for char, length in encoded) + + +if __name__ == "__main__": + from doctest import testmod + + testmod(name="run_length_encode", verbose=True) + testmod(name="run_length_decode", verbose=True) diff --git a/data_structures/AVL/AVL.py b/data_structures/AVL/AVL.py deleted file mode 100644 index 0e3008dc15b1..000000000000 --- a/data_structures/AVL/AVL.py +++ /dev/null @@ -1,131 +0,0 @@ -''' -A AVL tree -''' - - -class Node: - - def __init__(self, label): - self.label = label - self.left = None - self.rigt = None - self.parent = None - self.height = 0 - - def getLabel(self): - return self.label - - def setLabel(self, label): - self.label = label - - def getLeft(self): - return self.left - - def setLeft(self, left): - self.left = left - - def getRight(self): - return self.rigt - - def setRight(self, right): - self.rigt = right - - def getParent(self): - return self.parent - - def setParent(self, parent): - self.parent = parent - - def setHeight(self, height): - self.height = height - - def getHeight(self, height): - return self.height - - -class AVL: - - def __init__(self): - self.root = None - self.size = 0 - - def insert(self, value): - node = Node(value) - if self.root is None: - self.root = node - self.size = 1 - else: - # Same as Binary Tree - dad_node = None - curr_node = self.root - - while True: - if curr_node is not None: - - dad_node = curr_node - - if node.getLabel() < curr_node.getLabel(): - curr_node = curr_node.getLeft() - else: - curr_node = curr_node.getRight() - else: - if node.getLabel() < dad_node.getLabel(): - dad_node.setLeft(node) - dad_node.setHeight(dad_node.getHeight() + 1) - - if (dad_node.getRight().getHeight() - - dad_node.getLeft.getHeight() > 1): - self.rebalance(dad_node) - - else: - dad_node.setRight(node) - dad_node.setHeight(dad_node.getHeight() + 1) - - if (dad_node.getRight().getHeight() - - dad_node.getLeft.getHeight() > 1): - self.rebalance(dad_node) - break - - def rebalance(self, node): - if (node.getRight().getHeight() - - node.getLeft.getHeight() > 1): - if (node.getRight().getHeight() > - node.getLeft.getHeight()): - pass - else: - pass - pass - elif (node.getRight().getHeight() - - node.getLeft.getHeight() > 2): - if (node.getRight().getHeight() > - node.getLeft.getHeight()): - pass - else: - pass - pass - pass - - def rotate_left(self, node): - # TODO: is this pythonic enought? - aux = node.getLabel() - node = aux.getRight() - node.setHeight(node.getHeight() - 1) - node.setLeft(Node(aux)) - node.getLeft().setHeight(node.getHeight() + 1) - node.getRight().setHeight(node.getRight().getHeight() - 1) - - def rotate_right(self, node): - aux = node.getLabel() - node = aux.getLeft() - node.setHeight(node.getHeight() - 1) - node.setRight(Node(aux)) - node.getLeft().setHeight(node.getHeight() + 1) - node.getLeft().setHeight(node.getLeft().getHeight() - 1) - - def double_rotate_left(self, node): - self.rotate_right(node.getRight().getRight()) - self.rotate_left(node) - - def double_rotate_right(self, node): - self.rotate_left(node.getLeft().getLeft()) - self.rotate_right(node) diff --git a/data_structures/Binary Tree/binary_seach_tree.py b/data_structures/Binary Tree/binary_seach_tree.py deleted file mode 100644 index 0b17265344e3..000000000000 --- a/data_structures/Binary Tree/binary_seach_tree.py +++ /dev/null @@ -1,103 +0,0 @@ -''' -A binary search Tree -''' - - -class Node: - - def __init__(self, label): - self.label = label - self.left = None - self.right = None - - def getLabel(self): - return self.label - - def setLabel(self, label): - self.label = label - - def getLeft(self): - return self.left - - def setLeft(self, left): - self.left = left - - def getRight(self): - return self.right - - def setRight(self, right): - self.right = right - - -class BinarySearchTree: - - def __init__(self): - self.root = None - - def insert(self, label): - - # Create a new Node - - node = Node(label) - - if self.empty(): - self.root = node - else: - dad_node = None - curr_node = self.root - - while True: - if curr_node is not None: - - dad_node = curr_node - - if node.getLabel() < curr_node.getLabel(): - curr_node = curr_node.getLeft() - else: - curr_node = curr_node.getRight() - else: - if node.getLabel() < dad_node.getLabel(): - dad_node.setLeft(node) - else: - dad_node.setRight(node) - break - - def empty(self): - if self.root is None: - return True - return False - - def preShow(self, curr_node): - if curr_node is None: - print(curr_node.getLabel(), end=" ") - - self.preShow(curr_node.getLeft()) - self.preShow(curr_node.getRight()) - - def getRoot(self): - return self.root - - -''' -Example - 8 - / \ - 3 10 - / \ \ - 1 6 14 - / \ / - 4 7 13 -''' - -t = BinarySearchTree() -t.insert(8) -t.insert(3) -t.insert(1) -t.insert(6) -t.insert(4) -t.insert(7) -t.insert(10) -t.insert(14) -t.insert(13) - -t.preShow(t.getRoot()) diff --git a/data_structures/Graph/Breadth_First_Search.py b/data_structures/Graph/Breadth_First_Search.py deleted file mode 100644 index 92a6e819b00c..000000000000 --- a/data_structures/Graph/Breadth_First_Search.py +++ /dev/null @@ -1,72 +0,0 @@ -class GRAPH: - """docstring for GRAPH""" - def __init__(self, nodes): - self.nodes = nodes - self.graph = [[0]*nodes for i in range (nodes)] - self.visited = [0]*nodes - - - def show(self): - - for i in self.graph: - for j in i: - print(j, end=' ') - print(' ') - def bfs(self,v): - - visited = [False]*self.vertex - visited[v - 1] = True - print('%d visited' % (v)) - - queue = [v - 1] - while len(queue) > 0: - v = queue[0] - for u in range(self.vertex): - if self.graph[v][u] == 1: - if visited[u] is False: - visited[u] = True - queue.append(u) - print('%d visited' % (u +1)) - queue.pop(0) - -g = Graph(10) - -g.add_edge(1,2) -g.add_edge(1,3) -g.add_edge(1,4) -g.add_edge(2,5) -g.add_edge(3,6) -g.add_edge(3,7) -g.add_edge(4,8) -g.add_edge(5,9) -g.add_edge(6,10) -g.bfs(4) - -print(self.graph) - - def add_edge(self, i, j): - self.graph[i][j]=1 - self.graph[j][i]=1 - - def bfs(self, s): - queue = [s] - self.visited[s] = 1 - while len(queue)!= 0: - x = queue.pop(0) - print(x) - for i in range(0, self.nodes): - if self.graph[x][i] == 1 and self.visited[i] == 0: - queue.append(i) - self.visited[i] = 1 - -n = int(input("Enter the number of Nodes : ")) -g = GRAPH(n) -e = int(input("Enter the no of edges : ")) -print("Enter the edges (u v)") - -for i in range(0, e): - u ,v = map(int, raw_input().split()) - g.add_edge(u, v) - -s = int(input("Enter the source node :")) -g.bfs(s) diff --git a/data_structures/Graph/Deep_First_Search.py b/data_structures/Graph/Deep_First_Search.py deleted file mode 100644 index 656ddfbafd34..000000000000 --- a/data_structures/Graph/Deep_First_Search.py +++ /dev/null @@ -1,32 +0,0 @@ -class GRAPH: - """docstring for GRAPH""" - def __init__(self, nodes): - self.nodes=nodes - self.graph=[[0]*nodes for i in range (nodes)] - self.visited=[0]*nodes - - - def show(self): - print self.graph - - def add_edge(self, i, j): - self.graph[i][j]=1 - self.graph[j][i]=1 - - def dfs(self,s): - self.visited[s]=1 - print(s) - for i in range(0,self.nodes): - if self.visited[i]==0 and self.graph[s][i]==1: - self.dfs(i) - - -n=int(input("Enter the number of Nodes : ")) -g=GRAPH(n) -e=int(input("Enter the no of edges : ")) -print("Enter the edges (u v)") -for i in range(0,e): - u,v=map(int, raw_input().split()) - g.add_edge(u,v) -s=int(input("Enter the source node :")) -g.dfs(s) diff --git a/data_structures/Graph/Graph.py b/data_structures/Graph/Graph.py deleted file mode 100644 index 0fa3f5593eb2..000000000000 --- a/data_structures/Graph/Graph.py +++ /dev/null @@ -1,40 +0,0 @@ -# Author: OMKAR PATHAK - -# We can use Python's dictionary for constructing the graph - -class AdjacencyList(object): - def __init__(self): - self.List = {} - - def addEdge(self, fromVertex, toVertex): - # check if vertex is already present - if fromVertex in self.List.keys(): - self.List[fromVertex].append(toVertex) - else: - self.List[fromVertex] = [toVertex] - - def printList(self): - for i in self.List: - print(i,'->',' -> '.join([str(j) for j in self.List[i]])) - -if __name__ == '__main__': - al = AdjacencyList() - al.addEdge(0, 1) - al.addEdge(0, 4) - al.addEdge(4, 1) - al.addEdge(4, 3) - al.addEdge(1, 0) - al.addEdge(1, 4) - al.addEdge(1, 3) - al.addEdge(1, 2) - al.addEdge(2, 3) - al.addEdge(3, 4) - - al.printList() - - # OUTPUT: - # 0 -> 1 -> 4 - # 1 -> 0 -> 4 -> 3 -> 2 - # 2 -> 3 - # 3 -> 4 - # 4 -> 1 -> 3 diff --git a/data_structures/Graph/Graph_list.py b/data_structures/Graph/Graph_list.py deleted file mode 100644 index 8e33cf55ce2f..000000000000 --- a/data_structures/Graph/Graph_list.py +++ /dev/null @@ -1,28 +0,0 @@ -class Graph: - def __init__(self, vertex): - self.vertex = vertex - self.graph = [[0] for i in range(vertex)] - - def add_edge(self, u, v): - self.graph[u - 1].append(v - 1) - - def show(self): - for i in range(self.vertex): - print('%d: '% (i + 1), end=' ') - for j in self.graph[i]: - print('%d-> '% (j + 1), end=' ') - print(' ') - - - -g = Graph(100) - -g.add_edge(1,3) -g.add_edge(2,3) -g.add_edge(3,4) -g.add_edge(3,5) -g.add_edge(4,5) - - -g.show() - diff --git a/data_structures/Graph/Graph_matrix.py b/data_structures/Graph/Graph_matrix.py deleted file mode 100644 index 1998fec8d6fe..000000000000 --- a/data_structures/Graph/Graph_matrix.py +++ /dev/null @@ -1,29 +0,0 @@ -class Graph: - - def __init__(self, vertex): - self.vertex = vertex - self.graph = [[0] * vertex for i in range(vertex) ] - - def add_edge(self, u, v): - self.graph[u - 1][v - 1] = 1 - self.graph[v - 1][u - 1] = 1 - - def show(self): - - for i in self.graph: - for j in i: - print(j, end=' ') - print(' ') - - - - -g = Graph(100) - -g.add_edge(1,4) -g.add_edge(4,2) -g.add_edge(4,5) -g.add_edge(2,5) -g.add_edge(5,3) -g.show() - diff --git a/data_structures/Heap/heap.py b/data_structures/Heap/heap.py deleted file mode 100644 index d43cc4baa177..000000000000 --- a/data_structures/Heap/heap.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/python - -class Heap: - def __init__(self): - self.h = [] - self.currsize = 0 - - def leftChild(self,i): - if 2*i+1 < self.currsize: - return 2*i+1 - return None - - def rightChild(self,i): - if 2*i+2 < self.currsize: - return 2*i+2 - return None - - def maxHeapify(self,node): - if node < self.currsize: - m = node - lc = self.leftChild(node) - rc = self.rightChild(node) - if lc is not None and self.h[lc] > self.h[m]: - m = lc - if rc is not None and self.h[rc] > self.h[m]: - m = rc - if m!=node: - temp = self.h[node] - self.h[node] = self.h[m] - self.h[m] = temp - self.maxHeapify(m) - - def buildHeap(self,a): - self.currsize = len(a) - self.h = list(a) - for i in range(self.currsize/2,-1,-1): - self.maxHeapify(i) - - def getMax(self): - if self.currsize >= 1: - me = self.h[0] - temp = self.h[0] - self.h[0] = self.h[self.currsize-1] - self.h[self.currsize-1] = temp - self.currsize -= 1 - self.maxHeapify(0) - return me - return None - - def heapSort(self): - size = self.currsize - while self.currsize-1 >= 0: - temp = self.h[0] - self.h[0] = self.h[self.currsize-1] - self.h[self.currsize-1] = temp - self.currsize -= 1 - self.maxHeapify(0) - self.currsize = size - - def insert(self,data): - self.h.append(data) - curr = self.currsize - self.currsize+=1 - while self.h[curr] > self.h[curr/2]: - temp = self.h[curr/2] - self.h[curr/2] = self.h[curr] - self.h[curr] = temp - curr = curr/2 - - def display(self): - print (self.h) - -def main(): - l = list(map(int,raw_input().split())) - h = Heap() - h.buildHeap(l) - h.heapSort() - h.display() - -if __name__=='__main__': - main() - - diff --git a/data_structures/LinkedList/DoublyLinkedList.py b/data_structures/LinkedList/DoublyLinkedList.py deleted file mode 100644 index d152e465b399..000000000000 --- a/data_structures/LinkedList/DoublyLinkedList.py +++ /dev/null @@ -1,73 +0,0 @@ -''' -- A linked list is similar to an array, it holds values. However, links in a linked list do not have indexes. -- This is an example of a double ended, doubly linked list. -- Each link references the next link and the previous one. -''' -class LinkedList: - def __init__(self): - self.head = None - self.tail = None - - def insertHead(self, x): - newLink = Link(x) #Create a new link with a value attached to it - if(self.isEmpty() == True): #Set the first element added to be the tail - self.tail = newLink - else: - self.head.previous = newLink # newLink <-- currenthead(head) - newLink.next = self.head # newLink <--> currenthead(head) - self.head = newLink # newLink(head) <--> oldhead - - def deleteHead(self): - temp = self.head - self.head = self.head.next # oldHead <--> 2ndElement(head) - self.head.previous = None # oldHead --> 2ndElement(head) nothing pointing at it so the old head will be removed - if(self.head == None): - self.tail = None - return temp - - def insertTail(self, x): - newLink = Link(x) - newLink.next = None # currentTail(tail) newLink --> - self.tail.next = newLink # currentTail(tail) --> newLink --> - newLink.previous = self.tail #currentTail(tail) <--> newLink --> - self.tail = newLink # oldTail <--> newLink(tail) --> - - def deleteTail(self): - temp = self.tail - self.tail = self.tail.previous # 2ndLast(tail) <--> oldTail --> None - self.tail.next = None # 2ndlast(tail) --> None - return temp - - def delete(self, x): - current = self.head - - while(current.value != x): # Find the position to delete - current = current.next - - if(current == self.head): - self.deleteHead() - - elif(current == self.tail): - self.deleteTail() - - else: #Before: 1 <--> 2(current) <--> 3 - current.previous.next = current.next # 1 --> 3 - current.next.previous = current.previous # 1 <--> 3 - - def isEmpty(self): #Will return True if the list is empty - return(self.head == None) - - def display(self): #Prints contents of the list - current = self.head - while(current != None): - current.displayLink() - current = current.next - print() - -class Link: - next = None #This points to the link in front of the new link - previous = None #This points to the link behind the new link - def __init__(self, x): - self.value = x - def displayLink(self): - print("{}".format(self.value), end=" ") \ No newline at end of file diff --git a/data_structures/LinkedList/__init__.py b/data_structures/LinkedList/__init__.py deleted file mode 100644 index 1d220599fb16..000000000000 --- a/data_structures/LinkedList/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -class Node: - def __init__(self, item, next): - self.item = item - self.next = next - -class LinkedList: - def __init__(self): - self.head = None - - def add(self, item): - self.head = Node(item, self.head) - - def remove(self): - if self.is_empty(): - return None - else: - item = self.head.item - self.head = self.head.next - return item - - def is_empty(self): - return self.head == None diff --git a/data_structures/LinkedList/singly_LinkedList.py b/data_structures/LinkedList/singly_LinkedList.py deleted file mode 100644 index 941e8a0faaa8..000000000000 --- a/data_structures/LinkedList/singly_LinkedList.py +++ /dev/null @@ -1,61 +0,0 @@ -class Node:#create a Node - def __int__(self,data): - self.data=data#given data - self.next=None#given next to None -class Linked_List: - pass - def insert_tail(Head,data):#insert the data at tail - tamp=Head#create a tamp as a head - if(tamp==None):#if linkedlist is empty - newNod=Node()#create newNode Node type and given data and next - newNod.data=data - newNod.next=None - Head=newNod - else: - while tamp.next!=None:#find the last Node - tamp=tamp.next - newNod = Node()#create a new node - newNod.data = data - newNod.next = None - tamp.next=newNod#put the newnode into last node - return Head#return first node of linked list - def insert_head(Head,data): - tamp = Head - if (tamp == None): - newNod = Node()#create a new Node - newNod.data = data - newNod.next = None - Head = newNod#make new node to Head - else: - newNod = Node() - newNod.data = data - newNod.next = Head#put the Head at NewNode Next - Head=newNod#make a NewNode to Head - return Head - def Print(Head):#print every node data - tamp=Node() - tamp=Head - while tamp!=None: - print(tamp.data) - tamp=tamp.next - def delete_head(Head):#delete from head - if Head!=None: - Head=Head.next - return Head#return new Head - def delete_tail(Head):#delete from tail - if Head!=None: - tamp = Node() - tamp = Head - while (tamp.next).next!= None:#find the 2nd last element - tamp = tamp.next - tamp.next=None#delete the last element by give next None to 2nd last Element - return Head - def isEmpty(Head): - if(Head==None):#check Head is None or Not - return True#return Ture if list is empty - else: - return False#check False if it's not empty - - - - diff --git a/data_structures/Queue/QueueOnList.py b/data_structures/Queue/QueueOnList.py deleted file mode 100644 index c8d0b41de5d5..000000000000 --- a/data_structures/Queue/QueueOnList.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Queue represented by a python list""" -class Queue(): - def __init__(self): - self.entries = [] - self.length = 0 - self.front=0 - - def __str__(self): - printed = '<' + str(self.entries)[1:-1] + '>' - return printed - - """Enqueues {@code item} - @param item - item to enqueue""" - def put(self, item): - self.entries.append(item) - self.length = self.length + 1 - - - """Dequeues {@code item} - @requirement: |self.length| > 0 - @return dequeued - item that was dequeued""" - def get(self): - self.length = self.length - 1 - dequeued = self.entries[self.front] - self.front-=1 - self.entries = self.entries[self.front:] - return dequeued - - """Rotates the queue {@code rotation} times - @param rotation - number of times to rotate queue""" - def rotate(self, rotation): - for i in range(rotation): - self.put(self.get()) - - """Enqueues {@code item} - @return item at front of self.entries""" - def front(self): - return self.entries[0] - - """Returns the length of this.entries""" - def size(self): - return self.length diff --git a/data_structures/Stacks/Balanced_Parentheses.py b/data_structures/Stacks/Balanced_Parentheses.py deleted file mode 100644 index 6b7740380841..000000000000 --- a/data_structures/Stacks/Balanced_Parentheses.py +++ /dev/null @@ -1,27 +0,0 @@ -# Author: OMKAR PATHAK - -import Stack - -def parseParenthesis(string): - balanced = 1 - index = 0 - myStack = Stack.Stack(len(string)) - while (index < len(string)) and (balanced == 1): - check = string[index] - if check == '(': - myStack.push(check) - else: - if myStack.isEmpty(): - balanced = 0 - else: - myStack.pop() - index += 1 - - if balanced == 1 and myStack.isEmpty(): - return True - else: - return False - -if __name__ == '__main__': - print(parseParenthesis('((()))')) # True - print(parseParenthesis('((())')) # False diff --git a/data_structures/Stacks/Infix_To_Postfix_Conversion.py b/data_structures/Stacks/Infix_To_Postfix_Conversion.py deleted file mode 100644 index e33926a3d14c..000000000000 --- a/data_structures/Stacks/Infix_To_Postfix_Conversion.py +++ /dev/null @@ -1,48 +0,0 @@ -# Author: OMKAR PATHAK - -import Stack - -def isOperand(char): - return (ord(char) >= ord('a') and ord(char) <= ord('z')) or (ord(char) >= ord('A') and ord(char) <= ord('Z')) - -def precedence(char): - if char == '+' or char == '-': - return 1 - elif char == '*' or char == '/': - return 2 - elif char == '^': - return 3 - else: - return -1 - -def infixToPostfix(myExp, myStack): - postFix = [] - for i in range(len(myExp)): - if (isOperand(myExp[i])): - postFix.append(myExp[i]) - elif(myExp[i] == '('): - myStack.push(myExp[i]) - elif(myExp[i] == ')'): - topOperator = myStack.pop() - while(not myStack.isEmpty() and topOperator != '('): - postFix.append(topOperator) - topOperator = myStack.pop() - else: - while (not myStack.isEmpty()) and (precedence(myExp[i]) <= precedence(myStack.peek())): - postFix.append(myStack.pop()) - myStack.push(myExp[i]) - - while(not myStack.isEmpty()): - postFix.append(myStack.pop()) - return ' '.join(postFix) - -if __name__ == '__main__': - myExp = 'a+b*(c^d-e)^(f+g*h)-i' - myExp = [i for i in myExp] - print('Infix:',' '.join(myExp)) - myStack = Stack.Stack(len(myExp)) - print('Postfix:',infixToPostfix(myExp, myStack)) - - # OUTPUT: - # Infix: a + b * ( c ^ d - e ) ^ ( f + g * h ) - i - # Postfix: a b c d ^ e - f g h * + ^ * + i - diff --git a/data_structures/Stacks/Stack.py b/data_structures/Stacks/Stack.py deleted file mode 100644 index 41bbdc9d2e7e..000000000000 --- a/data_structures/Stacks/Stack.py +++ /dev/null @@ -1,50 +0,0 @@ -# Author: OMKAR PATHAK - -class Stack(object): - def __init__(self, limit = 10): - self.stack = [] - self.limit = limit - - # for printing the stack contents - def __str__(self): - return ' '.join([str(i) for i in self.stack]) - - # for pushing an element on to the stack - def push(self, data): - if len(self.stack) >= self.limit: - print('Stack Overflow') - else: - self.stack.append(data) - - # for popping the uppermost element - def pop(self): - if len(self.stack) <= 0: - return -1 - else: - return self.stack.pop() - - # for peeking the top-most element of the stack - def peek(self): - if len(self.stack) <= 0: - return -1 - else: - return self.stack[len(self.stack) - 1] - - # to check if stack is empty - def isEmpty(self): - return self.stack == [] - - # for checking the size of stack - def size(self): - return len(self.stack) - -if __name__ == '__main__': - myStack = Stack() - for i in range(10): - myStack.push(i) - print(myStack) - myStack.pop() # popping the top element - print(myStack) - myStack.peek() # printing the top element - myStack.isEmpty() - myStack.size() diff --git a/data_structures/Stacks/__init__.py b/data_structures/Stacks/__init__.py deleted file mode 100644 index f7e92ae2d269..000000000000 --- a/data_structures/Stacks/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -class Stack: - - def __init__(self): - self.stack = [] - self.top = 0 - - def is_empty(self): - return (self.top == 0) - - def push(self, item): - if self.top < len(self.stack): - self.stack[self.top] = item - else: - self.stack.append(item) - - self.top += 1 - - def pop(self): - if self.is_empty(): - return None - else: - self.top -= 1 - return self.stack[self.top] diff --git a/data_structures/Trie/Trie.py b/data_structures/Trie/Trie.py deleted file mode 100644 index 7c886144d1f4..000000000000 --- a/data_structures/Trie/Trie.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -A Trie/Prefix Tree is a kind of search tree used to provide quick lookup -of words/patterns in a set of words. A basic Trie however has O(n^2) space complexity -making it impractical in practice. It however provides O(max(search_string, length of longest word)) lookup -time making it an optimal approach when space is not an issue. - -""" - - -class TrieNode: - def __init__(self): - self.nodes = dict() # Mapping from char to TrieNode - self.is_leaf = False - - def insert_many(self, words: [str]): - """ - Inserts a list of words into the Trie - :param words: list of string words - :return: None - """ - for word in words: - self.insert(word) - - def insert(self, word: str): - """ - Inserts a word into the Trie - :param word: word to be inserted - :return: None - """ - curr = self - for char in word: - if char not in curr.nodes: - curr.nodes[char] = TrieNode() - curr = curr.nodes[char] - curr.is_leaf = True - - def find(self, word: str) -> bool: - """ - Tries to find word in a Trie - :param word: word to look for - :return: Returns True if word is found, False otherwise - """ - curr = self - for char in word: - if char not in curr.nodes: - return False - curr = curr.nodes[char] - return curr.is_leaf - - -def print_words(node: TrieNode, word: str): - """ - Prints all the words in a Trie - :param node: root node of Trie - :param word: Word variable should be empty at start - :return: None - """ - if node.is_leaf: - print(word, end=' ') - - for key, value in node.nodes.items(): - print_words(value, word + key) - - -def test(): - words = ['banana', 'bananas', 'bandana', 'band', 'apple', 'all', 'beast'] - root = TrieNode() - root.insert_many(words) - # print_words(root, '') - assert root.find('banana') - assert not root.find('bandanas') - assert not root.find('apps') - assert root.find('apple') - -test() diff --git a/data_structures/arrays/__init__.py b/data_structures/arrays/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/arrays/dynamic_array.py b/data_structures/arrays/dynamic_array.py new file mode 100644 index 000000000000..9724270cb277 --- /dev/null +++ b/data_structures/arrays/dynamic_array.py @@ -0,0 +1,127 @@ +class DynamicArray: + def __init__(self) -> None: + self.size = 0 # Number of elements in the array + self.capacity = 1 # Initial capacity of the array + self.array = [None] * self.capacity # Create an array with initial capacity + + def append(self, item: int) -> None: + """ + The function adds an item to the end of the dynamic array. + + Runtime : O(1) amortized + Space: O(1) amortized + + >>> arr = DynamicArray() + >>> arr.append(1) + >>> arr.append(2) + >>> arr.append(3) + >>> arr.array[:arr.size] # Display only the filled part of the array + [1, 2, 3] + >>> arr.append(4) + >>> arr.array[:arr.size] + [1, 2, 3, 4] + >>> arr.append(5) + >>> arr.array[:arr.size] + [1, 2, 3, 4, 5] + """ + if self.size == self.capacity: + self._resize(2 * self.capacity) # Double the capacity + + self.array[self.size] = item + self.size += 1 + + def _resize(self, new_capacity: int) -> None: + """ + Resizes the array to the new capacity. + + Runtime : O(n) + Space: O(n) + + >>> arr = DynamicArray() + >>> arr.append(1) + >>> arr.append(2) + >>> arr._resize(10) + >>> arr.capacity + 10 + >>> arr.array[:arr.size] + [1, 2] + """ + new_array = [None] * new_capacity + for i in range(self.size): + new_array[i] = self.array[i] + self.array = new_array + self.capacity = new_capacity + + def get(self, index: int) -> int: + """ + The function returns the item at the specified index. + + Runtime : O(1) + Space: O(1) + + >>> arr = DynamicArray() + >>> arr.append(1) + >>> arr.append(2) + >>> arr.get(0) + 1 + >>> arr.get(1) + 2 + >>> arr.get(2) + Traceback (most recent call last): + ... + IndexError: index out of range + >>> arr.get(-1) + Traceback (most recent call last): + ... + IndexError: index out of range + """ + if index < 0 or index >= self.size: + raise IndexError("index out of range") + return self.array[index] + + def __setitem__(self, index: int, value: int) -> None: + if index < 0 or index >= self.size: + raise IndexError("index out of range") + self.array[index] = value + + def __len__(self) -> int: + """ + Returns the number of elements in the dynamic array. + + Runtime : O(1) + Space: O(1) + + >>> arr = DynamicArray() + >>> arr.append(1) + >>> len(arr) + 1 + >>> arr.append(2) + >>> len(arr) + 2 + >>> arr.append(3) + >>> len(arr) + 3 + """ + return self.size + + def __str__(self) -> str: + """ + Returns a string representation of the dynamic array. + + >>> arr = DynamicArray() + >>> arr.append(1) + >>> arr.append(2) + >>> arr.append(3) + >>> str(arr) + '[1, 2, 3]' + >>> arr.append(4) + >>> str(arr) + '[1, 2, 3, 4]' + """ + return "[" + ", ".join(str(self.array[i]) for i in range(self.size)) + "]" + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/arrays/equilibrium_index_in_array.py b/data_structures/arrays/equilibrium_index_in_array.py new file mode 100644 index 000000000000..dcb39959f8a7 --- /dev/null +++ b/data_structures/arrays/equilibrium_index_in_array.py @@ -0,0 +1,66 @@ +""" +Find the Equilibrium Index of an Array. + +Reference: +https://www.geeksforgeeks.org/equilibrium-index-of-an-array + +Python doctest can be run with: + +python -m doctest -v equilibrium_index_in_array.py + +Given an array arr of size n, return an equilibrium index +if one exists; otherwise return -1. + +An equilibrium index is an index where the sum of all +elements to the left equals the sum of all elements +to the right. +""" + + +def equilibrium_index(arr: list[int]) -> int: + """ + Find the first equilibrium index of an array. + Args: + arr: The input array of integers. + Returns: + The first equilibrium index, or -1 if none exists. + Examples: + >>> equilibrium_index([]) + -1 + >>> equilibrium_index([5]) + 0 + >>> equilibrium_index([-7, 1, 5, 2, -4, 3, 0]) + 3 + >>> equilibrium_index([2, 4, 6, 8, 10, 3]) + -1 + >>> equilibrium_index([1, 2, 3, 4, 5]) + -1 + >>> equilibrium_index([1, 1, 1, 1, 1]) + 2 + >>> equilibrium_index([0, 0, 0]) + 0 + >>> equilibrium_index([-1, -1, -1]) + 1 + >>> equilibrium_index([1, -1, 0]) + 2 + + Time Complexity: + O(n), where n is the length of the array. + + Space Complexity: + O(1), using only constant extra space. + """ + total_sum = sum(arr) + left_sum = 0 + for i, value in enumerate(arr): + total_sum -= value + if left_sum == total_sum: + return i + left_sum += value + return -1 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/arrays/find_triplets_with_0_sum.py b/data_structures/arrays/find_triplets_with_0_sum.py new file mode 100644 index 000000000000..b436d51fcbf0 --- /dev/null +++ b/data_structures/arrays/find_triplets_with_0_sum.py @@ -0,0 +1,139 @@ +from itertools import combinations + + +def find_triplets_with_0_sum(nums: list[int]) -> list[list[int]]: + """ + Given a list of integers, return elements a, b, c such that a + b + c = 0. + Args: + nums: list of integers + Returns: + list of lists of integers where sum(each_list) == 0 + Examples: + >>> find_triplets_with_0_sum([-1, 0, 1, 2, -1, -4]) + [[-1, -1, 2], [-1, 0, 1]] + >>> find_triplets_with_0_sum([]) + [] + >>> find_triplets_with_0_sum([0, 0, 0]) + [[0, 0, 0]] + >>> find_triplets_with_0_sum([1, 2, 3, 0, -1, -2, -3]) + [[-3, 0, 3], [-3, 1, 2], [-2, -1, 3], [-2, 0, 2], [-1, 0, 1]] + """ + return [ + list(x) + for x in sorted({abc for abc in combinations(sorted(nums), 3) if not sum(abc)}) + ] + + +def find_triplets_with_0_sum_hashing(arr: list[int]) -> list[list[int]]: + """ + Function for finding the triplets with a given sum in the array using hashing. + + Given a list of integers, return elements a, b, c such that a + b + c = 0. + + Args: + nums: list of integers + Returns: + list of lists of integers where sum(each_list) == 0 + Examples: + >>> find_triplets_with_0_sum_hashing([-1, 0, 1, 2, -1, -4]) + [[-1, 0, 1], [-1, -1, 2]] + >>> find_triplets_with_0_sum_hashing([]) + [] + >>> find_triplets_with_0_sum_hashing([0, 0, 0]) + [[0, 0, 0]] + >>> find_triplets_with_0_sum_hashing([1, 2, 3, 0, -1, -2, -3]) + [[-1, 0, 1], [-3, 1, 2], [-2, 0, 2], [-2, -1, 3], [-3, 0, 3]] + + Time complexity: O(N^2) + Auxiliary Space: O(N) + + """ + target_sum = 0 + + # Initialize the final output array with blank. + output_arr = [] + + # Set the initial element as arr[i]. + for index, item in enumerate(arr[:-2]): + # to store second elements that can complement the final sum. + set_initialize = set() + + # current sum needed for reaching the target sum + current_sum = target_sum - item + + # Traverse the subarray arr[i+1:]. + for other_item in arr[index + 1 :]: + # required value for the second element + required_value = current_sum - other_item + + # Verify if the desired value exists in the set. + if required_value in set_initialize: + # finding triplet elements combination. + combination_array = sorted([item, other_item, required_value]) + if combination_array not in output_arr: + output_arr.append(combination_array) + + # Include the current element in the set + # for subsequent complement verification. + set_initialize.add(other_item) + + # Return all the triplet combinations. + return output_arr + + +def find_triplets_with_0_sum_two_pointers(nums: list[int]) -> list[list[int]]: + """ + Finds all unique triplets in the array which gives the sum of zero + using the two-pointer technique. + + Args: + nums: list of integers + Returns: + list of lists of integers where sum(each_list) == 0 + + Examples: + >>> find_triplets_with_0_sum_two_pointers([-1, 0, 1, 2, -1, -4]) + [[-1, -1, 2], [-1, 0, 1]] + >>> find_triplets_with_0_sum_two_pointers([]) + [] + >>> find_triplets_with_0_sum_two_pointers([0, 0, 0, 0]) + [[0, 0, 0]] + + Time Complexity: O(N^2) + Auxiliary Space: O(1) (excluding output) + """ + nums.sort() + result = [] + n = len(nums) + + for i in range(n - 2): + if i > 0 and nums[i] == nums[i - 1]: + continue + + left, right = i + 1, n - 1 + + while left < right: + total = nums[i] + nums[left] + nums[right] + + if total == 0: + result.append([nums[i], nums[left], nums[right]]) + left += 1 + right -= 1 + + while left < right and nums[left] == nums[left - 1]: + left += 1 + while left < right and nums[right] == nums[right + 1]: + right -= 1 + + elif total < 0: + left += 1 + else: + right -= 1 + + return result + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/data_structures/arrays/gas_station.py b/data_structures/arrays/gas_station.py new file mode 100644 index 000000000000..28eb88968fc6 --- /dev/null +++ b/data_structures/arrays/gas_station.py @@ -0,0 +1,50 @@ +from typing import NamedTuple + + +class GasStation(NamedTuple): + gas: int # Amount of gas available at this gas station + cost: int # Cost of gas required to drive to the next station + + +def can_complete_circuit(gas_stations: list[GasStation]) -> int: + """ + Finds the starting station index to complete the circuit, + or returns -1 if not possible. + Args: + gas_stations (List[GasStation]): List of gas stations with gas and cost. + Returns: + The index of the starting station, or -1 if no solution exists. + Examples: + >>> GS = GasStation + >>> test_stations = ( + ... [GS(1, 3), GS(2, 4), GS(3, 5), GS(4, 1), GS(5, 2)], + ... [GS(2, 3), GS(3, 4), GS(4, 3)], + ... [GS(5, 4), GS(1, 4), GS(2, 1), GS(3, 5), GS(4, 1)] + ... ) + >>> can_complete_circuit(test_stations[0]) + 3 + >>> can_complete_circuit(test_stations[1]) + -1 + >>> can_complete_circuit(test_stations[2]) + 4 + """ + total_gas = sum(station.gas - station.cost for station in gas_stations) + current_gas: int = 0 + start_station = 0 + for i, gas_station in enumerate(gas_stations): + needed_gas = gas_station.gas - gas_station.cost + total_gas += needed_gas + current_gas += needed_gas + if current_gas < 0: + start_station = i + 1 + current_gas = 0 + if total_gas < 0: + return -1 + return start_station + + +# Example usage with doctests +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/arrays/index_2d_array_in_1d.py b/data_structures/arrays/index_2d_array_in_1d.py new file mode 100644 index 000000000000..27a9fa5f9121 --- /dev/null +++ b/data_structures/arrays/index_2d_array_in_1d.py @@ -0,0 +1,105 @@ +""" +Retrieves the value of an 0-indexed 1D index from a 2D array. +There are two ways to retrieve value(s): + +1. Index2DArrayIterator(matrix) -> Iterator[int] +This iterator allows you to iterate through a 2D array by passing in the matrix and +calling next(your_iterator). You can also use the iterator in a loop. +Examples: +list(Index2DArrayIterator(matrix)) +set(Index2DArrayIterator(matrix)) +tuple(Index2DArrayIterator(matrix)) +sum(Index2DArrayIterator(matrix)) +-5 in Index2DArrayIterator(matrix) + +2. index_2d_array_in_1d(array: list[int], index: int) -> int +This function allows you to provide a 2D array and a 0-indexed 1D integer index, +and retrieves the integer value at that index. + +Python doctests can be run using this command: +python3 -m doctest -v index_2d_array_in_1d.py +""" + +from collections.abc import Iterator +from dataclasses import dataclass + + +@dataclass +class Index2DArrayIterator: + matrix: list[list[int]] + + def __iter__(self) -> Iterator[int]: + """ + >>> tuple(Index2DArrayIterator([[5], [-523], [-1], [34], [0]])) + (5, -523, -1, 34, 0) + >>> tuple(Index2DArrayIterator([[5, -523, -1], [34, 0]])) + (5, -523, -1, 34, 0) + >>> tuple(Index2DArrayIterator([[5, -523, -1, 34, 0]])) + (5, -523, -1, 34, 0) + >>> t = Index2DArrayIterator([[5, 2, 25], [23, 14, 5], [324, -1, 0]]) + >>> tuple(t) + (5, 2, 25, 23, 14, 5, 324, -1, 0) + >>> list(t) + [5, 2, 25, 23, 14, 5, 324, -1, 0] + >>> sorted(t) + [-1, 0, 2, 5, 5, 14, 23, 25, 324] + >>> tuple(t)[3] + 23 + >>> sum(t) + 397 + >>> -1 in t + True + >>> t = iter(Index2DArrayIterator([[5], [-523], [-1], [34], [0]])) + >>> next(t) + 5 + >>> next(t) + -523 + """ + for row in self.matrix: + yield from row + + +def index_2d_array_in_1d(array: list[list[int]], index: int) -> int: + """ + Retrieves the value of the one-dimensional index from a two-dimensional array. + + Args: + array: A 2D array of integers where all rows are the same size and all + columns are the same size. + index: A 1D index. + + Returns: + int: The 0-indexed value of the 1D index in the array. + + Examples: + >>> index_2d_array_in_1d([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]], 5) + 5 + >>> index_2d_array_in_1d([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]], -1) + Traceback (most recent call last): + ... + ValueError: index out of range + >>> index_2d_array_in_1d([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]], 12) + Traceback (most recent call last): + ... + ValueError: index out of range + >>> index_2d_array_in_1d([[]], 0) + Traceback (most recent call last): + ... + ValueError: no items in array + """ + rows = len(array) + cols = len(array[0]) + + if rows == 0 or cols == 0: + raise ValueError("no items in array") + + if index < 0 or index >= rows * cols: + raise ValueError("index out of range") + + return array[index // cols][index % cols] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/arrays/kth_largest_element.py b/data_structures/arrays/kth_largest_element.py new file mode 100644 index 000000000000..f25cc68e9b72 --- /dev/null +++ b/data_structures/arrays/kth_largest_element.py @@ -0,0 +1,117 @@ +""" +Given an array of integers and an integer k, find the kth largest element in the array. + +https://stackoverflow.com/questions/251781 +""" + + +def partition(arr: list[int], low: int, high: int) -> int: + """ + Partitions list based on the pivot element. + + This function rearranges the elements in the input list 'elements' such that + all elements greater than or equal to the chosen pivot are on the right side + of the pivot, and all elements smaller than the pivot are on the left side. + + Args: + arr: The list to be partitioned + low: The lower index of the list + high: The higher index of the list + + Returns: + int: The index of pivot element after partitioning + + Examples: + >>> partition([3, 1, 4, 5, 9, 2, 6, 5, 3, 5], 0, 9) + 4 + >>> partition([7, 1, 4, 5, 9, 2, 6, 5, 8], 0, 8) + 1 + >>> partition(['apple', 'cherry', 'date', 'banana'], 0, 3) + 2 + >>> partition([3.1, 1.2, 5.6, 4.7], 0, 3) + 1 + """ + pivot = arr[high] + i = low - 1 + for j in range(low, high): + if arr[j] >= pivot: + i += 1 + arr[i], arr[j] = arr[j], arr[i] + arr[i + 1], arr[high] = arr[high], arr[i + 1] + return i + 1 + + +def kth_largest_element(arr: list[int], position: int) -> int: + """ + Finds the kth largest element in a list. + Should deliver similar results to: + ```python + def kth_largest_element(arr, position): + return sorted(arr)[-position] + ``` + + Args: + nums: The list of numbers. + k: The position of the desired kth largest element. + + Returns: + int: The kth largest element. + + Examples: + >>> kth_largest_element([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5], 3) + 5 + >>> kth_largest_element([2, 5, 6, 1, 9, 3, 8, 4, 7, 3, 5], 1) + 9 + >>> kth_largest_element([2, 5, 6, 1, 9, 3, 8, 4, 7, 3, 5], -2) + Traceback (most recent call last): + ... + ValueError: Invalid value of 'position' + >>> kth_largest_element([9, 1, 3, 6, 7, 9, 8, 4, 2, 4, 9], 110) + Traceback (most recent call last): + ... + ValueError: Invalid value of 'position' + >>> kth_largest_element([1, 2, 4, 3, 5, 9, 7, 6, 5, 9, 3], 0) + Traceback (most recent call last): + ... + ValueError: Invalid value of 'position' + >>> kth_largest_element(['apple', 'cherry', 'date', 'banana'], 2) + 'cherry' + >>> kth_largest_element([3.1, 1.2, 5.6, 4.7,7.9,5,0], 2) + 5.6 + >>> kth_largest_element([-2, -5, -4, -1], 1) + -1 + >>> kth_largest_element([], 1) + -1 + >>> kth_largest_element([3.1, 1.2, 5.6, 4.7, 7.9, 5, 0], 1.5) + Traceback (most recent call last): + ... + ValueError: The position should be an integer + >>> kth_largest_element((4, 6, 1, 2), 4) + Traceback (most recent call last): + ... + TypeError: 'tuple' object does not support item assignment + """ + if not arr: + return -1 + if not isinstance(position, int): + raise ValueError("The position should be an integer") + if not 1 <= position <= len(arr): + raise ValueError("Invalid value of 'position'") + low, high = 0, len(arr) - 1 + while low <= high: + if low > len(arr) - 1 or high < 0: + return -1 + pivot_index = partition(arr, low, high) + if pivot_index == position - 1: + return arr[pivot_index] + elif pivot_index > position - 1: + high = pivot_index - 1 + else: + low = pivot_index + 1 + return -1 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/arrays/median_two_array.py b/data_structures/arrays/median_two_array.py new file mode 100644 index 000000000000..972b0ee44201 --- /dev/null +++ b/data_structures/arrays/median_two_array.py @@ -0,0 +1,61 @@ +""" +https://www.enjoyalgorithms.com/blog/median-of-two-sorted-arrays +""" + + +def find_median_sorted_arrays(nums1: list[int], nums2: list[int]) -> float: + """ + Find the median of two arrays. + + Args: + nums1: The first array. + nums2: The second array. + + Returns: + The median of the two arrays. + + Examples: + >>> find_median_sorted_arrays([1, 3], [2]) + 2.0 + + >>> find_median_sorted_arrays([1, 2], [3, 4]) + 2.5 + + >>> find_median_sorted_arrays([0, 0], [0, 0]) + 0.0 + + >>> find_median_sorted_arrays([], []) + Traceback (most recent call last): + ... + ValueError: Both input arrays are empty. + + >>> find_median_sorted_arrays([], [1]) + 1.0 + + >>> find_median_sorted_arrays([-1000], [1000]) + 0.0 + + >>> find_median_sorted_arrays([-1.1, -2.2], [-3.3, -4.4]) + -2.75 + """ + if not nums1 and not nums2: + raise ValueError("Both input arrays are empty.") + + # Merge the arrays into a single sorted array. + merged = sorted(nums1 + nums2) + total = len(merged) + + if total % 2 == 1: # If the total number of elements is odd + return float(merged[total // 2]) # then return the middle element + + # If the total number of elements is even, calculate + # the average of the two middle elements as the median. + middle1 = merged[total // 2 - 1] + middle2 = merged[total // 2] + return (float(middle1) + float(middle2)) / 2.0 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/arrays/merge_intervals.py b/data_structures/arrays/merge_intervals.py new file mode 100644 index 000000000000..c4c41ae1f016 --- /dev/null +++ b/data_structures/arrays/merge_intervals.py @@ -0,0 +1,64 @@ +def merge_intervals(intervals: list[list[int]]) -> list[list[int]]: + """ + Merge all overlapping intervals. + + Each interval is represented as a list of two integers [start, end]. + The function merges overlapping intervals and returns a list of + non-overlapping intervals sorted by start time. + + Parameters: + intervals (list[list[int]]): A list of intervals. + + Returns: + list[list[int]]: A list of merged non-overlapping intervals. + + Edge Cases Handled: + - Empty list: returns [] + - Single interval: returns the interval itself + - Intervals already sorted or unsorted + - Fully overlapping intervals + - Invalid intervals (e.g., [[]] or intervals not having exactly + 2 integers) raise ValueError + + Examples: + >>> merge_intervals([[1, 3], [2, 6], [8, 10], [15, 18]]) + [[1, 6], [8, 10], [15, 18]] + >>> merge_intervals([[1, 4], [4, 5]]) + [[1, 5]] + >>> merge_intervals([[6, 8], [1, 3], [2, 4]]) + [[1, 4], [6, 8]] + >>> merge_intervals([]) + [] + >>> merge_intervals([[1, 4]]) + [[1, 4]] + + Time Complexity: + O(n log n) - sorting the intervals, where n is the number of intervals. + + Space Complexity: + O(n) - storing the merged intervals. + """ + + if not intervals: + return [] + + for interval in intervals: + msg = f"Each interval must have exactly 2 integers, got {interval}" + if len(interval) != 2: + raise ValueError(msg) + + # Sort intervals based on the start time + # Sort a copy so the caller's list (and its inner lists) are never mutated + sorted_intervals = sorted(intervals, key=lambda interval: interval[0]) + + merged: list[list[int]] = [sorted_intervals[0][:]] + + for current in sorted_intervals[1:]: + last = merged[-1] + + # If current interval overlaps with the last merged interval + if current[0] <= last[1]: + last[1] = max(last[1], current[1]) + else: + merged.append(current[:]) + return merged diff --git a/data_structures/arrays/merge_sorted.py b/data_structures/arrays/merge_sorted.py new file mode 100644 index 000000000000..cef98fe3b435 --- /dev/null +++ b/data_structures/arrays/merge_sorted.py @@ -0,0 +1,76 @@ +def merge_sorted_arrays(nums1: list[int], nums2: list[int]) -> list[int]: + """ + Merge two sorted arrays into one sorted array. + + Args: + nums1: The first sorted array. + nums2: The second sorted array. + + Returns: + A single merged and sorted array. + + Examples: + >>> merge_sorted_arrays([1, 3, 5], [2, 4, 6]) + [1, 2, 3, 4, 5, 6] + + >>> merge_sorted_arrays([1, 2], []) + [1, 2] + + >>> merge_sorted_arrays([], [3, 4]) + [3, 4] + + >>> merge_sorted_arrays([], []) + [] + + >>> merge_sorted_arrays([0, 0], [0, 0]) + [0, 0, 0, 0] + + >>> merge_sorted_arrays([-5, -3, -1], [-2, -2]) + [-5, -3, -2, -2, -1] + + >>> merge_sorted_arrays(range(5), range(5)) + [0, 0, 1, 1, 2, 2, 3, 3, 4, 4] + + >>> merge_sorted_arrays([1, -1], []) + Traceback (most recent call last): + ... + ValueError: nums = [1, -1] is not sorted + + >>> merge_sorted_arrays([], [1, -1]) + Traceback (most recent call last): + ... + ValueError: nums = [1, -1] is not sorted + """ + for nums in (nums1, nums2): + if list(nums) != sorted(nums): + msg = f"{nums = } is not sorted" + raise ValueError(msg) + # If one array is empty, simply return the other. + if not nums1: + return nums2 + if not nums2: + return nums1 + + # Two-pointer approach to merge both sorted arrays. + i, j = 0, 0 + merged = [] + + while i < len(nums1) and j < len(nums2): + if nums1[i] <= nums2[j]: + merged.append(nums1[i]) + i += 1 + else: + merged.append(nums2[j]) + j += 1 + + # Append remaining elements if any. + merged.extend(nums1[i:]) + merged.extend(nums2[j:]) + + return merged + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/arrays/monotonic_array.py b/data_structures/arrays/monotonic_array.py new file mode 100644 index 000000000000..342d443a9cfc --- /dev/null +++ b/data_structures/arrays/monotonic_array.py @@ -0,0 +1,37 @@ +# https://leetcode.com/problems/monotonic-array/ +def is_monotonic(nums: list[int]) -> bool: + """ + Check if a list is monotonic. + + >>> is_monotonic([1, 2, 2, 3]) + True + >>> is_monotonic([6, 5, 4, 4]) + True + >>> is_monotonic([1, 3, 2]) + False + >>> is_monotonic([1,2,3,4,5,6,5]) + False + >>> is_monotonic([-3,-2,-1]) + True + >>> is_monotonic([-5,-6,-7]) + True + >>> is_monotonic([0,0,0]) + True + >>> is_monotonic([-100,0,100]) + True + """ + return all(nums[i] <= nums[i + 1] for i in range(len(nums) - 1)) or all( + nums[i] >= nums[i + 1] for i in range(len(nums) - 1) + ) + + +# Test the function with your examples +if __name__ == "__main__": + # Test the function with your examples + print(is_monotonic([1, 2, 2, 3])) # Output: True + print(is_monotonic([6, 5, 4, 4])) # Output: True + print(is_monotonic([1, 3, 2])) # Output: False + + import doctest + + doctest.testmod() diff --git a/data_structures/arrays/pairs_with_given_sum.py b/data_structures/arrays/pairs_with_given_sum.py new file mode 100644 index 000000000000..b27bd78e1e0f --- /dev/null +++ b/data_structures/arrays/pairs_with_given_sum.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +""" +Given an array of integers and an integer req_sum, find the number of pairs of array +elements whose sum is equal to req_sum. + +https://practice.geeksforgeeks.org/problems/count-pairs-with-given-sum5022/0 +""" + +from itertools import combinations + + +def pairs_with_sum(arr: list, req_sum: int) -> int: + """ + Return the no. of pairs with sum "sum" + >>> pairs_with_sum([1, 5, 7, 1], 6) + 2 + >>> pairs_with_sum([1, 1, 1, 1, 1, 1, 1, 1], 2) + 28 + >>> pairs_with_sum([1, 7, 6, 2, 5, 4, 3, 1, 9, 8], 7) + 4 + """ + return len([1 for a, b in combinations(arr, 2) if a + b == req_sum]) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/data_structures/arrays/pairwise_iteration.py b/data_structures/arrays/pairwise_iteration.py new file mode 100644 index 000000000000..7342b90e9575 --- /dev/null +++ b/data_structures/arrays/pairwise_iteration.py @@ -0,0 +1,145 @@ +""" +Author : Matheus F. Vesco +Date : October 3, 2023 + +Implementation of pairwise iteration algorithms, which can be useful in +many domains. +Currently, there are two different implementations. +""" + +from collections.abc import Iterable, Iterator +from itertools import tee + + +def pairwise_iteration_tee(iterable: Iterable) -> Iterator[tuple]: + """ + Generate pairs of elements from an iterable just like: + https://docs.python.org/3/library/itertools.html#itertools.pairwise + + This function uses the `tee` function from the `itertools` module to + create two independent iterators (`a` and `b`) from the input + iterable. The `next` function is used to offset the `b` iterator by + one index, and then the two iterators are zipped together to create + pairs of elements. This implementation should work with any iterable + in Python. + + Args: + iterable (Iterable): The input iterable. + + Yields: + Iterator[Tuple]: An iterator that yields pairs of objects. + + Examples: + >>> list(pairwise_iteration_tee([1, 2, 3])) + [(1, 2), (2, 3)] + + >>> list(pairwise_iteration_tee((4, 3, 5))) + [(4, 3), (3, 5)] + + >>> list(pairwise_iteration_tee({'x':3, 'y':1, 'z':2, 'foo':4})) + [('x', 'y'), ('y', 'z'), ('z', 'foo')] + + >>> list(pairwise_iteration_tee('2345')) + [('2', '3'), ('3', '4'), ('4', '5')] + + >>> list(pairwise_iteration_tee(['ATG','GCT','TGC','TAA'])) + [('ATG', 'GCT'), ('GCT', 'TGC'), ('TGC', 'TAA')] + + >>> list(pairwise_iteration_tee(['a'])) + [] + + >>> from itertools import pairwise + >>> all(list(pairwise_iteration_tee(test)) == list(pairwise(test)) + ... for test in ( + ... [1, 2, 3], + ... (4, 3, 5), + ... {'x':3, 'y':1, 'z':2, 'foo':4}, + ... '2345', + ... ['ATG','GCT','TGC','TAA'], + ... [], + ... )) + True + """ + # Uses itertools.tee to create two independent iterators (a and b) + # from the iterable. This means we can use next() on each one + # without affecting the other, no matter the iterable type + a, b = tee(iterable) + + # Offsets the second iterator (b) by one step to create a staggered + # alignment. + # this means that (a[i],b[i]) represents the same as (a[i],a[i+1]) + next(b, None) + + # Returns a zip generator that pairs items from the two iterators in + # the format (a[i], a[i+1]). + return zip(a, b) + + +def pairwise_iteration_comprehension( + iterable: Iterable, step: int = 1 +) -> Iterator[tuple]: + """ + Generate pairs of elements from an iterable with a given step size. + + This function uses list comprehensions to get the items that are step + distance from each other and later the `iter()` conversion to create + two independent list iterators (`a` and `b`) from the input iterable. + The `next` function is used to offset the `b` iterator by one index, + and then the two iterators are zipped together to create pairs of + elements. + + Args: + iterable (Iterable): The input iterable. + step (int, optional): The step size for iterating through the + input iterable. Defaults to 1. + + Yields: + Iterator[Tuple]: An iterator that yields pairs of objects. + + Examples: + >>> list(pairwise_iteration_comprehension([0, 1, 2, 3, 4, 5, 6], step=2)) + [(0, 2), (2, 4), (4, 6)] + + >>> list(pairwise_iteration_comprehension([0, 1, 2, 3, 4, 5, 6], step=3)) + [(0, 3), (3, 6)] + + >>> list(pairwise_iteration_comprehension((0, 1, 2, 3, 4), step=2)) + [(0, 2), (2, 4)] + + >>> python_set = pairwise_iteration_comprehension( + ... {4, 3, 2, 1, 0}, step=2) + >>> list(python_set) # sets are unordered + [(0, 2), (2, 4)] + + >>> dictionary = pairwise_iteration_comprehension( + ... {'x1':4, 'y1':5, 'x2':1, 'y2':'a', 'spam':7}, step=2) + >>> list(dictionary) + [('x1', 'x2'), ('x2', 'spam')] + + >>> list(pairwise_iteration_comprehension({0, 1, 2, 3, 4, 5, 6}, step=3)) + [(0, 3), (3, 6)] + + >>> list(pairwise_iteration_comprehension(['ATG','GCT','TGC','TAA'])) + [('ATG', 'GCT'), ('GCT', 'TGC'), ('TGC', 'TAA')] + + >>> list(pairwise_iteration_comprehension(['a'], step=1)) + [] + """ + # creates a list, using list comprehensions, that only stores items + # that are n steps apart from each other. + items = [item for i, item in enumerate(iterable) if i % step == 0] + + # creates two independent list iterators (a and b) from the list + # we created earlier, using the iter() function. This means we can + # use next() on each one without affecting the other, no matter the + # iterable type + a, b = (iter(items), iter(items)) + + # Offsets the second iterator (b) by one step to create a staggered + # alignment. + # this means that (a[i],b[i]) represents the same as (a[i],a[i+1]) + next(b, None) + + # Returns a zip generator that pairs items from the two iterators in + # the format (a[i], a[i+1]). + return zip(a, b) diff --git a/data_structures/arrays/permutations.py b/data_structures/arrays/permutations.py new file mode 100644 index 000000000000..4906dd5c2ae1 --- /dev/null +++ b/data_structures/arrays/permutations.py @@ -0,0 +1,48 @@ +def permute_recursive(nums: list[int]) -> list[list[int]]: + """ + Return all permutations. + + >>> permute_recursive([1, 2, 3]) + [[3, 2, 1], [2, 3, 1], [1, 3, 2], [3, 1, 2], [2, 1, 3], [1, 2, 3]] + """ + result: list[list[int]] = [] + if len(nums) == 0: + return [[]] + for _ in range(len(nums)): + n = nums.pop(0) + permutations = permute_recursive(nums.copy()) + for perm in permutations: + perm.append(n) + result.extend(permutations) + nums.append(n) + return result + + +def permute_backtrack(nums: list[int]) -> list[list[int]]: + """ + Return all permutations of the given list. + + >>> permute_backtrack([1, 2, 3]) + [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], [3, 1, 2]] + """ + + def backtrack(start: int) -> None: + if start == len(nums) - 1: + output.append(nums[:]) + else: + for i in range(start, len(nums)): + nums[start], nums[i] = nums[i], nums[start] + backtrack(start + 1) + nums[start], nums[i] = nums[i], nums[start] # backtrack + + output: list[list[int]] = [] + backtrack(0) + return output + + +if __name__ == "__main__": + import doctest + + result = permute_backtrack([1, 2, 3]) + print(result) + doctest.testmod() diff --git a/data_structures/arrays/prefix_sum.py b/data_structures/arrays/prefix_sum.py new file mode 100644 index 000000000000..717b5f9d7e7e --- /dev/null +++ b/data_structures/arrays/prefix_sum.py @@ -0,0 +1,96 @@ +""" +Author : Alexander Pantyukhin +Date : November 3, 2022 + +Implement the class of prefix sum with useful functions based on it. + +""" + + +class PrefixSum: + def __init__(self, array: list[int]) -> None: + len_array = len(array) + self.prefix_sum = [0] * len_array + + if len_array > 0: + self.prefix_sum[0] = array[0] + + for i in range(1, len_array): + self.prefix_sum[i] = self.prefix_sum[i - 1] + array[i] + + def get_sum(self, start: int, end: int) -> int: + """ + The function returns the sum of array from the start to the end indexes. + Runtime : O(1) + Space: O(1) + + >>> PrefixSum([1,2,3]).get_sum(0, 2) + 6 + >>> PrefixSum([1,2,3]).get_sum(1, 2) + 5 + >>> PrefixSum([1,2,3]).get_sum(2, 2) + 3 + >>> PrefixSum([]).get_sum(0, 0) + Traceback (most recent call last): + ... + ValueError: The array is empty. + >>> PrefixSum([1,2,3]).get_sum(-1, 2) + Traceback (most recent call last): + ... + ValueError: Invalid range specified. + >>> PrefixSum([1,2,3]).get_sum(2, 3) + Traceback (most recent call last): + ... + ValueError: Invalid range specified. + >>> PrefixSum([1,2,3]).get_sum(2, 1) + Traceback (most recent call last): + ... + ValueError: Invalid range specified. + """ + if not self.prefix_sum: + raise ValueError("The array is empty.") + + if start < 0 or end >= len(self.prefix_sum) or start > end: + raise ValueError("Invalid range specified.") + + if start == 0: + return self.prefix_sum[end] + + return self.prefix_sum[end] - self.prefix_sum[start - 1] + + def contains_sum(self, target_sum: int) -> bool: + """ + The function returns True if array contains the target_sum, + False otherwise. + + Runtime : O(n) + Space: O(n) + + >>> PrefixSum([1,2,3]).contains_sum(6) + True + >>> PrefixSum([1,2,3]).contains_sum(5) + True + >>> PrefixSum([1,2,3]).contains_sum(3) + True + >>> PrefixSum([1,2,3]).contains_sum(4) + False + >>> PrefixSum([1,2,3]).contains_sum(7) + False + >>> PrefixSum([1,-2,3]).contains_sum(2) + True + """ + + sums = {0} + for sum_item in self.prefix_sum: + if sum_item - target_sum in sums: + return True + + sums.add(sum_item) + + return False + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/arrays/product_sum.py b/data_structures/arrays/product_sum.py new file mode 100644 index 000000000000..1d7d6ee94dbc --- /dev/null +++ b/data_structures/arrays/product_sum.py @@ -0,0 +1,222 @@ +""" +Calculate the Product Sum from a Special Array. +reference: https://dev.to/sfrasica/algorithms-product-sum-from-an-array-dc6 + +Python doctests can be run with the following command: +python -m doctest -v product_sum.py + +Calculate the product sum of a "special" array which can contain integers or nested +arrays. The product sum is obtained by adding all elements and multiplying by their +respective depths. + +For example, in the array [x, y], the product sum is (x + y). In the array [x, [y, z]], +the product sum is x + 2 * (y + z). In the array [x, [y, [z]]], +the product sum is x + 2 * (y + 3z). + +Example Input: +[5, 2, [-7, 1], 3, [6, [-13, 8], 4]] +Output: -12 + +""" + +from timeit import timeit + + +def product_sum(arr: list[int | list], depth: int) -> int: + """ + Recursively calculates the product sum of an array. + + The product sum of an array is defined as the sum of its elements multiplied by + their respective depths. If an element is a list, its product sum is calculated + recursively by multiplying the sum of its elements with its depth plus one. + + Args: + arr: The array of integers and nested lists. + depth: The current depth level. + + Returns: + int: The product sum of the array. + + Examples: + >>> product_sum([1, 2, 3], 1) + 6 + >>> product_sum([-1, 2, [-3, 4]], 2) + 8 + >>> product_sum([1, 2, 3], -1) + -6 + >>> product_sum([1, 2, 3], 0) + 0 + >>> product_sum([1, 2, 3], 7) + 42 + >>> product_sum((1, 2, 3), 7) + 42 + >>> product_sum({1, 2, 3}, 7) + 42 + >>> product_sum([1, -1], 1) + 0 + >>> product_sum([1, -2], 1) + -1 + >>> product_sum([-3.5, [1, [0.5]]], 1) + 1.5 + + """ + total_sum = 0 + for ele in arr: + total_sum += product_sum(ele, depth + 1) if isinstance(ele, list) else ele + return total_sum * depth + + +def product_sum_array(array: list[int | list]) -> int: + """ + Calculates the product sum of an array. + + Args: + array (List[Union[int, List]]): The array of integers and nested lists. + + Returns: + int: The product sum of the array. + + Examples: + >>> product_sum_array([1, 2, 3]) + 6 + >>> product_sum_array([1, [2, 3]]) + 11 + >>> product_sum_array([1, [2, [3, 4]]]) + 47 + >>> product_sum_array([0]) + 0 + >>> product_sum_array([-3.5, [1, [0.5]]]) + 1.5 + >>> product_sum_array([1, -2]) + -1 + + """ + return product_sum(array, 1) + + +def product_sum_iterative(arr: list[int | list]) -> int: + """ + It Calculates the product sum of an array using iterative approach. + It's similar to BFS algorithm (Breadth first search algorithm). + It's won't run into stack overflow as compared to recursion approach + + Args: + array(List[Union[int, List]]): The array of integers/lists + + Returns: + int: The product sum of the array. + + Logic : + 1. Initialize a queue which stores the list, current + depth and it's multiplication factor + eg. queue -> [(arr, depth, multiplication_factor)] + + 2. Loop until queue is empty + 1. Take front item from Queue and pop it + 2. Iterate on front element + If current element is nested list + - then add that into queue with updated depth + and multiplication factor + Else if current element is not nested + - then update product sum variable by multiplying + current element with multiplicaton factor + + Algorithm flow example -> + Input list - [5, 2, [-7, 1], 3, [6, [-13, 8], 4]] + + Initialize queue - [([5, 2, [-7, 1], 3, [6, [-13, 8], 4]], 1, 1)] + + Step 0 + Queue - [([5, 2, [-7, 1], 3, [6, [-13, 8], 4]], 1, 1)] + Queue front item - + List - [5, 2, [-7, 1], 3, [6, [-13, 8], 4]] + depth - 1 + multiplication factor - 1 + + product sum = 0 (previous) + 5 * 1 + 2 * 1 + 3 * 1 = 10 + ------------------------------------------------------- + Step 1 + Queue - [([-7, 1], 2, 2), ([6, [-13, 8], 4], 2, 2)] + Queue front item - + List - [-7, 1] + depth - 2 + multiplication factor - 2 + + product sum = 10 (previous) + (-7) * 2 + 1 * 2 = -2 + ------------------------------------------------------- + Step 2 + Queue - [([6, [-13, 8], 4], 2, 2)] + Queue front item - + List - [6, [-13, 8], 4] + depth - 2 + multiplication factor - 2 + + product sum = -2 (previous) + 6 * 2 + 4 * 2 = 18 + ------------------------------------------------------- + Step 3 + Queue - [([-13, 8], 3, 6)] + Queue front item - + List - [-13, 8] + depth - 3 + multiplication factor - 6 + + product sum = 18 (previous) + (-13) * 6 + 8 * 6 = -12 + ------------------------------------------------------- + + Examples: + >>> product_sum_array([1, 2, 3]) + 6 + >>> product_sum_array([1, [2, 3]]) + 11 + >>> product_sum_array([1, [2, [3, 4]]]) + 47 + >>> product_sum_array([0]) + 0 + >>> product_sum_array([-3.5, [1, [0.5]]]) + 1.5 + >>> product_sum_array([1, -2]) + -1 + """ + + # Initialize queue with depth and multiplication factor + queue = [(arr, 1, 1)] + + product_sum = 0 + + while queue: + queue_front_list, depth, multiplication_factor = queue.pop(0) + + for element in queue_front_list: + if isinstance(element, list): + queue.append((element, depth + 1, multiplication_factor * (depth + 1))) + else: + product_sum += element * multiplication_factor + + return product_sum + + +def benchmark() -> None: + """ + Benchmark code comparing different version. + """ + + setup = "from __main__ import product_sum_array, product_sum_iterative" + + print( + timeit("product_sum_array([5, 2, [-7, 1], 3, [6, [-13, 8], 4]])", setup=setup) + ) + # 1.1448657000437379 + + print( + timeit( + "product_sum_iterative([5, 2, [-7, 1], 3, [6, [-13, 8], 4]])", setup=setup + ) + ) + # 1.6824490998405963 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + benchmark() diff --git a/data_structures/arrays/reverse_array.py b/data_structures/arrays/reverse_array.py new file mode 100644 index 000000000000..8834d5007461 --- /dev/null +++ b/data_structures/arrays/reverse_array.py @@ -0,0 +1,72 @@ +""" +In-place array reversal that also returns the reversed list to the caller. +This algorithm reverses the elements of a list without using extra space. +""" + +from typing import Any + + +def reverse_array(arr: list[Any]) -> list[Any]: + """ + Reverses a list in-place. + + This function takes a list and reverses its elements using a two-pointer + approach. The left pointer starts at the beginning of the list, and the + right pointer starts at the end. The elements at these two pointers are + swapped, and the pointers move towards the center until they meet or cross. + + Args: + arr: The list to be reversed. + + Returns: + The same list, now reversed. This allows for method chaining. + + Doctests: + >>> reverse_array([1, 2, 3, 4, 5]) + [5, 4, 3, 2, 1] + >>> reverse_array(['a', 'b', 'c', 'd']) + ['d', 'c', 'b', 'a'] + >>> reverse_array([10.5, 20.2, 30.8]) + [30.8, 20.2, 10.5] + >>> reverse_array(["apple", "banana", "cherry"]) + ['cherry', 'banana', 'apple'] + >>> reverse_array([1]) + [1] + >>> reverse_array([]) + [] + >>> reverse_array(list(range(5))) + [4, 3, 2, 1, 0] + >>> reverse_array(tuple(range(5))) + Traceback (most recent call last): + ... + TypeError: 'tuple' object does not support item assignment + >>> reverse_array(range(5)) + Traceback (most recent call last): + ... + TypeError: 'range' object does not support item assignment + """ + left = 0 + right = len(arr) - 1 + + while left < right: + # Swap the elements at the left and right pointers + arr[left], arr[right] = arr[right], arr[left] + # Move the pointers towards the center + left += 1 + right -= 1 + return arr + + +if __name__ == "__main__": + # The doctest module runs the tests embedded in the function's docstring. + # To run the tests, execute this script from the command line: + # python -m doctest -v reverse_array.py + import doctest + + doctest.testmod() + + # Example usage: + print("\n--- Example Usage ---") + sample_array = [10, 20, 30, 40, 50, 60] + print(f"{sample_array = }") + print(f"{reverse_array(sample_array) = }") diff --git a/data_structures/arrays/rotate_array.py b/data_structures/arrays/rotate_array.py new file mode 100644 index 000000000000..d5ce4b4078b3 --- /dev/null +++ b/data_structures/arrays/rotate_array.py @@ -0,0 +1,80 @@ +def rotate_array(arr: list[int], steps: int) -> list[int]: + """ + Rotates a list to the right by steps positions. + + Parameters: + arr (List[int]): The list of integers to rotate. + steps (int): Number of positions to rotate. Can be negative for left rotation. + + Returns: + List[int]: Rotated list. + + Examples: + >>> rotate_array([1, 2, 3, 4, 5], 2) + [4, 5, 1, 2, 3] + >>> rotate_array([1, 2, 3, 4, 5], -2) + [3, 4, 5, 1, 2] + >>> rotate_array([1, 2, 3, 4, 5], 7) + [4, 5, 1, 2, 3] + >>> rotate_array([], 3) + [] + """ + + n = len(arr) + if n == 0: + return arr + + steps = steps % n + + if steps < 0: + steps += n + + def reverse(start: int, end: int) -> None: + """ + Reverses a portion of the list in place from index start to end. + + Parameters: + start (int): Starting index of the portion to reverse. + end (int): Ending index of the portion to reverse. + + Returns: + None + + Examples: + >>> example = [1, 2, 3, 4, 5] + >>> def reverse_test(arr, start, end): + ... while start < end: + ... arr[start], arr[end] = arr[end], arr[start] + ... start += 1 + ... end -= 1 + >>> reverse_test(example, 0, 2) + >>> example + [3, 2, 1, 4, 5] + >>> reverse_test(example, 2, 4) + >>> example + [3, 2, 5, 4, 1] + """ + + while start < end: + arr[start], arr[end] = arr[end], arr[start] + start += 1 + end -= 1 + + reverse(0, n - 1) + reverse(0, steps - 1) + reverse(steps, n - 1) + + return arr + + +if __name__ == "__main__": + examples = [ + ([1, 2, 3, 4, 5], 2), + ([1, 2, 3, 4, 5], -2), + ([1, 2, 3, 4, 5], 7), + ([], 3), + ] + + for arr, steps in examples: + rotated = rotate_array(arr.copy(), steps) + print(f"Rotate {arr} by {steps}: {rotated}") diff --git a/data_structures/arrays/set_matrix_zeroes.py b/data_structures/arrays/set_matrix_zeroes.py new file mode 100644 index 000000000000..905d03b2daf5 --- /dev/null +++ b/data_structures/arrays/set_matrix_zeroes.py @@ -0,0 +1,67 @@ +""" +Set Matrix Zeroes Algorithm +--------------------------- +If an element in an m x n matrix is 0, set its entire row and column to 0. + +Explanation: +We use the first row and first column as markers to track which rows and +columns should be zeroed, avoiding extra space usage (O(1) space complexity). + +References: +https://leetcode.com/problems/set-matrix-zeroes/ + +Doctest: +>>> matrix = [ +... [1, 1, 1], +... [1, 0, 1], +... [1, 1, 1] +... ] +>>> set_matrix_zeroes(matrix) +>>> matrix +[[1, 0, 1], [0, 0, 0], [1, 0, 1]] +>>> matrix = [[0, 1, 2, 0], [3, 4, 5, 2], [1, 3, 1, 5]] +>>> set_matrix_zeroes(matrix) +>>> matrix +[[0, 0, 0, 0], [0, 4, 5, 0], [0, 3, 1, 0]] +""" + + +def set_matrix_zeroes(matrix: list[list[int]]) -> None: + """ + Modify the matrix in-place such that if an element is 0, + its entire row and column are set to 0. + + :param matrix: 2D list of integers + :return: None (modifies matrix in-place) + + Time Complexity: O(m * n) + Space Complexity: O(1) + """ + rows = len(matrix) + cols = len(matrix[0]) + col0 = 1 + + # Step 1: Mark rows and columns that need to be zeroed + for i in range(rows): + if matrix[i][0] == 0: + col0 = 0 + for j in range(1, cols): + if matrix[i][j] == 0: + matrix[i][0] = 0 + matrix[0][j] = 0 + + # Step 2: Update the inner matrix cells + for i in range(1, rows): + for j in range(1, cols): + if matrix[i][0] == 0 or matrix[0][j] == 0: + matrix[i][j] = 0 + + # Step 3: Handle the first row + if matrix[0][0] == 0: + for j in range(cols): + matrix[0][j] = 0 + + # Step 4: Handle the first column + if col0 == 0: + for i in range(rows): + matrix[i][0] = 0 diff --git a/data_structures/arrays/sparse_table.py b/data_structures/arrays/sparse_table.py new file mode 100644 index 000000000000..4606fe908607 --- /dev/null +++ b/data_structures/arrays/sparse_table.py @@ -0,0 +1,95 @@ +""" +Sparse table is a data structure that allows answering range queries on +a static number list, i.e. the elements do not change throughout all the queries. + +The implementation below will solve the problem of Range Minimum Query: +Finding the minimum value of a subset [L..R] of a static number list. + +Overall time complexity: O(nlogn) +Overall space complexity: O(nlogn) + +Wikipedia link: https://en.wikipedia.org/wiki/Range_minimum_query +""" + +from math import log2 + + +def build_sparse_table(number_list: list[int]) -> list[list[int]]: + """ + Precompute range minimum queries with power of two length and store the precomputed + values in a table. + + >>> build_sparse_table([8, 1, 0, 3, 4, 9, 3]) + [[8, 1, 0, 3, 4, 9, 3], [1, 0, 0, 3, 4, 3, 0], [0, 0, 0, 3, 0, 0, 0]] + >>> build_sparse_table([3, 1, 9]) + [[3, 1, 9], [1, 1, 0]] + >>> build_sparse_table([]) + Traceback (most recent call last): + ... + ValueError: empty number list not allowed + """ + if not number_list: + raise ValueError("empty number list not allowed") + + length = len(number_list) + # Initialise sparse_table -- sparse_table[j][i] represents the minimum value of the + # subset of length (2 ** j) of number_list, starting from index i. + + # smallest power of 2 subset length that fully covers number_list + row = int(log2(length)) + 1 + sparse_table = [[0 for i in range(length)] for j in range(row)] + + # minimum of subset of length 1 is that value itself + for i, value in enumerate(number_list): + sparse_table[0][i] = value + j = 1 + + # compute the minimum value for all intervals with size (2 ** j) + while (1 << j) <= length: + i = 0 + # while subset starting from i still have at least (2 ** j) elements + while (i + (1 << j) - 1) < length: + # split range [i, i + 2 ** j] and find minimum of 2 halves + sparse_table[j][i] = min( + sparse_table[j - 1][i + (1 << (j - 1))], sparse_table[j - 1][i] + ) + i += 1 + j += 1 + return sparse_table + + +def query(sparse_table: list[list[int]], left_bound: int, right_bound: int) -> int: + """ + >>> query(build_sparse_table([8, 1, 0, 3, 4, 9, 3]), 0, 4) + 0 + >>> query(build_sparse_table([8, 1, 0, 3, 4, 9, 3]), 4, 6) + 3 + >>> query(build_sparse_table([3, 1, 9]), 2, 2) + 9 + >>> query(build_sparse_table([3, 1, 9]), 0, 1) + 1 + >>> query(build_sparse_table([8, 1, 0, 3, 4, 9, 3]), 0, 11) + Traceback (most recent call last): + ... + IndexError: list index out of range + >>> query(build_sparse_table([]), 0, 0) + Traceback (most recent call last): + ... + ValueError: empty number list not allowed + """ + if left_bound < 0 or right_bound >= len(sparse_table[0]): + raise IndexError("list index out of range") + + # highest subset length of power of 2 that is within range [left_bound, right_bound] + j = int(log2(right_bound - left_bound + 1)) + + # minimum of 2 overlapping smaller subsets: + # [left_bound, left_bound + 2 ** j - 1] and [right_bound - 2 ** j + 1, right_bound] + return min(sparse_table[j][right_bound - (1 << j) + 1], sparse_table[j][left_bound]) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + print(f"{query(build_sparse_table([3, 1, 9]), 2, 2) = }") diff --git a/data_structures/arrays/sudoku_solver.py b/data_structures/arrays/sudoku_solver.py new file mode 100644 index 000000000000..05e2d9f7e964 --- /dev/null +++ b/data_structures/arrays/sudoku_solver.py @@ -0,0 +1,259 @@ +""" +Please do not modify this file! It is published at https://norvig.com/sudoku.html with +only minimal changes to work with modern versions of Python. If you have improvements, +please make them in a separate file. +""" + +import random +import time + + +def cross(items_a, items_b): + """ + Cross product of elements in A and elements in B. + + >>> cross('AB', '12') + ['A1', 'A2', 'B1', 'B2'] + >>> cross('ABC', '123') + ['A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3'] + >>> cross('ABC', '1234') + ['A1', 'A2', 'A3', 'A4', 'B1', 'B2', 'B3', 'B4', 'C1', 'C2', 'C3', 'C4'] + >>> cross('', '12') + [] + >>> cross('A', '') + [] + >>> cross('', '') + [] + """ + return [a + b for a in items_a for b in items_b] + + +digits = "123456789" +rows = "ABCDEFGHI" +cols = digits +squares = cross(rows, cols) +unitlist = ( + [cross(rows, c) for c in cols] + + [cross(r, cols) for r in rows] + + [cross(rs, cs) for rs in ("ABC", "DEF", "GHI") for cs in ("123", "456", "789")] +) +units = {s: [u for u in unitlist if s in u] for s in squares} +peers = {s: {x for u in units[s] for x in u} - {s} for s in squares} + + +def test() -> None: + """A set of unit tests.""" + assert len(squares) == 81 + assert len(unitlist) == 27 + assert all(len(units[s]) == 3 for s in squares) + assert all(len(peers[s]) == 20 for s in squares) + assert units["C2"] == [ + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2", "I2"], + ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9"], + ["A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", "C3"], + ] + # fmt: off + assert peers["C2"] == { + "A2", "B2", "D2", "E2", "F2", "G2", "H2", "I2", "C1", "C3", + "C4", "C5", "C6", "C7", "C8", "C9", "A1", "A3", "B1", "B3" + } + # fmt: on + print("All tests pass.") + + +def parse_grid(grid): + """ + Convert grid to a dict of possible values, {square: digits}, or + return False if a contradiction is detected. + """ + ## To start, every square can be any digit; then assign values from the grid. + values = dict.fromkeys(squares, digits) + for s, d in grid_values(grid).items(): + if d in digits and not assign(values, s, d): + return False ## (Fail if we can't assign d to square s.) + return values + + +def grid_values(grid): + """ + Convert grid into a dict of {square: char} with '0' or '.' for empties. + """ + chars = [c for c in grid if c in digits or c in "0."] + assert len(chars) == 81 + return dict(zip(squares, chars)) + + +def assign(values, s, d): + """ + Eliminate all the other values (except d) from values[s] and propagate. + Return values, except return False if a contradiction is detected. + """ + other_values = values[s].replace(d, "") + if all(eliminate(values, s, d2) for d2 in other_values): + return values + else: + return False + + +def eliminate(values, s, d): + """ + Eliminate d from values[s]; propagate when values or places <= 2. + Return values, except return False if a contradiction is detected. + """ + if d not in values[s]: + return values ## Already eliminated + values[s] = values[s].replace(d, "") + ## (1) If a square s is reduced to one value d2, then eliminate d2 from the peers. + if len(values[s]) == 0: + return False ## Contradiction: removed last value + elif len(values[s]) == 1: + d2 = values[s] + if not all(eliminate(values, s2, d2) for s2 in peers[s]): + return False + ## (2) If a unit u is reduced to only one place for a value d, then put it there. + for u in units[s]: + dplaces = [s for s in u if d in values[s]] + if len(dplaces) == 0: + return False ## Contradiction: no place for this value + # d can only be in one place in unit; assign it there + elif len(dplaces) == 1 and not assign(values, dplaces[0], d): + return False + return values + + +def display(values) -> None: + """ + Display these values as a 2-D grid. + """ + width = 1 + max(len(values[s]) for s in squares) + line = "+".join(["-" * (width * 3)] * 3) + for r in rows: + print( + "".join( + values[r + c].center(width) + ("|" if c in "36" else "") for c in cols + ) + ) + if r in "CF": + print(line) + print() + + +def solve(grid): + """ + Solve the grid. + """ + return search(parse_grid(grid)) + + +def some(seq): + """Return some element of seq that is true.""" + for e in seq: + if e: + return e + return False + + +def search(values): + """ + Using depth-first search and propagation, try all possible values. + """ + if values is False: + return False ## Failed earlier + if all(len(values[s]) == 1 for s in squares): + return values ## Solved! + ## Chose the unfilled square s with the fewest possibilities + _n, s = min((len(values[s]), s) for s in squares if len(values[s]) > 1) + return some(search(assign(values.copy(), s, d)) for d in values[s]) + + +def solve_all(grids, name="", showif=0.0) -> None: + """ + Attempt to solve a sequence of grids. Report results. + When showif is a number of seconds, display puzzles that take longer. + When showif is None, don't display any puzzles. + """ + + def time_solve(grid): + start = time.monotonic() + values = solve(grid) + t = time.monotonic() - start + ## Display puzzles that take long enough + if showif is not None and t > showif: + display(grid_values(grid)) + if values: + display(values) + print(f"({t:.5f} seconds)\n") + return (t, solved(values)) + + times, results = zip(*[time_solve(grid) for grid in grids]) + if (n := len(grids)) > 1: + print( + "Solved %d of %d %s puzzles (avg %.2f secs (%d Hz), max %.2f secs)." # noqa: UP031 + % (sum(results), n, name, sum(times) / n, n / sum(times), max(times)) + ) + + +def solved(values): + """ + A puzzle is solved if each unit is a permutation of the digits 1 to 9. + """ + + def unitsolved(unit): + return {values[s] for s in unit} == set(digits) + + return values is not False and all(unitsolved(unit) for unit in unitlist) + + +def from_file(filename, sep="\n"): + "Parse a file into a list of strings, separated by sep." + with open(filename) as file: + return file.read().strip().split(sep) + + +def random_puzzle(assignments=17): + """ + Make a random puzzle with N or more assignments. Restart on contradictions. + Note the resulting puzzle is not guaranteed to be solvable, but empirically + about 99.8% of them are solvable. Some have multiple solutions. + """ + values = dict.fromkeys(squares, digits) + for s in shuffled(squares): + if not assign(values, s, random.choice(values[s])): + break + ds = [values[s] for s in squares if len(values[s]) == 1] + if len(ds) >= assignments and len(set(ds)) >= 8: + return "".join(values[s] if len(values[s]) == 1 else "." for s in squares) + return random_puzzle(assignments) ## Give up and make a new puzzle + + +def shuffled(seq): + """ + Return a randomly shuffled copy of the input sequence. + """ + seq = list(seq) + random.shuffle(seq) + return seq + + +grid1 = ( + "003020600900305001001806400008102900700000008006708200002609500800203009005010300" +) +grid2 = ( + "4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......" +) +hard1 = ( + ".....6....59.....82....8....45........3........6..3.54...325..6.................." +) + +if __name__ == "__main__": + test() + # solve_all(from_file("easy50.txt", '========'), "easy", None) + # solve_all(from_file("top95.txt"), "hard", None) + # solve_all(from_file("hardest.txt"), "hardest", None) + solve_all([random_puzzle() for _ in range(99)], "random", 100.0) + for puzzle in (grid1, grid2): # , hard1): # Takes 22 sec to solve on my M1 Mac. + display(parse_grid(puzzle)) + start = time.monotonic() + solve(puzzle) + t = time.monotonic() - start + print(f"Solved: {t:.5f} sec") diff --git a/data_structures/binary_tree/README.md b/data_structures/binary_tree/README.md new file mode 100644 index 000000000000..07116fa5b038 --- /dev/null +++ b/data_structures/binary_tree/README.md @@ -0,0 +1,113 @@ +# Binary Tree Traversal + +## Overview + +The combination of binary trees being data structures and traversal being an algorithm relates to classic problems, either directly or indirectly. + +> If you can grasp the traversal of binary trees, the traversal of other complicated trees will be easy for you. + +The following are some common ways to traverse trees. + +- Depth First Traversals (DFS): In-order, Pre-order, Post-order + +- Level Order Traversal or Breadth First or Traversal (BFS) + +There are applications for both DFS and BFS. + +Stack can be used to simplify the process of DFS traversal. Besides, since tree is a recursive data structure, recursion and stack are two key points for DFS. + +Graph for DFS: + +![binary-tree-traversal-dfs](https://tva1.sinaimg.cn/large/007S8ZIlly1ghluhzhynsg30dw0dw3yl.gif) + +The key point of BFS is how to determine whether the traversal of each level has been completed. The answer is to use a variable as a flag to represent the end of the traversal of current level. + +## Pre-order Traversal + +The traversal order of pre-order traversal is `root-left-right`. + +Algorithm Pre-order + +1. Visit the root node and push it into a stack. + +2. Pop a node from the stack, and push its right and left child node into the stack respectively. + +3. Repeat step 2. + +Conclusion: This problem involves the classic recursive data structure (i.e. a binary tree), and the algorithm above demonstrates how a simplified solution can be reached by using a stack. + +If you look at the bigger picture, you'll find that the process of traversal is as followed. `Visit the left subtrees respectively from top to bottom, and visit the right subtrees respectively from bottom to top`. +If we are to implement it from this perspective, things will be somewhat different. For the `top to bottom` part we can simply use recursion, and for the `bottom to top` part we can turn to stack. + +## In-order Traversal + +The traversal order of in-order traversal is `left-root-right`. + +So the root node is not printed first. Things are getting a bit complicated here. + +Algorithm In-order + +1. Visit the root and push it into a stack. + +2. If there is a left child node, push it into the stack. Repeat this process until a leaf node reached. + + > At this point the root node and all the left nodes are in the stack. + +3. Start popping nodes from the stack. If a node has a right child node, push the child node into the stack. Repeat step 2. + +It's worth pointing out that the in-order traversal of a binary search tree (BST) is a sorted array, which is helpful for coming up simplified solutions for some problems. + +## Post-order Traversal + +The traversal order of post-order traversal is `left-right-root`. + +This one is a bit of a challenge. It deserves the `hard` tag of LeetCode. + +In this case, the root node is printed not as the first but the last one. A cunning way to do it is to: + +Record whether the current node has been visited. If 1) it's a leaf node or 2) both its left and right subtrees have been traversed, then it can be popped from the stack. + +As for `1) it's a leaf node`, you can easily tell whether a node is a leaf if both its left and right are `null`. + +As for `2) both its left and right subtrees have been traversed`, we only need a variable to record whether a node has been visited or not. In the worst case, we need to record the status for every single node and the space complexity is `O(n)`. +But if you come to think about it, as we are using a stack and start printing the result from the leaf nodes, it makes sense that we only record the status for the current node popping from the stack, reducing the space complexity to `O(1)`. + +## Level Order Traversal + +The key point of level order traversal is how do we know whether the traversal of each level is done. The answer is that we use a variable as a flag representing the end of the traversal of the current level. + +![binary-tree-traversal-bfs](https://tva1.sinaimg.cn/large/007S8ZIlly1ghlui1tpoug30dw0dw3yl.gif) + +Algorithm Level-order + +1. Visit the root node, put it in a FIFO queue, put in the queue a special flag (we are using `null` here). + +2. Dequeue a node. + +3. If the node equals `null`, it means that all nodes of the current level have been visited. If the queue is empty, we do nothing. Or else we put in another `null`. + +4. If the node is not `null`, meaning the traversal of current level has not finished yet, we enqueue its left subtree and right subtree respectively. + +## Bi-color marking + +We know that there is a tri-color marking in garbage collection algorithm, which works as described below. + +- The white color represents "not visited". + +- The gray color represents "not all child nodes visited". + +- The black color represents "all child nodes visited". + +Enlightened by tri-color marking, a bi-color marking method can be invented to solve all three traversal problems with one solution. + +The core idea is as follow. + +- Use a color to mark whether a node has been visited or not. Nodes yet to be visited are marked as white and visited nodes are marked as gray. + +- If we are visiting a white node, turn it into gray, and push its right child node, itself, and it's left child node into the stack respectively. + +- If we are visiting a gray node, print it. + +Implementation of pre-order and post-order traversal algorithms can be easily done by changing the order of pushing the child nodes into the stack. + +Reference: [LeetCode](https://github.com/azl397985856/leetcode/blob/master/thinkings/binary-tree-traversal.en.md) diff --git a/data_structures/binary_tree/__init__.py b/data_structures/binary_tree/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/binary_tree/avl_tree.py b/data_structures/binary_tree/avl_tree.py new file mode 100644 index 000000000000..bb15afd331a8 --- /dev/null +++ b/data_structures/binary_tree/avl_tree.py @@ -0,0 +1,350 @@ +""" +Implementation of an auto-balanced binary tree! +For doctests run following command: +python3 -m doctest -v avl_tree.py +For testing run: +python avl_tree.py +""" + +from __future__ import annotations + +import math +import random +from typing import Any + + +class MyQueue: + def __init__(self) -> None: + self.data: list[Any] = [] + self.head: int = 0 + self.tail: int = 0 + + def is_empty(self) -> bool: + return self.head == self.tail + + def push(self, data: Any) -> None: + self.data.append(data) + self.tail = self.tail + 1 + + def pop(self) -> Any: + ret = self.data[self.head] + self.head = self.head + 1 + return ret + + def count(self) -> int: + return self.tail - self.head + + def print_queue(self) -> None: + print(self.data) + print("**************") + print(self.data[self.head : self.tail]) + + +class MyNode: + def __init__(self, data: Any) -> None: + self.data = data + self.left: MyNode | None = None + self.right: MyNode | None = None + self.height: int = 1 + + def get_data(self) -> Any: + return self.data + + def get_left(self) -> MyNode | None: + return self.left + + def get_right(self) -> MyNode | None: + return self.right + + def get_height(self) -> int: + return self.height + + def set_data(self, data: Any) -> None: + self.data = data + + def set_left(self, node: MyNode | None) -> None: + self.left = node + + def set_right(self, node: MyNode | None) -> None: + self.right = node + + def set_height(self, height: int) -> None: + self.height = height + + +def get_height(node: MyNode | None) -> int: + if node is None: + return 0 + return node.get_height() + + +def my_max(a: int, b: int) -> int: + if a > b: + return a + return b + + +def right_rotation(node: MyNode) -> MyNode: + r""" + A B + / \ / \ + B C Bl A + / \ --> / / \ + Bl Br UB Br C + / + UB + UB = unbalanced node + """ + print("left rotation node:", node.get_data()) + ret = node.get_left() + assert ret is not None + node.set_left(ret.get_right()) + ret.set_right(node) + h1 = my_max(get_height(node.get_right()), get_height(node.get_left())) + 1 + node.set_height(h1) + h2 = my_max(get_height(ret.get_right()), get_height(ret.get_left())) + 1 + ret.set_height(h2) + return ret + + +def left_rotation(node: MyNode) -> MyNode: + """ + a mirror symmetry rotation of the left_rotation + """ + print("right rotation node:", node.get_data()) + ret = node.get_right() + assert ret is not None + node.set_right(ret.get_left()) + ret.set_left(node) + h1 = my_max(get_height(node.get_right()), get_height(node.get_left())) + 1 + node.set_height(h1) + h2 = my_max(get_height(ret.get_right()), get_height(ret.get_left())) + 1 + ret.set_height(h2) + return ret + + +def lr_rotation(node: MyNode) -> MyNode: + r""" + A A Br + / \ / \ / \ + B C LR Br C RR B A + / \ --> / \ --> / / \ + Bl Br B UB Bl UB C + \ / + UB Bl + RR = right_rotation LR = left_rotation + """ + left_child = node.get_left() + assert left_child is not None + node.set_left(left_rotation(left_child)) + return right_rotation(node) + + +def rl_rotation(node: MyNode) -> MyNode: + right_child = node.get_right() + assert right_child is not None + node.set_right(right_rotation(right_child)) + return left_rotation(node) + + +def insert_node(node: MyNode | None, data: Any) -> MyNode | None: + if node is None: + return MyNode(data) + if data < node.get_data(): + node.set_left(insert_node(node.get_left(), data)) + if ( + get_height(node.get_left()) - get_height(node.get_right()) == 2 + ): # an unbalance detected + left_child = node.get_left() + assert left_child is not None + if ( + data < left_child.get_data() + ): # new node is the left child of the left child + node = right_rotation(node) + else: + node = lr_rotation(node) + else: + node.set_right(insert_node(node.get_right(), data)) + if get_height(node.get_right()) - get_height(node.get_left()) == 2: + right_child = node.get_right() + assert right_child is not None + if data < right_child.get_data(): + node = rl_rotation(node) + else: + node = left_rotation(node) + h1 = my_max(get_height(node.get_right()), get_height(node.get_left())) + 1 + node.set_height(h1) + return node + + +def get_right_most(root: MyNode) -> Any: + while True: + right_child = root.get_right() + if right_child is None: + break + root = right_child + return root.get_data() + + +def get_left_most(root: MyNode) -> Any: + while True: + left_child = root.get_left() + if left_child is None: + break + root = left_child + return root.get_data() + + +def del_node(root: MyNode, data: Any) -> MyNode | None: + left_child = root.get_left() + right_child = root.get_right() + if root.get_data() == data: + if left_child is not None and right_child is not None: + temp_data = get_left_most(right_child) + root.set_data(temp_data) + root.set_right(del_node(right_child, temp_data)) + elif left_child is not None: + root = left_child + elif right_child is not None: + root = right_child + else: + return None + elif root.get_data() > data: + if left_child is None: + print("No such data") + return root + else: + root.set_left(del_node(left_child, data)) + # root.get_data() < data + elif right_child is None: + return root + else: + root.set_right(del_node(right_child, data)) + + # Re-fetch left_child and right_child references + left_child = root.get_left() + right_child = root.get_right() + + match get_height(right_child) - get_height(left_child): + case 2: + assert right_child is not None + if get_height(right_child.get_right()) > get_height(right_child.get_left()): + root = left_rotation(root) + else: + root = rl_rotation(root) + case -2: + assert left_child is not None + if get_height(left_child.get_left()) > get_height(left_child.get_right()): + root = right_rotation(root) + else: + root = lr_rotation(root) + height = my_max(get_height(root.get_right()), get_height(root.get_left())) + 1 + root.set_height(height) + return root + + +class AVLtree: + """ + An AVL tree doctest + Examples: + >>> t = AVLtree() + >>> t.insert(4) + insert:4 + >>> print(str(t).replace(" \\n","\\n")) + 4 + ************************************* + >>> t.insert(2) + insert:2 + >>> print(str(t).replace(" \\n","\\n").replace(" \\n","\\n")) + 4 + 2 * + ************************************* + >>> t.insert(3) + insert:3 + right rotation node: 2 + left rotation node: 4 + >>> print(str(t).replace(" \\n","\\n").replace(" \\n","\\n")) + 3 + 2 4 + ************************************* + >>> t.get_height() + 2 + >>> t.del_node(3) + delete:3 + >>> print(str(t).replace(" \\n","\\n").replace(" \\n","\\n")) + 4 + 2 * + ************************************* + """ + + def __init__(self) -> None: + self.root: MyNode | None = None + + def get_height(self) -> int: + return get_height(self.root) + + def insert(self, data: Any) -> None: + print("insert:" + str(data)) + self.root = insert_node(self.root, data) + + def del_node(self, data: Any) -> None: + print("delete:" + str(data)) + if self.root is None: + print("Tree is empty!") + return + self.root = del_node(self.root, data) + + def __str__( + self, + ) -> str: # a level traversale, gives a more intuitive look on the tree + output = "" + q = MyQueue() + q.push(self.root) + layer = self.get_height() + if layer == 0: + return output + cnt = 0 + while not q.is_empty(): + node = q.pop() + space = " " * int(math.pow(2, layer - 1)) + output += space + if node is None: + output += "*" + q.push(None) + q.push(None) + else: + output += str(node.get_data()) + q.push(node.get_left()) + q.push(node.get_right()) + output += space + cnt = cnt + 1 + for i in range(100): + if cnt == math.pow(2, i) - 1: + layer = layer - 1 + if layer == 0: + output += "\n*************************************" + return output + output += "\n" + break + output += "\n*************************************" + return output + + +def _test() -> None: + import doctest + + doctest.testmod() + + +if __name__ == "__main__": + _test() + t = AVLtree() + lst = list(range(10)) + random.shuffle(lst) + for i in lst: + t.insert(i) + print(str(t)) + random.shuffle(lst) + for i in lst: + t.del_node(i) + print(str(t)) diff --git a/data_structures/binary_tree/basic_binary_tree.py b/data_structures/binary_tree/basic_binary_tree.py new file mode 100644 index 000000000000..9d4c1bdbb57a --- /dev/null +++ b/data_structures/binary_tree/basic_binary_tree.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass + + +@dataclass +class Node: + data: int + left: Node | None = None + right: Node | None = None + + def __iter__(self) -> Iterator[int]: + if self.left: + yield from self.left + yield self.data + if self.right: + yield from self.right + + def __len__(self) -> int: + return sum(1 for _ in self) + + def is_full(self) -> bool: + if not self or (not self.left and not self.right): + return True + if self.left and self.right: + return self.left.is_full() and self.right.is_full() + return False + + +@dataclass +class BinaryTree: + root: Node + + def __iter__(self) -> Iterator[int]: + return iter(self.root) + + def __len__(self) -> int: + return len(self.root) + + @classmethod + def small_tree(cls) -> BinaryTree: + """ + Return a small binary tree with 3 nodes. + >>> binary_tree = BinaryTree.small_tree() + >>> len(binary_tree) + 3 + >>> list(binary_tree) + [1, 2, 3] + """ + binary_tree = BinaryTree(Node(2)) + binary_tree.root.left = Node(1) + binary_tree.root.right = Node(3) + return binary_tree + + @classmethod + def medium_tree(cls) -> BinaryTree: + """ + Return a medium binary tree with 3 nodes. + >>> binary_tree = BinaryTree.medium_tree() + >>> len(binary_tree) + 7 + >>> list(binary_tree) + [1, 2, 3, 4, 5, 6, 7] + """ + binary_tree = BinaryTree(Node(4)) + binary_tree.root.left = two = Node(2) + two.left = Node(1) + two.right = Node(3) + binary_tree.root.right = five = Node(5) + five.right = six = Node(6) + six.right = Node(7) + return binary_tree + + def depth(self) -> int: + """ + Returns the depth of the tree + + >>> BinaryTree(Node(1)).depth() + 1 + >>> BinaryTree.small_tree().depth() + 2 + >>> BinaryTree.medium_tree().depth() + 4 + """ + return self._depth(self.root) + + def _depth(self, node: Node | None) -> int: + if not node: + return 0 + return 1 + max(self._depth(node.left), self._depth(node.right)) + + def is_full(self) -> bool: + """ + Returns True if the tree is full + + >>> BinaryTree(Node(1)).is_full() + True + >>> BinaryTree.small_tree().is_full() + True + >>> BinaryTree.medium_tree().is_full() + False + """ + return self.root.is_full() + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/binary_tree/binary_search_tree.py b/data_structures/binary_tree/binary_search_tree.py new file mode 100644 index 000000000000..16b7b6b1ede2 --- /dev/null +++ b/data_structures/binary_tree/binary_search_tree.py @@ -0,0 +1,354 @@ +r""" +A binary search Tree + +Example + 8 + / \ + 3 10 + / \ \ + 1 6 14 + / \ / + 4 7 13 + +>>> t = BinarySearchTree().insert(8, 3, 6, 1, 10, 14, 13, 4, 7) +>>> print(" ".join(repr(i.value) for i in t.traversal_tree())) +8 3 1 6 4 7 10 14 13 + +>>> tuple(i.value for i in t.traversal_tree(inorder)) +(1, 3, 4, 6, 7, 8, 10, 13, 14) +>>> tuple(t) +(1, 3, 4, 6, 7, 8, 10, 13, 14) +>>> t.find_kth_smallest(3, t.root) +4 +>>> tuple(t)[3-1] +4 + +>>> print(" ".join(repr(i.value) for i in t.traversal_tree(postorder))) +1 4 7 6 3 13 14 10 8 +>>> t.remove(20) +Traceback (most recent call last): + ... +ValueError: Value 20 not found +>>> BinarySearchTree().search(6) +Traceback (most recent call last): + ... +IndexError: Warning: Tree is empty! please use another. + +Other example: + +>>> testlist = (8, 3, 6, 1, 10, 14, 13, 4, 7) +>>> t = BinarySearchTree() +>>> for i in testlist: +... t.insert(i) # doctest: +ELLIPSIS +BinarySearchTree(root=8) +BinarySearchTree(root={'8': (3, None)}) +BinarySearchTree(root={'8': ({'3': (None, 6)}, None)}) +BinarySearchTree(root={'8': ({'3': (1, 6)}, None)}) +BinarySearchTree(root={'8': ({'3': (1, 6)}, 10)}) +BinarySearchTree(root={'8': ({'3': (1, 6)}, {'10': (None, 14)})}) +BinarySearchTree(root={'8': ({'3': (1, 6)}, {'10': (None, {'14': (13, None)})})}) +BinarySearchTree(root={'8': ({'3': (1, {'6': (4, None)})}, {'10': (None, {'14': ... +BinarySearchTree(root={'8': ({'3': (1, {'6': (4, 7)})}, {'10': (None, {'14': (13, ... + +Prints all the elements of the list in order traversal +>>> print(t) +{'8': ({'3': (1, {'6': (4, 7)})}, {'10': (None, {'14': (13, None)})})} + +Test existence +>>> t.search(6) is not None +True +>>> 6 in t +True +>>> t.search(-1) is not None +False +>>> -1 in t +False + +>>> t.search(6).is_right +True +>>> t.search(1).is_right +False + +>>> t.get_max().value +14 +>>> max(t) +14 +>>> t.get_min().value +1 +>>> min(t) +1 +>>> t.empty() +False +>>> not t +False +>>> for i in testlist: +... t.remove(i) +>>> t.empty() +True +>>> not t +True +""" + +from __future__ import annotations + +from collections.abc import Iterable, Iterator +from dataclasses import dataclass +from typing import Any, Self + + +@dataclass +class Node: + value: int + left: Node | None = None + right: Node | None = None + parent: Node | None = None # Added in order to delete a node easier + + def __iter__(self) -> Iterator[int]: + """ + >>> list(Node(0)) + [0] + >>> list(Node(0, Node(-1), Node(1), None)) + [-1, 0, 1] + """ + yield from self.left or [] + yield self.value + yield from self.right or [] + + def __repr__(self) -> str: + from pprint import pformat + + if self.left is None and self.right is None: + return str(self.value) + return pformat({f"{self.value}": (self.left, self.right)}, indent=1) + + @property + def is_right(self) -> bool: + return bool(self.parent and self is self.parent.right) + + +@dataclass +class BinarySearchTree: + root: Node | None = None + + def __bool__(self) -> bool: + return bool(self.root) + + def __iter__(self) -> Iterator[int]: + yield from self.root or [] + + def __str__(self) -> str: + """ + Return a string of all the Nodes using in order traversal + """ + return str(self.root) + + def __reassign_nodes(self, node: Node, new_children: Node | None) -> None: + if new_children is not None: # reset its kids + new_children.parent = node.parent + if node.parent is not None: # reset its parent + if node.is_right: # If it is the right child + node.parent.right = new_children + else: + node.parent.left = new_children + else: + self.root = new_children + + def empty(self) -> bool: + """ + Returns True if the tree does not have any element(s). + False if the tree has element(s). + + >>> BinarySearchTree().empty() + True + >>> BinarySearchTree().insert(1).empty() + False + >>> BinarySearchTree().insert(8, 3, 6, 1, 10, 14, 13, 4, 7).empty() + False + """ + return not self.root + + def __insert(self, value) -> None: + """ + Insert a new node in Binary Search Tree with value label + """ + new_node = Node(value) # create a new Node + if self.empty(): # if Tree is empty + self.root = new_node # set its root + else: # Tree is not empty + parent_node = self.root # from root + if parent_node is None: + return + while True: # While we don't get to a leaf + if value < parent_node.value: # We go left + if parent_node.left is None: + parent_node.left = new_node # We insert the new node in a leaf + break + parent_node = parent_node.left + elif parent_node.right is None: + parent_node.right = new_node + break + else: + parent_node = parent_node.right + new_node.parent = parent_node + + def insert(self, *values) -> Self: + for value in values: + self.__insert(value) + return self + + def search(self, value) -> Node | None: + """ + >>> tree = BinarySearchTree().insert(10, 20, 30, 40, 50) + >>> tree.search(10) + {'10': (None, {'20': (None, {'30': (None, {'40': (None, 50)})})})} + >>> tree.search(20) + {'20': (None, {'30': (None, {'40': (None, 50)})})} + >>> tree.search(30) + {'30': (None, {'40': (None, 50)})} + >>> tree.search(40) + {'40': (None, 50)} + >>> tree.search(50) + 50 + >>> tree.search(5) is None # element not present + True + >>> tree.search(0) is None # element not present + True + >>> tree.search(-5) is None # element not present + True + >>> BinarySearchTree().search(10) + Traceback (most recent call last): + ... + IndexError: Warning: Tree is empty! please use another. + """ + + if self.empty(): + raise IndexError("Warning: Tree is empty! please use another.") + node = self.root + # use lazy evaluation here to avoid NoneType Attribute error + while node is not None and node.value is not value: + node = node.left if value < node.value else node.right + return node + + def get_max(self, node: Node | None = None) -> Node | None: + """ + We go deep on the right branch + + >>> BinarySearchTree().insert(10, 20, 30, 40, 50).get_max() + 50 + >>> BinarySearchTree().insert(-5, -1, 0.1, -0.3, -4.5).get_max() + {'0.1': (-0.3, None)} + >>> BinarySearchTree().insert(1, 78.3, 30, 74.0, 1).get_max() + {'78.3': ({'30': (1, 74.0)}, None)} + >>> BinarySearchTree().insert(1, 783, 30, 740, 1).get_max() + {'783': ({'30': (1, 740)}, None)} + """ + if node is None: + if self.root is None: + return None + node = self.root + + if not self.empty(): + while node.right is not None: + node = node.right + return node + + def get_min(self, node: Node | None = None) -> Node | None: + """ + We go deep on the left branch + + >>> BinarySearchTree().insert(10, 20, 30, 40, 50).get_min() + {'10': (None, {'20': (None, {'30': (None, {'40': (None, 50)})})})} + >>> BinarySearchTree().insert(-5, -1, 0, -0.3, -4.5).get_min() + {'-5': (None, {'-1': (-4.5, {'0': (-0.3, None)})})} + >>> BinarySearchTree().insert(1, 78.3, 30, 74.0, 1).get_min() + {'1': (None, {'78.3': ({'30': (1, 74.0)}, None)})} + >>> BinarySearchTree().insert(1, 783, 30, 740, 1).get_min() + {'1': (None, {'783': ({'30': (1, 740)}, None)})} + """ + if node is None: + node = self.root + if self.root is None: + return None + if not self.empty(): + node = self.root + while node.left is not None: + node = node.left + return node + + def remove(self, value: int) -> None: + # Look for the node with that label + node = self.search(value) + if node is None: + msg = f"Value {value} not found" + raise ValueError(msg) + + if node.left is None and node.right is None: # If it has no children + self.__reassign_nodes(node, None) + elif node.left is None: # Has only right children + self.__reassign_nodes(node, node.right) + elif node.right is None: # Has only left children + self.__reassign_nodes(node, node.left) + else: + predecessor = self.get_max( + node.left + ) # Gets the max value of the left branch + self.remove(predecessor.value) # type: ignore[union-attr] + node.value = ( + predecessor.value # type: ignore[union-attr] + ) # Assigns the value to the node to delete and keep tree structure + + def preorder_traverse(self, node: Node | None) -> Iterable: + if node is not None: + yield node # Preorder Traversal + yield from self.preorder_traverse(node.left) + yield from self.preorder_traverse(node.right) + + def traversal_tree(self, traversal_function=None) -> Any: + """ + This function traversal the tree. + You can pass a function to traversal the tree as needed by client code + """ + if traversal_function is None: + return self.preorder_traverse(self.root) + else: + return traversal_function(self.root) + + def inorder(self, arr: list, node: Node | None) -> None: + """Perform an inorder traversal and append values of the nodes to + a list named arr""" + if node: + self.inorder(arr, node.left) + arr.append(node.value) + self.inorder(arr, node.right) + + def find_kth_smallest(self, k: int, node: Node) -> int: + """Return the kth smallest element in a binary search tree""" + arr: list[int] = [] + self.inorder(arr, node) # append all values to list using inorder traversal + return arr[k - 1] + + +def inorder(curr_node: Node | None) -> list[Node]: + """ + inorder (left, self, right) + """ + node_list = [] + if curr_node is not None: + node_list = [*inorder(curr_node.left), curr_node, *inorder(curr_node.right)] + return node_list + + +def postorder(curr_node: Node | None) -> list[Node]: + """ + postOrder (left, right, self) + """ + node_list = [] + if curr_node is not None: + node_list = postorder(curr_node.left) + postorder(curr_node.right) + [curr_node] + return node_list + + +if __name__ == "__main__": + import doctest + + doctest.testmod(verbose=True) diff --git a/data_structures/binary_tree/binary_search_tree_recursive.py b/data_structures/binary_tree/binary_search_tree_recursive.py new file mode 100644 index 000000000000..0bafcc8f4ea0 --- /dev/null +++ b/data_structures/binary_tree/binary_search_tree_recursive.py @@ -0,0 +1,641 @@ +""" +This is a python3 implementation of binary search tree using recursion + +To run tests: +python -m unittest binary_search_tree_recursive.py + +To run an example: +python binary_search_tree_recursive.py +""" + +from __future__ import annotations + +import unittest +from collections.abc import Iterator + +import pytest + + +class Node: + def __init__(self, label: int, parent: Node | None) -> None: + self.label = label + self.parent = parent + self.left: Node | None = None + self.right: Node | None = None + + +class BinarySearchTree: + def __init__(self) -> None: + self.root: Node | None = None + + def empty(self) -> None: + """ + Empties the tree + + >>> t = BinarySearchTree() + >>> assert t.root is None + >>> t.put(8) + >>> assert t.root is not None + """ + self.root = None + + def is_empty(self) -> bool: + """ + Checks if the tree is empty + + >>> t = BinarySearchTree() + >>> t.is_empty() + True + >>> t.put(8) + >>> t.is_empty() + False + """ + return self.root is None + + def put(self, label: int) -> None: + """ + Put a new node in the tree + + >>> t = BinarySearchTree() + >>> t.put(8) + >>> assert t.root.parent is None + >>> assert t.root.label == 8 + + >>> t.put(10) + >>> assert t.root.right.parent == t.root + >>> assert t.root.right.label == 10 + + >>> t.put(3) + >>> assert t.root.left.parent == t.root + >>> assert t.root.left.label == 3 + """ + self.root = self._put(self.root, label) + + def _put(self, node: Node | None, label: int, parent: Node | None = None) -> Node: + if node is None: + node = Node(label, parent) + elif label < node.label: + node.left = self._put(node.left, label, node) + elif label > node.label: + node.right = self._put(node.right, label, node) + else: + msg = f"Node with label {label} already exists" + raise ValueError(msg) + + return node + + def search(self, label: int) -> Node: + """ + Searches a node in the tree + + >>> t = BinarySearchTree() + >>> t.put(8) + >>> t.put(10) + >>> node = t.search(8) + >>> assert node.label == 8 + + >>> node = t.search(3) + Traceback (most recent call last): + ... + ValueError: Node with label 3 does not exist + """ + return self._search(self.root, label) + + def _search(self, node: Node | None, label: int) -> Node: + if node is None: + msg = f"Node with label {label} does not exist" + raise ValueError(msg) + if label < node.label: + node = self._search(node.left, label) + elif label > node.label: + node = self._search(node.right, label) + + return node + + def remove(self, label: int) -> None: + """ + Removes a node in the tree + + >>> t = BinarySearchTree() + >>> t.put(8) + >>> t.put(10) + >>> t.remove(8) + >>> assert t.root.label == 10 + + >>> t.remove(3) + Traceback (most recent call last): + ... + ValueError: Node with label 3 does not exist + """ + node = self.search(label) + if node.right and node.left: + lowest_node = self._get_lowest_node(node.right) + lowest_node.left = node.left + lowest_node.right = node.right + node.left.parent = lowest_node + if node.right: + node.right.parent = lowest_node + self._reassign_nodes(node, lowest_node) + elif not node.right and node.left: + self._reassign_nodes(node, node.left) + elif node.right and not node.left: + self._reassign_nodes(node, node.right) + else: + self._reassign_nodes(node, None) + + def _reassign_nodes(self, node: Node, new_children: Node | None) -> None: + if new_children: + new_children.parent = node.parent + + if node.parent: + if node.parent.right == node: + node.parent.right = new_children + else: + node.parent.left = new_children + else: + self.root = new_children + + def _get_lowest_node(self, node: Node) -> Node: + if node.left: + lowest_node = self._get_lowest_node(node.left) + else: + lowest_node = node + self._reassign_nodes(node, node.right) + + return lowest_node + + def exists(self, label: int) -> bool: + """ + Checks if a node exists in the tree + + >>> t = BinarySearchTree() + >>> t.put(8) + >>> t.put(10) + >>> t.exists(8) + True + + >>> t.exists(3) + False + """ + try: + self.search(label) + return True + except ValueError: + return False + + def get_max_label(self) -> int: + """ + Gets the max label inserted in the tree + + >>> t = BinarySearchTree() + >>> t.get_max_label() + Traceback (most recent call last): + ... + ValueError: Binary search tree is empty + + >>> t.put(8) + >>> t.put(10) + >>> t.get_max_label() + 10 + """ + if self.root is None: + raise ValueError("Binary search tree is empty") + + node = self.root + while node.right is not None: + node = node.right + + return node.label + + def get_min_label(self) -> int: + """ + Gets the min label inserted in the tree + + >>> t = BinarySearchTree() + >>> t.get_min_label() + Traceback (most recent call last): + ... + ValueError: Binary search tree is empty + + >>> t.put(8) + >>> t.put(10) + >>> t.get_min_label() + 8 + """ + if self.root is None: + raise ValueError("Binary search tree is empty") + + node = self.root + while node.left is not None: + node = node.left + + return node.label + + def inorder_traversal(self) -> Iterator[Node]: + """ + Return the inorder traversal of the tree + + >>> t = BinarySearchTree() + >>> [i.label for i in t.inorder_traversal()] + [] + + >>> t.put(8) + >>> t.put(10) + >>> t.put(9) + >>> [i.label for i in t.inorder_traversal()] + [8, 9, 10] + """ + return self._inorder_traversal(self.root) + + def _inorder_traversal(self, node: Node | None) -> Iterator[Node]: + if node is not None: + yield from self._inorder_traversal(node.left) + yield node + yield from self._inorder_traversal(node.right) + + def preorder_traversal(self) -> Iterator[Node]: + """ + Return the preorder traversal of the tree + + >>> t = BinarySearchTree() + >>> [i.label for i in t.preorder_traversal()] + [] + + >>> t.put(8) + >>> t.put(10) + >>> t.put(9) + >>> [i.label for i in t.preorder_traversal()] + [8, 10, 9] + """ + return self._preorder_traversal(self.root) + + def _preorder_traversal(self, node: Node | None) -> Iterator[Node]: + if node is not None: + yield node + yield from self._preorder_traversal(node.left) + yield from self._preorder_traversal(node.right) + + +class BinarySearchTreeTest(unittest.TestCase): + @staticmethod + def _get_binary_search_tree() -> BinarySearchTree: + r""" + 8 + / \ + 3 10 + / \ \ + 1 6 14 + / \ / + 4 7 13 + \ + 5 + """ + t = BinarySearchTree() + t.put(8) + t.put(3) + t.put(6) + t.put(1) + t.put(10) + t.put(14) + t.put(13) + t.put(4) + t.put(7) + t.put(5) + + return t + + def test_put(self) -> None: + t = BinarySearchTree() + assert t.is_empty() + + t.put(8) + r""" + 8 + """ + assert t.root is not None + assert t.root.parent is None + assert t.root.label == 8 + + t.put(10) + r""" + 8 + \ + 10 + """ + assert t.root.right is not None + assert t.root.right.parent == t.root + assert t.root.right.label == 10 + + t.put(3) + r""" + 8 + / \ + 3 10 + """ + assert t.root.left is not None + assert t.root.left.parent == t.root + assert t.root.left.label == 3 + + t.put(6) + r""" + 8 + / \ + 3 10 + \ + 6 + """ + assert t.root.left.right is not None + assert t.root.left.right.parent == t.root.left + assert t.root.left.right.label == 6 + + t.put(1) + r""" + 8 + / \ + 3 10 + / \ + 1 6 + """ + assert t.root.left.left is not None + assert t.root.left.left.parent == t.root.left + assert t.root.left.left.label == 1 + + with pytest.raises(ValueError): + t.put(1) + + def test_search(self) -> None: + t = self._get_binary_search_tree() + + node = t.search(6) + assert node.label == 6 + + node = t.search(13) + assert node.label == 13 + + with pytest.raises(ValueError): + t.search(2) + + def test_remove(self) -> None: + t = self._get_binary_search_tree() + + t.remove(13) + r""" + 8 + / \ + 3 10 + / \ \ + 1 6 14 + / \ + 4 7 + \ + 5 + """ + assert t.root is not None + assert t.root.right is not None + assert t.root.right.right is not None + assert t.root.right.right.right is None + assert t.root.right.right.left is None + + t.remove(7) + r""" + 8 + / \ + 3 10 + / \ \ + 1 6 14 + / + 4 + \ + 5 + """ + assert t.root.left is not None + assert t.root.left.right is not None + assert t.root.left.right.left is not None + assert t.root.left.right.right is None + assert t.root.left.right.left.label == 4 + + t.remove(6) + r""" + 8 + / \ + 3 10 + / \ \ + 1 4 14 + \ + 5 + """ + assert t.root.left.left is not None + assert t.root.left.right.right is not None + assert t.root.left.left.label == 1 + assert t.root.left.right.label == 4 + assert t.root.left.right.right.label == 5 + assert t.root.left.right.left is None + assert t.root.left.left.parent == t.root.left + assert t.root.left.right.parent == t.root.left + + t.remove(3) + r""" + 8 + / \ + 4 10 + / \ \ + 1 5 14 + """ + assert t.root is not None + assert t.root.left.label == 4 + assert t.root.left.right.label == 5 + assert t.root.left.left.label == 1 + assert t.root.left.parent == t.root + assert t.root.left.left.parent == t.root.left + assert t.root.left.right.parent == t.root.left + + t.remove(4) + r""" + 8 + / \ + 5 10 + / \ + 1 14 + """ + assert t.root.left is not None + assert t.root.left.left is not None + assert t.root.left.label == 5 + assert t.root.left.right is None + assert t.root.left.left.label == 1 + assert t.root.left.parent == t.root + assert t.root.left.left.parent == t.root.left + + def test_remove_2(self) -> None: + t = self._get_binary_search_tree() + + t.remove(3) + r""" + 8 + / \ + 4 10 + / \ \ + 1 6 14 + / \ / + 5 7 13 + """ + assert t.root is not None + assert t.root.left is not None + assert t.root.left.left is not None + assert t.root.left.right is not None + assert t.root.left.right.left is not None + assert t.root.left.right.right is not None + assert t.root.left.label == 4 + assert t.root.left.right.label == 6 + assert t.root.left.left.label == 1 + assert t.root.left.right.right.label == 7 + assert t.root.left.right.left.label == 5 + assert t.root.left.parent == t.root + assert t.root.left.right.parent == t.root.left + assert t.root.left.left.parent == t.root.left + assert t.root.left.right.left.parent == t.root.left.right + + def test_empty(self) -> None: + t = self._get_binary_search_tree() + t.empty() + assert t.root is None + + def test_is_empty(self) -> None: + t = self._get_binary_search_tree() + assert not t.is_empty() + + t.empty() + assert t.is_empty() + + def test_exists(self) -> None: + t = self._get_binary_search_tree() + + assert t.exists(6) + assert not t.exists(-1) + + def test_get_max_label(self) -> None: + t = self._get_binary_search_tree() + + assert t.get_max_label() == 14 + + t.empty() + with pytest.raises(ValueError): + t.get_max_label() + + def test_get_min_label(self) -> None: + t = self._get_binary_search_tree() + + assert t.get_min_label() == 1 + + t.empty() + with pytest.raises(ValueError): + t.get_min_label() + + def test_inorder_traversal(self) -> None: + t = self._get_binary_search_tree() + + inorder_traversal_nodes = [i.label for i in t.inorder_traversal()] + assert inorder_traversal_nodes == [1, 3, 4, 5, 6, 7, 8, 10, 13, 14] + + def test_preorder_traversal(self) -> None: + t = self._get_binary_search_tree() + + preorder_traversal_nodes = [i.label for i in t.preorder_traversal()] + assert preorder_traversal_nodes == [8, 3, 1, 6, 4, 5, 7, 10, 14, 13] + + +def binary_search_tree_example() -> None: + r""" + Example + 8 + / \ + 3 10 + / \ \ + 1 6 14 + / \ / + 4 7 13 + \ + 5 + + Example After Deletion + 4 + / \ + 1 7 + \ + 5 + + """ + + t = BinarySearchTree() + t.put(8) + t.put(3) + t.put(6) + t.put(1) + t.put(10) + t.put(14) + t.put(13) + t.put(4) + t.put(7) + t.put(5) + + print( + """ + 8 + / \\ + 3 10 + / \\ \\ + 1 6 14 + / \\ / + 4 7 13 + \\ + 5 + """ + ) + + print("Label 6 exists:", t.exists(6)) + print("Label 13 exists:", t.exists(13)) + print("Label -1 exists:", t.exists(-1)) + print("Label 12 exists:", t.exists(12)) + + # Prints all the elements of the list in inorder traversal + inorder_traversal_nodes = [i.label for i in t.inorder_traversal()] + print("Inorder traversal:", inorder_traversal_nodes) + + # Prints all the elements of the list in preorder traversal + preorder_traversal_nodes = [i.label for i in t.preorder_traversal()] + print("Preorder traversal:", preorder_traversal_nodes) + + print("Max. label:", t.get_max_label()) + print("Min. label:", t.get_min_label()) + + # Delete elements + print("\nDeleting elements 13, 10, 8, 3, 6, 14") + print( + """ + 4 + / \\ + 1 7 + \\ + 5 + """ + ) + t.remove(13) + t.remove(10) + t.remove(8) + t.remove(3) + t.remove(6) + t.remove(14) + + # Prints all the elements of the list in inorder traversal after delete + inorder_traversal_nodes = [i.label for i in t.inorder_traversal()] + print("Inorder traversal after delete:", inorder_traversal_nodes) + + # Prints all the elements of the list in preorder traversal after delete + preorder_traversal_nodes = [i.label for i in t.preorder_traversal()] + print("Preorder traversal after delete:", preorder_traversal_nodes) + + print("Max. label:", t.get_max_label()) + print("Min. label:", t.get_min_label()) + + +if __name__ == "__main__": + binary_search_tree_example() diff --git a/data_structures/binary_tree/binary_tree_maximum_path_sum.py b/data_structures/binary_tree/binary_tree_maximum_path_sum.py new file mode 100644 index 000000000000..cc8c7a3c2e55 --- /dev/null +++ b/data_structures/binary_tree/binary_tree_maximum_path_sum.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +# Leetcode Reference : https://leetcode.com/problems/binary-tree-maximum-path-sum/ +@dataclass +class TreeNode: + val: int + left: TreeNode | None = None + right: TreeNode | None = None + + +class GetMaxPathSum: + r""" + + GetMaxPathSum takes root node of a tree as initial argument. + Upon calling max_path_sum(), it returns maximum path + sum from the tree. + + # Test + + The below tree looks like this + 10 + / \ + 5 -3 + / \ \ + 3 2 11 + / \ \ + 3 -2 1 + + Result will be calculated like : 3 -> 3 -> 5 -> 10 -> -3 -> 11 + As it is the maximum path possible. + + + >>> root = TreeNode(10) + >>> root.left = TreeNode(5) + >>> root.right = TreeNode(-3) + >>> root.left.left = TreeNode(3) + >>> root.left.right = TreeNode(2) + >>> root.right.right = TreeNode(11) + >>> root.left.left.left = TreeNode(3) + >>> root.left.left.right = TreeNode(-2) + >>> root.left.right.right = TreeNode(1) + + >>> GetMaxPathSum(root).max_path_sum() + 29 + """ + + def __init__(self, root: TreeNode) -> None: + self.sum = -9999999999 + self.root = root + + def traverse(self, root: TreeNode | None) -> int: + """ + Returns maximum path sum by recursively taking max_path_sum from left + and max_path_sum from right if current Node has a left or right Node. + + :param root -> tree root: + :return int: + """ + + if root is None: + return 0 + + right_sum = max(self.traverse(root.right), 0) + left_sum = max(self.traverse(root.left), 0) + + val = root.val + right_sum + left_sum + self.sum = max(val, self.sum) + + return root.val + max(right_sum, left_sum) + + def max_path_sum(self) -> int: + """ + Driver method to get max_path_sum by calling traverse method. + :return max_path_sum: + """ + self.traverse(self.root) + return self.sum + + +def construct_tree() -> TreeNode: + r""" + The below tree + -10 + / \ + 9 20 + / \ + 15 7 + + >>> root = TreeNode(-10) + >>> root.left = TreeNode(9) + >>> root.right = TreeNode(20) + >>> root.right.left = TreeNode(15) + >>> root.right.right = TreeNode(7) + + >>> GetMaxPathSum(construct_tree()).max_path_sum() + 42 + """ + + root = TreeNode(-10) + root.left = TreeNode(9) + root.right = TreeNode(20) + root.right.left = TreeNode(15) + root.right.right = TreeNode(7) + return root + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + tree = GetMaxPathSum(construct_tree()) + print(f"{tree.max_path_sum() = }") diff --git a/data_structures/binary_tree/binary_tree_mirror.py b/data_structures/binary_tree/binary_tree_mirror.py new file mode 100644 index 000000000000..47cb7fc3917a --- /dev/null +++ b/data_structures/binary_tree/binary_tree_mirror.py @@ -0,0 +1,45 @@ +""" +Problem Description: +Given a binary tree, return its mirror. +""" + + +def binary_tree_mirror_dict(binary_tree_mirror_dictionary: dict, root: int) -> None: + if not root or root not in binary_tree_mirror_dictionary: + return + left_child, right_child = binary_tree_mirror_dictionary[root][:2] + binary_tree_mirror_dictionary[root] = [right_child, left_child] + binary_tree_mirror_dict(binary_tree_mirror_dictionary, left_child) + binary_tree_mirror_dict(binary_tree_mirror_dictionary, right_child) + + +def binary_tree_mirror(binary_tree: dict, root: int = 1) -> dict: + """ + >>> binary_tree_mirror({ 1: [2,3], 2: [4,5], 3: [6,7], 7: [8,9]}, 1) + {1: [3, 2], 2: [5, 4], 3: [7, 6], 7: [9, 8]} + >>> binary_tree_mirror({ 1: [2,3], 2: [4,5], 3: [6,7], 4: [10,11]}, 1) + {1: [3, 2], 2: [5, 4], 3: [7, 6], 4: [11, 10]} + >>> binary_tree_mirror({ 1: [2,3], 2: [4,5], 3: [6,7], 4: [10,11]}, 5) + Traceback (most recent call last): + ... + ValueError: root 5 is not present in the binary_tree + >>> binary_tree_mirror({}, 5) + Traceback (most recent call last): + ... + ValueError: binary tree cannot be empty + """ + if not binary_tree: + raise ValueError("binary tree cannot be empty") + if root not in binary_tree: + msg = f"root {root} is not present in the binary_tree" + raise ValueError(msg) + binary_tree_mirror_dictionary = dict(binary_tree) + binary_tree_mirror_dict(binary_tree_mirror_dictionary, root) + return binary_tree_mirror_dictionary + + +if __name__ == "__main__": + binary_tree = {1: [2, 3], 2: [4, 5], 3: [6, 7], 7: [8, 9]} + print(f"Binary tree: {binary_tree}") + binary_tree_mirror_dictionary = binary_tree_mirror(binary_tree, 5) + print(f"Binary tree mirror: {binary_tree_mirror_dictionary}") diff --git a/data_structures/binary_tree/binary_tree_node_sum.py b/data_structures/binary_tree/binary_tree_node_sum.py new file mode 100644 index 000000000000..066617b616c4 --- /dev/null +++ b/data_structures/binary_tree/binary_tree_node_sum.py @@ -0,0 +1,75 @@ +""" +Sum of all nodes in a binary tree. + +Python implementation: + O(n) time complexity - Recurses through :meth:`depth_first_search` + with each element. + O(n) space complexity - At any point in time maximum number of stack + frames that could be in memory is `n` +""" + +from __future__ import annotations + +from collections.abc import Iterator + + +class Node: + """ + A Node has a value variable and pointers to Nodes to its left and right. + """ + + def __init__(self, value: int) -> None: + self.value = value + self.left: Node | None = None + self.right: Node | None = None + + +class BinaryTreeNodeSum: + r""" + The below tree looks like this + 10 + / \ + 5 -3 + / / \ + 12 8 0 + + >>> tree = Node(10) + >>> sum(BinaryTreeNodeSum(tree)) + 10 + + >>> tree.left = Node(5) + >>> sum(BinaryTreeNodeSum(tree)) + 15 + + >>> tree.right = Node(-3) + >>> sum(BinaryTreeNodeSum(tree)) + 12 + + >>> tree.left.left = Node(12) + >>> sum(BinaryTreeNodeSum(tree)) + 24 + + >>> tree.right.left = Node(8) + >>> tree.right.right = Node(0) + >>> sum(BinaryTreeNodeSum(tree)) + 32 + """ + + def __init__(self, tree: Node) -> None: + self.tree = tree + + def depth_first_search(self, node: Node | None) -> int: + if node is None: + return 0 + return node.value + ( + self.depth_first_search(node.left) + self.depth_first_search(node.right) + ) + + def __iter__(self) -> Iterator[int]: + yield self.depth_first_search(self.tree) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/binary_tree/binary_tree_path_sum.py b/data_structures/binary_tree/binary_tree_path_sum.py new file mode 100644 index 000000000000..8477690c777a --- /dev/null +++ b/data_structures/binary_tree/binary_tree_path_sum.py @@ -0,0 +1,108 @@ +""" +Given the root of a binary tree and an integer target, +find the number of paths where the sum of the values +along the path equals target. + + +Leetcode reference: https://leetcode.com/problems/path-sum-iii/ +""" + +from __future__ import annotations + + +class Node: + """ + A Node has value variable and pointers to Nodes to its left and right. + """ + + def __init__(self, value: int) -> None: + self.value = value + self.left: Node | None = None + self.right: Node | None = None + + +class BinaryTreePathSum: + r""" + The below tree looks like this + 10 + / \ + 5 -3 + / \ \ + 3 2 11 + / \ \ + 3 -2 1 + + + >>> tree = Node(10) + >>> tree.left = Node(5) + >>> tree.right = Node(-3) + >>> tree.left.left = Node(3) + >>> tree.left.right = Node(2) + >>> tree.right.right = Node(11) + >>> tree.left.left.left = Node(3) + >>> tree.left.left.right = Node(-2) + >>> tree.left.right.right = Node(1) + + >>> BinaryTreePathSum().path_sum(tree, 8) + 3 + >>> BinaryTreePathSum().path_sum(tree, 7) + 2 + >>> tree.right.right = Node(10) + >>> BinaryTreePathSum().path_sum(tree, 8) + 2 + >>> BinaryTreePathSum().path_sum(None, 0) + 0 + >>> BinaryTreePathSum().path_sum(tree, 0) + 0 + + The second tree looks like this + 0 + / \ + 5 5 + + >>> tree2 = Node(0) + >>> tree2.left = Node(5) + >>> tree2.right = Node(5) + + >>> BinaryTreePathSum().path_sum(tree2, 5) + 4 + >>> BinaryTreePathSum().path_sum(tree2, -1) + 0 + >>> BinaryTreePathSum().path_sum(tree2, 0) + 1 + """ + + target: int + + def __init__(self) -> None: + self.paths = 0 + + def depth_first_search(self, node: Node | None, path_sum: int) -> None: + if node is None: + return + + if path_sum == self.target: + self.paths += 1 + + if node.left: + self.depth_first_search(node.left, path_sum + node.left.value) + if node.right: + self.depth_first_search(node.right, path_sum + node.right.value) + + def path_sum(self, node: Node | None, target: int | None = None) -> int: + if node is None: + return 0 + if target is not None: + self.target = target + + self.depth_first_search(node, node.value) + self.path_sum(node.left) + self.path_sum(node.right) + + return self.paths + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/binary_tree/binary_tree_traversals.py b/data_structures/binary_tree/binary_tree_traversals.py new file mode 100644 index 000000000000..5ba149d0cbc6 --- /dev/null +++ b/data_structures/binary_tree/binary_tree_traversals.py @@ -0,0 +1,213 @@ +from __future__ import annotations + +from collections import deque +from collections.abc import Generator +from dataclasses import dataclass + + +# https://en.wikipedia.org/wiki/Tree_traversal +@dataclass +class Node: + data: int + left: Node | None = None + right: Node | None = None + + +def make_tree() -> Node | None: + r""" + The below tree + 1 + / \ + 2 3 + / \ + 4 5 + """ + tree = Node(1) + tree.left = Node(2) + tree.right = Node(3) + tree.left.left = Node(4) + tree.left.right = Node(5) + return tree + + +def preorder(root: Node | None) -> Generator[int]: + """ + Pre-order traversal visits root node, left subtree, right subtree. + >>> list(preorder(make_tree())) + [1, 2, 4, 5, 3] + """ + if not root: + return + yield root.data + yield from preorder(root.left) + yield from preorder(root.right) + + +def postorder(root: Node | None) -> Generator[int]: + """ + Post-order traversal visits left subtree, right subtree, root node. + >>> list(postorder(make_tree())) + [4, 5, 2, 3, 1] + """ + if not root: + return + yield from postorder(root.left) + yield from postorder(root.right) + yield root.data + + +def inorder(root: Node | None) -> Generator[int]: + """ + In-order traversal visits left subtree, root node, right subtree. + >>> list(inorder(make_tree())) + [4, 2, 5, 1, 3] + """ + if not root: + return + yield from inorder(root.left) + yield root.data + yield from inorder(root.right) + + +def reverse_inorder(root: Node | None) -> Generator[int]: + """ + Reverse in-order traversal visits right subtree, root node, left subtree. + >>> list(reverse_inorder(make_tree())) + [3, 1, 5, 2, 4] + """ + if not root: + return + yield from reverse_inorder(root.right) + yield root.data + yield from reverse_inorder(root.left) + + +def height(root: Node | None) -> int: + """ + Recursive function for calculating the height of the binary tree. + >>> height(None) + 0 + >>> height(make_tree()) + 3 + """ + return (max(height(root.left), height(root.right)) + 1) if root else 0 + + +def level_order(root: Node | None) -> Generator[int]: + """ + Returns a list of nodes value from a whole binary tree in Level Order Traverse. + Level Order traverse: Visit nodes of the tree level-by-level. + >>> list(level_order(make_tree())) + [1, 2, 3, 4, 5] + """ + + if root is None: + return + + process_queue = deque([root]) + + while process_queue: + node = process_queue.popleft() + yield node.data + + if node.left: + process_queue.append(node.left) + if node.right: + process_queue.append(node.right) + + +def get_nodes_from_left_to_right(root: Node | None, level: int) -> Generator[int]: + """ + Returns a list of nodes value from a particular level: + Left to right direction of the binary tree. + >>> list(get_nodes_from_left_to_right(make_tree(), 1)) + [1] + >>> list(get_nodes_from_left_to_right(make_tree(), 2)) + [2, 3] + """ + + def populate_output(root: Node | None, level: int) -> Generator[int]: + if not root: + return + if level == 1: + yield root.data + elif level > 1: + yield from populate_output(root.left, level - 1) + yield from populate_output(root.right, level - 1) + + yield from populate_output(root, level) + + +def get_nodes_from_right_to_left(root: Node | None, level: int) -> Generator[int]: + """ + Returns a list of nodes value from a particular level: + Right to left direction of the binary tree. + >>> list(get_nodes_from_right_to_left(make_tree(), 1)) + [1] + >>> list(get_nodes_from_right_to_left(make_tree(), 2)) + [3, 2] + """ + + def populate_output(root: Node | None, level: int) -> Generator[int]: + if not root: + return + if level == 1: + yield root.data + elif level > 1: + yield from populate_output(root.right, level - 1) + yield from populate_output(root.left, level - 1) + + yield from populate_output(root, level) + + +def zigzag(root: Node | None) -> Generator[int]: + """ + ZigZag traverse: + Returns a list of nodes value from left to right and right to left, alternatively. + >>> list(zigzag(make_tree())) + [1, 3, 2, 4, 5] + """ + if root is None: + return + + flag = 0 + height_tree = height(root) + + for h in range(1, height_tree + 1): + if not flag: + yield from get_nodes_from_left_to_right(root, h) + flag = 1 + else: + yield from get_nodes_from_right_to_left(root, h) + flag = 0 + + +def main() -> None: # Main function for testing. + # Create binary tree. + root = make_tree() + + # All Traversals of the binary are as follows: + print(f"In-order Traversal: {list(inorder(root))}") + print(f"Reverse In-order Traversal: {list(reverse_inorder(root))}") + print(f"Pre-order Traversal: {list(preorder(root))}") + print(f"Post-order Traversal: {list(postorder(root))}", "\n") + + print(f"Height of Tree: {height(root)}", "\n") + + print("Complete Level Order Traversal: ") + print(f"{list(level_order(root))} \n") + + print("Level-wise order Traversal: ") + + for level in range(1, height(root) + 1): + print(f"Level {level}:", list(get_nodes_from_left_to_right(root, level=level))) + + print("\nZigZag order Traversal: ") + print(f"{list(zigzag(root))}") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/data_structures/binary_tree/diameter_of_binary_tree.py b/data_structures/binary_tree/diameter_of_binary_tree.py new file mode 100644 index 000000000000..75e5e7373323 --- /dev/null +++ b/data_structures/binary_tree/diameter_of_binary_tree.py @@ -0,0 +1,73 @@ +""" +The diameter/width of a tree is defined as the number of nodes on the longest path +between two end nodes. +""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class Node: + data: int + left: Node | None = None + right: Node | None = None + + def depth(self) -> int: + """ + >>> root = Node(1) + >>> root.depth() + 1 + >>> root.left = Node(2) + >>> root.depth() + 2 + >>> root.left.depth() + 1 + >>> root.right = Node(3) + >>> root.depth() + 2 + """ + left_depth = self.left.depth() if self.left else 0 + right_depth = self.right.depth() if self.right else 0 + return max(left_depth, right_depth) + 1 + + def diameter(self) -> int: + """ + >>> root = Node(1) + >>> root.diameter() + 1 + >>> root.left = Node(2) + >>> root.diameter() + 2 + >>> root.left.diameter() + 1 + >>> root.right = Node(3) + >>> root.diameter() + 3 + """ + left_depth = self.left.depth() if self.left else 0 + right_depth = self.right.depth() if self.right else 0 + return left_depth + right_depth + 1 + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + root = Node(1) + root.left = Node(2) + root.right = Node(3) + root.left.left = Node(4) + root.left.right = Node(5) + r""" + Constructed binary tree is + 1 + / \ + 2 3 + / \ + 4 5 + """ + print(f"{root.diameter() = }") # 4 + print(f"{root.left.diameter() = }") # 3 + print(f"{root.right.diameter() = }") # 1 diff --git a/data_structures/binary_tree/diff_views_of_binary_tree.py b/data_structures/binary_tree/diff_views_of_binary_tree.py new file mode 100644 index 000000000000..3198d8065918 --- /dev/null +++ b/data_structures/binary_tree/diff_views_of_binary_tree.py @@ -0,0 +1,210 @@ +r""" +Problem: Given root of a binary tree, return the: +1. binary-tree-right-side-view +2. binary-tree-left-side-view +3. binary-tree-top-side-view +4. binary-tree-bottom-side-view +""" + +from __future__ import annotations + +from collections import defaultdict +from dataclasses import dataclass + + +@dataclass +class TreeNode: + val: int + left: TreeNode | None = None + right: TreeNode | None = None + + +def make_tree() -> TreeNode: + """ + >>> make_tree().val + 3 + """ + return TreeNode(3, TreeNode(9), TreeNode(20, TreeNode(15), TreeNode(7))) + + +def binary_tree_right_side_view(root: TreeNode) -> list[int]: + r""" + Function returns the right side view of binary tree. + + 3 <- 3 + / \ + 9 20 <- 20 + / \ + 15 7 <- 7 + + >>> binary_tree_right_side_view(make_tree()) + [3, 20, 7] + >>> binary_tree_right_side_view(None) + [] + """ + + def depth_first_search( + root: TreeNode | None, depth: int, right_view: list[int] + ) -> None: + """ + A depth first search preorder traversal to append the values at + right side of tree. + """ + if not root: + return + + if depth == len(right_view): + right_view.append(root.val) + + depth_first_search(root.right, depth + 1, right_view) + depth_first_search(root.left, depth + 1, right_view) + + right_view: list = [] + if not root: + return right_view + + depth_first_search(root, 0, right_view) + return right_view + + +def binary_tree_left_side_view(root: TreeNode) -> list[int]: + r""" + Function returns the left side view of binary tree. + + 3 -> 3 + / \ + 9 -> 9 20 + / \ + 15 -> 15 7 + + >>> binary_tree_left_side_view(make_tree()) + [3, 9, 15] + >>> binary_tree_left_side_view(None) + [] + """ + + def depth_first_search( + root: TreeNode | None, depth: int, left_view: list[int] + ) -> None: + """ + A depth first search preorder traversal to append the values + at left side of tree. + """ + if not root: + return + + if depth == len(left_view): + left_view.append(root.val) + + depth_first_search(root.left, depth + 1, left_view) + depth_first_search(root.right, depth + 1, left_view) + + left_view: list = [] + if not root: + return left_view + + depth_first_search(root, 0, left_view) + return left_view + + +def binary_tree_top_side_view(root: TreeNode) -> list[int]: + r""" + Function returns the top side view of binary tree. + + 9 3 20 7 + ⬇ ⬇ ⬇ ⬇ + + 3 + / \ + 9 20 + / \ + 15 7 + + >>> binary_tree_top_side_view(make_tree()) + [9, 3, 20, 7] + >>> binary_tree_top_side_view(None) + [] + """ + + def breadth_first_search(root: TreeNode, top_view: list[int]) -> None: + """ + A breadth first search traversal with defaultdict ds to append + the values of tree from top view + """ + queue = [(root, 0)] + lookup = defaultdict(list) + + while queue: + first = queue.pop(0) + node, hd = first + + lookup[hd].append(node.val) + + if node.left: + queue.append((node.left, hd - 1)) + if node.right: + queue.append((node.right, hd + 1)) + + for pair in sorted(lookup.items(), key=lambda each: each[0]): + top_view.append(pair[1][0]) + + top_view: list = [] + if not root: + return top_view + + breadth_first_search(root, top_view) + return top_view + + +def binary_tree_bottom_side_view(root: TreeNode) -> list[int]: + r""" + Function returns the bottom side view of binary tree + + 3 + / \ + 9 20 + / \ + 15 7 + ↑ ↑ ↑ ↑ + 9 15 20 7 + + >>> binary_tree_bottom_side_view(make_tree()) + [9, 15, 20, 7] + >>> binary_tree_bottom_side_view(None) + [] + """ + from collections import defaultdict + + def breadth_first_search(root: TreeNode, bottom_view: list[int]) -> None: + """ + A breadth first search traversal with defaultdict ds to append + the values of tree from bottom view + """ + queue = [(root, 0)] + lookup = defaultdict(list) + + while queue: + first = queue.pop(0) + node, hd = first + lookup[hd].append(node.val) + + if node.left: + queue.append((node.left, hd - 1)) + if node.right: + queue.append((node.right, hd + 1)) + + for pair in sorted(lookup.items(), key=lambda each: each[0]): + bottom_view.append(pair[1][-1]) + + bottom_view: list = [] + if not root: + return bottom_view + + breadth_first_search(root, bottom_view) + return bottom_view + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/binary_tree/distribute_coins.py b/data_structures/binary_tree/distribute_coins.py new file mode 100644 index 000000000000..5712604cb87c --- /dev/null +++ b/data_structures/binary_tree/distribute_coins.py @@ -0,0 +1,137 @@ +""" +Author : Alexander Pantyukhin +Date : November 7, 2022 + +Task: +You are given a tree root of a binary tree with n nodes, where each node has +node.data coins. There are exactly n coins in whole tree. + +In one move, we may choose two adjacent nodes and move one coin from one node +to another. A move may be from parent to child, or from child to parent. + +Return the minimum number of moves required to make every node have exactly one coin. + +Example 1: + + 3 + / \ + 0 0 + +Result: 2 + +Example 2: + + 0 + / \ + 3 0 + +Result 3 + +leetcode: https://leetcode.com/problems/distribute-coins-in-binary-tree/ + +Implementation notes: +User depth-first search approach. + +Let n is the number of nodes in tree +Runtime: O(n) +Space: O(1) +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import NamedTuple + + +@dataclass +class TreeNode: + data: int + left: TreeNode | None = None + right: TreeNode | None = None + + +class CoinsDistribResult(NamedTuple): + moves: int + excess: int + + +def distribute_coins(root: TreeNode | None) -> int: + """ + >>> distribute_coins(TreeNode(3, TreeNode(0), TreeNode(0))) + 2 + >>> distribute_coins(TreeNode(0, TreeNode(3), TreeNode(0))) + 3 + >>> distribute_coins(TreeNode(0, TreeNode(0), TreeNode(3))) + 3 + >>> distribute_coins(None) + 0 + >>> distribute_coins(TreeNode(0, TreeNode(0), TreeNode(0))) + Traceback (most recent call last): + ... + ValueError: The nodes number should be same as the number of coins + >>> distribute_coins(TreeNode(0, TreeNode(1), TreeNode(1))) + Traceback (most recent call last): + ... + ValueError: The nodes number should be same as the number of coins + """ + + if root is None: + return 0 + + # Validation + def count_nodes(node: TreeNode | None) -> int: + """ + >>> count_nodes(None) + 0 + """ + if node is None: + return 0 + + return count_nodes(node.left) + count_nodes(node.right) + 1 + + def count_coins(node: TreeNode | None) -> int: + """ + >>> count_coins(None) + 0 + """ + if node is None: + return 0 + + return count_coins(node.left) + count_coins(node.right) + node.data + + if count_nodes(root) != count_coins(root): + raise ValueError("The nodes number should be same as the number of coins") + + # Main calculation + def get_distrib(node: TreeNode | None) -> CoinsDistribResult: + """ + >>> get_distrib(None) + namedtuple("CoinsDistribResult", "0 2") + """ + + if node is None: + return CoinsDistribResult(0, 1) + + left_distrib_moves, left_distrib_excess = get_distrib(node.left) + right_distrib_moves, right_distrib_excess = get_distrib(node.right) + + coins_to_left = 1 - left_distrib_excess + coins_to_right = 1 - right_distrib_excess + + result_moves = ( + left_distrib_moves + + right_distrib_moves + + abs(coins_to_left) + + abs(coins_to_right) + ) + result_excess = node.data - coins_to_left - coins_to_right + + return CoinsDistribResult(result_moves, result_excess) + + return get_distrib(root)[0] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/binary_tree/fenwick_tree.py b/data_structures/binary_tree/fenwick_tree.py new file mode 100644 index 000000000000..88b0873a10fb --- /dev/null +++ b/data_structures/binary_tree/fenwick_tree.py @@ -0,0 +1,247 @@ +from copy import deepcopy + + +class FenwickTree: + """ + Fenwick Tree + + More info: https://en.wikipedia.org/wiki/Fenwick_tree + """ + + def __init__(self, arr: list[int] | None = None, size: int | None = None) -> None: + """ + Constructor for the Fenwick tree + + Parameters: + arr (list): list of elements to initialize the tree with (optional) + size (int): size of the Fenwick tree (if arr is None) + """ + + if arr is None and size is not None: + self.size = size + self.tree = [0] * size + elif arr is not None: + self.init(arr) + else: + raise ValueError("Either arr or size must be specified") + + def init(self, arr: list[int]) -> None: + """ + Initialize the Fenwick tree with arr in O(N) + + Parameters: + arr (list): list of elements to initialize the tree with + + Returns: + None + + >>> a = [1, 2, 3, 4, 5] + >>> f1 = FenwickTree(a) + >>> f2 = FenwickTree(size=len(a)) + >>> for index, value in enumerate(a): + ... f2.add(index, value) + >>> f1.tree == f2.tree + True + """ + self.size = len(arr) + self.tree = deepcopy(arr) + for i in range(1, self.size): + j = self.next_(i) + if j < self.size: + self.tree[j] += self.tree[i] + + def get_array(self) -> list[int]: + """ + Get the Normal Array of the Fenwick tree in O(N) + + Returns: + list: Normal Array of the Fenwick tree + + >>> a = [i for i in range(128)] + >>> f = FenwickTree(a) + >>> f.get_array() == a + True + """ + arr = self.tree[:] + for i in range(self.size - 1, 0, -1): + j = self.next_(i) + if j < self.size: + arr[j] -= arr[i] + return arr + + @staticmethod + def next_(index: int) -> int: + return index + (index & (-index)) + + @staticmethod + def prev(index: int) -> int: + return index - (index & (-index)) + + def add(self, index: int, value: int) -> None: + """ + Add a value to index in O(lg N) + + Parameters: + index (int): index to add value to + value (int): value to add to index + + Returns: + None + + >>> f = FenwickTree([1, 2, 3, 4, 5]) + >>> f.add(0, 1) + >>> f.add(1, 2) + >>> f.add(2, 3) + >>> f.add(3, 4) + >>> f.add(4, 5) + >>> f.get_array() + [2, 4, 6, 8, 10] + """ + if index == 0: + self.tree[0] += value + return + while index < self.size: + self.tree[index] += value + index = self.next_(index) + + def update(self, index: int, value: int) -> None: + """ + Set the value of index in O(lg N) + + Parameters: + index (int): index to set value to + value (int): value to set in index + + Returns: + None + + >>> f = FenwickTree([5, 4, 3, 2, 1]) + >>> f.update(0, 1) + >>> f.update(1, 2) + >>> f.update(2, 3) + >>> f.update(3, 4) + >>> f.update(4, 5) + >>> f.get_array() + [1, 2, 3, 4, 5] + """ + self.add(index, value - self.get(index)) + + def prefix(self, right: int) -> int: + """ + Prefix sum of all elements in [0, right) in O(lg N) + + Parameters: + right (int): right bound of the query (exclusive) + + Returns: + int: sum of all elements in [0, right) + + >>> a = [i for i in range(128)] + >>> f = FenwickTree(a) + >>> res = True + >>> for i in range(len(a)): + ... res = res and f.prefix(i) == sum(a[:i]) + >>> res + True + """ + if right == 0: + return 0 + result = self.tree[0] + right -= 1 # make right inclusive + while right > 0: + result += self.tree[right] + right = self.prev(right) + return result + + def query(self, left: int, right: int) -> int: + """ + Query the sum of all elements in [left, right) in O(lg N) + + Parameters: + left (int): left bound of the query (inclusive) + right (int): right bound of the query (exclusive) + + Returns: + int: sum of all elements in [left, right) + + >>> a = [i for i in range(128)] + >>> f = FenwickTree(a) + >>> res = True + >>> for i in range(len(a)): + ... for j in range(i + 1, len(a)): + ... res = res and f.query(i, j) == sum(a[i:j]) + >>> res + True + """ + return self.prefix(right) - self.prefix(left) + + def get(self, index: int) -> int: + """ + Get value at index in O(lg N) + + Parameters: + index (int): index to get the value + + Returns: + int: Value of element at index + + >>> a = [i for i in range(128)] + >>> f = FenwickTree(a) + >>> res = True + >>> for i in range(len(a)): + ... res = res and f.get(i) == a[i] + >>> res + True + """ + return self.query(index, index + 1) + + def rank_query(self, value: int) -> int: + """ + Find the largest index with prefix(i) <= value in O(lg N) + NOTE: Requires that all values are non-negative! + + Parameters: + value (int): value to find the largest index of + + Returns: + -1: if value is smaller than all elements in prefix sum + int: largest index with prefix(i) <= value + + >>> f = FenwickTree([1, 2, 0, 3, 0, 5]) + >>> f.rank_query(0) + -1 + >>> f.rank_query(2) + 0 + >>> f.rank_query(1) + 0 + >>> f.rank_query(3) + 2 + >>> f.rank_query(5) + 2 + >>> f.rank_query(6) + 4 + >>> f.rank_query(11) + 5 + """ + value -= self.tree[0] + if value < 0: + return -1 + + j = 1 # Largest power of 2 <= size + while j * 2 < self.size: + j *= 2 + + i = 0 + + while j > 0: + if i + j < self.size and self.tree[i + j] <= value: + value -= self.tree[i + j] + i += j + j //= 2 + return i + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/binary_tree/flatten_binarytree_to_linkedlist.py b/data_structures/binary_tree/flatten_binarytree_to_linkedlist.py new file mode 100644 index 000000000000..9b2c7b9af24b --- /dev/null +++ b/data_structures/binary_tree/flatten_binarytree_to_linkedlist.py @@ -0,0 +1,139 @@ +""" +Binary Tree Flattening Algorithm + +This code defines an algorithm to flatten a binary tree into a linked list +represented using the right pointers of the tree nodes. It uses in-place +flattening and demonstrates the flattening process along with a display +function to visualize the flattened linked list. +https://www.geeksforgeeks.org/flatten-a-binary-tree-into-linked-list + +Author: Arunkumar A +Date: 04/09/2023 +""" + +from __future__ import annotations + + +class TreeNode: + """ + A TreeNode has data variable and pointers to TreeNode objects + for its left and right children. + """ + + def __init__(self, data: int) -> None: + self.data = data + self.left: TreeNode | None = None + self.right: TreeNode | None = None + + +def build_tree() -> TreeNode: + """ + Build and return a sample binary tree. + + Returns: + TreeNode: The root of the binary tree. + + Examples: + >>> root = build_tree() + >>> root.data + 1 + >>> root.left.data + 2 + >>> root.right.data + 5 + >>> root.left.left.data + 3 + >>> root.left.right.data + 4 + >>> root.right.right.data + 6 + """ + root = TreeNode(1) + root.left = TreeNode(2) + root.right = TreeNode(5) + root.left.left = TreeNode(3) + root.left.right = TreeNode(4) + root.right.right = TreeNode(6) + return root + + +def flatten(root: TreeNode | None) -> None: + """ + Flatten a binary tree into a linked list in-place, where the linked list is + represented using the right pointers of the tree nodes. + + Args: + root (TreeNode): The root of the binary tree to be flattened. + + Examples: + >>> root = TreeNode(1) + >>> root.left = TreeNode(2) + >>> root.right = TreeNode(5) + >>> root.left.left = TreeNode(3) + >>> root.left.right = TreeNode(4) + >>> root.right.right = TreeNode(6) + >>> flatten(root) + >>> root.data + 1 + >>> root.right.right is None + False + >>> root.right.right = TreeNode(3) + >>> root.right.right.right is None + True + """ + if not root: + return + + # Flatten the left subtree + flatten(root.left) + + # Save the right subtree + right_subtree = root.right + + # Make the left subtree the new right subtree + root.right = root.left + root.left = None + + # Find the end of the new right subtree + current = root + while current.right: + current = current.right + + # Append the original right subtree to the end + current.right = right_subtree + + # Flatten the updated right subtree + flatten(right_subtree) + + +def display_linked_list(root: TreeNode | None) -> None: + """ + Display the flattened linked list. + + Args: + root (TreeNode | None): The root of the flattened linked list. + + Examples: + >>> root = TreeNode(1) + >>> root.right = TreeNode(2) + >>> root.right.right = TreeNode(3) + >>> display_linked_list(root) + 1 2 3 + >>> root = None + >>> display_linked_list(root) + + """ + current = root + while current: + if current.right is None: + print(current.data, end="") + break + print(current.data, end=" ") + current = current.right + + +if __name__ == "__main__": + print("Flattened Linked List:") + root = build_tree() + flatten(root) + display_linked_list(root) diff --git a/data_structures/binary_tree/floor_and_ceiling.py b/data_structures/binary_tree/floor_and_ceiling.py new file mode 100644 index 000000000000..b464aefad3a2 --- /dev/null +++ b/data_structures/binary_tree/floor_and_ceiling.py @@ -0,0 +1,88 @@ +""" +In a binary search tree (BST): +* The floor of key 'k' is the maximum value that is smaller than or equal to 'k'. +* The ceiling of key 'k' is the minimum value that is greater than or equal to 'k'. + +Reference: +https://bit.ly/46uB0a2 + +Author : Arunkumar +Date : 14th October 2023 +""" + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass + + +@dataclass +class Node: + key: int + left: Node | None = None + right: Node | None = None + + def __iter__(self) -> Iterator[int]: + if self.left: + yield from self.left + yield self.key + if self.right: + yield from self.right + + def __len__(self) -> int: + return sum(1 for _ in self) + + +def floor_ceiling(root: Node | None, key: int) -> tuple[int | None, int | None]: + """ + Find the floor and ceiling values for a given key in a Binary Search Tree (BST). + + Args: + root: The root of the binary search tree. + key: The key for which to find the floor and ceiling. + + Returns: + A tuple containing the floor and ceiling values, respectively. + + Examples: + >>> root = Node(10) + >>> root.left = Node(5) + >>> root.right = Node(20) + >>> root.left.left = Node(3) + >>> root.left.right = Node(7) + >>> root.right.left = Node(15) + >>> root.right.right = Node(25) + >>> tuple(root) + (3, 5, 7, 10, 15, 20, 25) + >>> floor_ceiling(root, 8) + (7, 10) + >>> floor_ceiling(root, 14) + (10, 15) + >>> floor_ceiling(root, -1) + (None, 3) + >>> floor_ceiling(root, 30) + (25, None) + """ + floor_val = None + ceiling_val = None + + while root: + if root.key == key: + floor_val = root.key + ceiling_val = root.key + break + + if key < root.key: + ceiling_val = root.key + root = root.left + else: + floor_val = root.key + root = root.right + + return floor_val, ceiling_val + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/binary_tree/inorder_tree_traversal_2022.py b/data_structures/binary_tree/inorder_tree_traversal_2022.py new file mode 100644 index 000000000000..a4846a5c29a5 --- /dev/null +++ b/data_structures/binary_tree/inorder_tree_traversal_2022.py @@ -0,0 +1,82 @@ +""" +Illustrate how to implement inorder traversal in binary search tree. +Author: Gurneet Singh +https://www.geeksforgeeks.org/tree-traversals-inorder-preorder-and-postorder/ +""" + + +class BinaryTreeNode: + """Defining the structure of BinaryTreeNode""" + + def __init__(self, data: int) -> None: + self.data = data + self.left_child: BinaryTreeNode | None = None + self.right_child: BinaryTreeNode | None = None + + +def insert(node: BinaryTreeNode | None, new_value: int) -> BinaryTreeNode | None: + """ + If the binary search tree is empty, make a new node and declare it as root. + >>> node_a = BinaryTreeNode(12345) + >>> node_b = insert(node_a, 67890) + >>> node_a.left_child == node_b.left_child + True + >>> node_a.right_child == node_b.right_child + True + >>> node_a.data == node_b.data + True + """ + if node is None: + node = BinaryTreeNode(new_value) + return node + + # binary search tree is not empty, + # so we will insert it into the tree + # if new_value is less than value of data in node, + # add it to left subtree and proceed recursively + if new_value < node.data: + node.left_child = insert(node.left_child, new_value) + else: + # if new_value is greater than value of data in node, + # add it to right subtree and proceed recursively + node.right_child = insert(node.right_child, new_value) + return node + + +def inorder(node: BinaryTreeNode | None) -> list[int]: # if node is None,return + """ + >>> inorder(make_tree()) + [6, 10, 14, 15, 20, 25, 60] + """ + if node: + inorder_array = inorder(node.left_child) + inorder_array = [*inorder_array, node.data] + inorder_array = inorder_array + inorder(node.right_child) + else: + inorder_array = [] + return inorder_array + + +def make_tree() -> BinaryTreeNode | None: + root = insert(None, 15) + insert(root, 10) + insert(root, 25) + insert(root, 6) + insert(root, 14) + insert(root, 20) + insert(root, 60) + return root + + +def main() -> None: + # main function + root = make_tree() + print("Printing values of binary search tree in Inorder Traversal.") + inorder(root) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/data_structures/binary_tree/is_sorted.py b/data_structures/binary_tree/is_sorted.py new file mode 100644 index 000000000000..91fc8ca82633 --- /dev/null +++ b/data_structures/binary_tree/is_sorted.py @@ -0,0 +1,98 @@ +""" +Given the root of a binary tree, determine if it is a valid binary search tree (BST). + +A valid binary search tree is defined as follows: +- The left subtree of a node contains only nodes with keys less than the node's key. +- The right subtree of a node contains only nodes with keys greater than the node's key. +- Both the left and right subtrees must also be binary search trees. + +In effect, a binary tree is a valid BST if its nodes are sorted in ascending order. +leetcode: https://leetcode.com/problems/validate-binary-search-tree/ + +If n is the number of nodes in the tree then: +Runtime: O(n) +Space: O(1) +""" + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass + + +@dataclass +class Node: + data: float + left: Node | None = None + right: Node | None = None + + def __iter__(self) -> Iterator[float]: + """ + >>> root = Node(data=2.1) + >>> list(root) + [2.1] + >>> root.left=Node(data=2.0) + >>> list(root) + [2.0, 2.1] + >>> root.right=Node(data=2.2) + >>> list(root) + [2.0, 2.1, 2.2] + """ + if self.left: + yield from self.left + yield self.data + if self.right: + yield from self.right + + @property + def is_sorted(self) -> bool: + """ + >>> Node(data='abc').is_sorted + True + >>> Node(data=2, + ... left=Node(data=1.999), + ... right=Node(data=3)).is_sorted + True + >>> Node(data=0, + ... left=Node(data=0), + ... right=Node(data=0)).is_sorted + True + >>> Node(data=0, + ... left=Node(data=-11), + ... right=Node(data=3)).is_sorted + True + >>> Node(data=5, + ... left=Node(data=1), + ... right=Node(data=4, left=Node(data=3))).is_sorted + False + >>> Node(data='a', + ... left=Node(data=1), + ... right=Node(data=4, left=Node(data=3))).is_sorted + Traceback (most recent call last): + ... + TypeError: '<' not supported between instances of 'str' and 'int' + >>> Node(data=2, + ... left=Node([]), + ... right=Node(data=4, left=Node(data=3))).is_sorted + Traceback (most recent call last): + ... + TypeError: '<' not supported between instances of 'int' and 'list' + """ + if self.left and (self.data < self.left.data or not self.left.is_sorted): + return False + return not ( + self.right and (self.data > self.right.data or not self.right.is_sorted) + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + tree = Node(data=2.1, left=Node(data=2.0), right=Node(data=2.2)) + print(f"Tree {list(tree)} is sorted: {tree.is_sorted = }.") + assert tree.right + tree.right.data = 2.0 + print(f"Tree {list(tree)} is sorted: {tree.is_sorted = }.") + tree.right.data = 2.1 + print(f"Tree {list(tree)} is sorted: {tree.is_sorted = }.") diff --git a/data_structures/binary_tree/is_sum_tree.py b/data_structures/binary_tree/is_sum_tree.py new file mode 100644 index 000000000000..846bea0fe0f2 --- /dev/null +++ b/data_structures/binary_tree/is_sum_tree.py @@ -0,0 +1,162 @@ +""" +Is a binary tree a sum tree where the value of every non-leaf node is equal to the sum +of the values of its left and right subtrees? +https://www.geeksforgeeks.org/check-if-a-given-binary-tree-is-sumtree +""" + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass + + +@dataclass +class Node: + data: int + left: Node | None = None + right: Node | None = None + + def __iter__(self) -> Iterator[int]: + """ + >>> root = Node(2) + >>> list(root) + [2] + >>> root.left = Node(1) + >>> tuple(root) + (1, 2) + """ + if self.left: + yield from self.left + yield self.data + if self.right: + yield from self.right + + def __len__(self) -> int: + """ + >>> root = Node(2) + >>> len(root) + 1 + >>> root.left = Node(1) + >>> len(root) + 2 + """ + return sum(1 for _ in self) + + @property + def is_sum_node(self) -> bool: + """ + >>> root = Node(3) + >>> root.is_sum_node + True + >>> root.left = Node(1) + >>> root.is_sum_node + False + >>> root.right = Node(2) + >>> root.is_sum_node + True + """ + if not self.left and not self.right: + return True # leaf nodes are considered sum nodes + left_sum = sum(self.left) if self.left else 0 + right_sum = sum(self.right) if self.right else 0 + return all( + ( + self.data == left_sum + right_sum, + self.left.is_sum_node if self.left else True, + self.right.is_sum_node if self.right else True, + ) + ) + + +@dataclass +class BinaryTree: + root: Node + + def __iter__(self) -> Iterator[int]: + """ + >>> list(BinaryTree.build_a_tree()) + [1, 2, 7, 11, 15, 29, 35, 40] + """ + return iter(self.root) + + def __len__(self) -> int: + """ + >>> len(BinaryTree.build_a_tree()) + 8 + """ + return len(self.root) + + def __str__(self) -> str: + """ + Returns a string representation of the inorder traversal of the binary tree. + + >>> str(list(BinaryTree.build_a_tree())) + '[1, 2, 7, 11, 15, 29, 35, 40]' + """ + return str(list(self)) + + @property + def is_sum_tree(self) -> bool: + """ + >>> BinaryTree.build_a_tree().is_sum_tree + False + >>> BinaryTree.build_a_sum_tree().is_sum_tree + True + """ + return self.root.is_sum_node + + @classmethod + def build_a_tree(cls) -> BinaryTree: + r""" + Create a binary tree with the specified structure: + 11 + / \ + 2 29 + / \ / \ + 1 7 15 40 + \ + 35 + >>> list(BinaryTree.build_a_tree()) + [1, 2, 7, 11, 15, 29, 35, 40] + """ + tree = BinaryTree(Node(11)) + root = tree.root + root.left = Node(2) + root.right = Node(29) + root.left.left = Node(1) + root.left.right = Node(7) + root.right.left = Node(15) + root.right.right = Node(40) + root.right.right.left = Node(35) + return tree + + @classmethod + def build_a_sum_tree(cls) -> BinaryTree: + r""" + Create a binary tree with the specified structure: + 26 + / \ + 10 3 + / \ \ + 4 6 3 + >>> list(BinaryTree.build_a_sum_tree()) + [4, 10, 6, 26, 3, 3] + """ + tree = BinaryTree(Node(26)) + root = tree.root + root.left = Node(10) + root.right = Node(3) + root.left.left = Node(4) + root.left.right = Node(6) + root.right.right = Node(3) + return tree + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + tree = BinaryTree.build_a_tree() + print(f"{tree} has {len(tree)} nodes and {tree.is_sum_tree = }.") + tree = BinaryTree.build_a_sum_tree() + print(f"{tree} has {len(tree)} nodes and {tree.is_sum_tree = }.") diff --git a/data_structures/binary_tree/lazy_segment_tree.py b/data_structures/binary_tree/lazy_segment_tree.py new file mode 100644 index 000000000000..c26b0619380c --- /dev/null +++ b/data_structures/binary_tree/lazy_segment_tree.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +import math + + +class SegmentTree: + def __init__(self, size: int) -> None: + self.size = size + # approximate the overall size of segment tree with given value + self.segment_tree = [0 for i in range(4 * size)] + # create array to store lazy update + self.lazy = [0 for i in range(4 * size)] + self.flag = [0 for i in range(4 * size)] # flag for lazy update + + def left(self, idx: int) -> int: + """ + >>> segment_tree = SegmentTree(15) + >>> segment_tree.left(1) + 2 + >>> segment_tree.left(2) + 4 + >>> segment_tree.left(12) + 24 + """ + return idx * 2 + + def right(self, idx: int) -> int: + """ + >>> segment_tree = SegmentTree(15) + >>> segment_tree.right(1) + 3 + >>> segment_tree.right(2) + 5 + >>> segment_tree.right(12) + 25 + """ + return idx * 2 + 1 + + def build( + self, idx: int, left_element: int, right_element: int, a: list[int] + ) -> None: + if left_element == right_element: + self.segment_tree[idx] = a[left_element - 1] + else: + mid = (left_element + right_element) // 2 + self.build(self.left(idx), left_element, mid, a) + self.build(self.right(idx), mid + 1, right_element, a) + self.segment_tree[idx] = max( + self.segment_tree[self.left(idx)], self.segment_tree[self.right(idx)] + ) + + def update( + self, idx: int, left_element: int, right_element: int, a: int, b: int, val: int + ) -> bool: + """ + update with O(lg n) (Normal segment tree without lazy update will take O(nlg n) + for each update) + + update(1, 1, size, a, b, v) for update val v to [a,b] + """ + if self.flag[idx] is True: + self.segment_tree[idx] = self.lazy[idx] + self.flag[idx] = False + if left_element != right_element: + self.lazy[self.left(idx)] = self.lazy[idx] + self.lazy[self.right(idx)] = self.lazy[idx] + self.flag[self.left(idx)] = True + self.flag[self.right(idx)] = True + + if right_element < a or left_element > b: + return True + if left_element >= a and right_element <= b: + self.segment_tree[idx] = val + if left_element != right_element: + self.lazy[self.left(idx)] = val + self.lazy[self.right(idx)] = val + self.flag[self.left(idx)] = True + self.flag[self.right(idx)] = True + return True + mid = (left_element + right_element) // 2 + self.update(self.left(idx), left_element, mid, a, b, val) + self.update(self.right(idx), mid + 1, right_element, a, b, val) + self.segment_tree[idx] = max( + self.segment_tree[self.left(idx)], self.segment_tree[self.right(idx)] + ) + return True + + # query with O(lg n) + def query( + self, idx: int, left_element: int, right_element: int, a: int, b: int + ) -> int | float: + """ + query(1, 1, size, a, b) for query max of [a,b] + >>> A = [1, 2, -4, 7, 3, -5, 6, 11, -20, 9, 14, 15, 5, 2, -8] + >>> segment_tree = SegmentTree(15) + >>> segment_tree.build(1, 1, 15, A) + >>> segment_tree.query(1, 1, 15, 4, 6) + 7 + >>> segment_tree.query(1, 1, 15, 7, 11) + 14 + >>> segment_tree.query(1, 1, 15, 7, 12) + 15 + """ + if self.flag[idx] is True: + self.segment_tree[idx] = self.lazy[idx] + self.flag[idx] = False + if left_element != right_element: + self.lazy[self.left(idx)] = self.lazy[idx] + self.lazy[self.right(idx)] = self.lazy[idx] + self.flag[self.left(idx)] = True + self.flag[self.right(idx)] = True + if right_element < a or left_element > b: + return -math.inf + if left_element >= a and right_element <= b: + return self.segment_tree[idx] + mid = (left_element + right_element) // 2 + q1 = self.query(self.left(idx), left_element, mid, a, b) + q2 = self.query(self.right(idx), mid + 1, right_element, a, b) + return max(q1, q2) + + def __str__(self) -> str: + return str([self.query(1, 1, self.size, i, i) for i in range(1, self.size + 1)]) + + +if __name__ == "__main__": + A = [1, 2, -4, 7, 3, -5, 6, 11, -20, 9, 14, 15, 5, 2, -8] + size = 15 + segt = SegmentTree(size) + segt.build(1, 1, size, A) + print(segt.query(1, 1, size, 4, 6)) + print(segt.query(1, 1, size, 7, 11)) + print(segt.query(1, 1, size, 7, 12)) + segt.update(1, 1, size, 1, 3, 111) + print(segt.query(1, 1, size, 1, 15)) + segt.update(1, 1, size, 7, 8, 235) + print(segt) diff --git a/data_structures/binary_tree/lowest_common_ancestor.py b/data_structures/binary_tree/lowest_common_ancestor.py new file mode 100644 index 000000000000..ea0e31256903 --- /dev/null +++ b/data_structures/binary_tree/lowest_common_ancestor.py @@ -0,0 +1,171 @@ +# https://en.wikipedia.org/wiki/Lowest_common_ancestor +# https://en.wikipedia.org/wiki/Breadth-first_search + +from __future__ import annotations + +from queue import Queue + + +def swap(a: int, b: int) -> tuple[int, int]: + """ + Return a tuple (b, a) when given two integers a and b + >>> swap(2,3) + (3, 2) + >>> swap(3,4) + (4, 3) + >>> swap(67, 12) + (12, 67) + >>> swap(3,-4) + (-4, 3) + """ + a ^= b + b ^= a + a ^= b + return a, b + + +def create_sparse(max_node: int, parent: list[list[int]]) -> list[list[int]]: + """ + creating sparse table which saves each nodes 2^i-th parent + >>> max_node = 6 + >>> parent = [[0, 0, 1, 1, 2, 2, 3]] + [[0] * 7 for _ in range(19)] + >>> parent = create_sparse(max_node=max_node, parent=parent) + >>> parent[0] + [0, 0, 1, 1, 2, 2, 3] + >>> parent[1] + [0, 0, 0, 0, 1, 1, 1] + >>> parent[2] + [0, 0, 0, 0, 0, 0, 0] + + >>> max_node = 1 + >>> parent = [[0, 0]] + [[0] * 2 for _ in range(19)] + >>> parent = create_sparse(max_node=max_node, parent=parent) + >>> parent[0] + [0, 0] + >>> parent[1] + [0, 0] + """ + j = 1 + while (1 << j) < max_node: + for i in range(1, max_node + 1): + parent[j][i] = parent[j - 1][parent[j - 1][i]] + j += 1 + return parent + + +# returns lca of node u,v +def lowest_common_ancestor( + u: int, v: int, level: list[int], parent: list[list[int]] +) -> int: + """ + Return the lowest common ancestor between u and v + + >>> level = [-1, 0, 1, 1, 2, 2, 2] + >>> parent = [[0, 0, 1, 1, 2, 2, 3],[0, 0, 0, 0, 1, 1, 1]] + \ + [[0] * 7 for _ in range(17)] + >>> lowest_common_ancestor(u=4, v=5, level=level, parent=parent) + 2 + >>> lowest_common_ancestor(u=4, v=6, level=level, parent=parent) + 1 + >>> lowest_common_ancestor(u=2, v=3, level=level, parent=parent) + 1 + >>> lowest_common_ancestor(u=6, v=6, level=level, parent=parent) + 6 + """ + # u must be deeper in the tree than v + if level[u] < level[v]: + u, v = swap(u, v) + # making depth of u same as depth of v + for i in range(18, -1, -1): + if level[u] - (1 << i) >= level[v]: + u = parent[i][u] + # at the same depth if u==v that mean lca is found + if u == v: + return u + # moving both nodes upwards till lca in found + for i in range(18, -1, -1): + if parent[i][u] not in [0, parent[i][v]]: + u, v = parent[i][u], parent[i][v] + # returning longest common ancestor of u,v + return parent[0][u] + + +# runs a breadth first search from root node of the tree +def breadth_first_search( + level: list[int], + parent: list[list[int]], + max_node: int, + graph: dict[int, list[int]], + root: int = 1, +) -> tuple[list[int], list[list[int]]]: + """ + sets every nodes direct parent + parent of root node is set to 0 + calculates depth of each node from root node + >>> level = [-1] * 7 + >>> parent = [[0] * 7 for _ in range(20)] + >>> graph = {1: [2, 3], 2: [4, 5], 3: [6], 4: [], 5: [], 6: []} + >>> level, parent = breadth_first_search( + ... level=level, parent=parent, max_node=6, graph=graph, root=1) + >>> level + [-1, 0, 1, 1, 2, 2, 2] + >>> parent[0] + [0, 0, 1, 1, 2, 2, 3] + + + >>> level = [-1] * 2 + >>> parent = [[0] * 2 for _ in range(20)] + >>> graph = {1: []} + >>> level, parent = breadth_first_search( + ... level=level, parent=parent, max_node=1, graph=graph, root=1) + >>> level + [-1, 0] + >>> parent[0] + [0, 0] + """ + level[root] = 0 + q: Queue[int] = Queue(maxsize=max_node) + q.put(root) + while q.qsize() != 0: + u = q.get() + for v in graph[u]: + if level[v] == -1: + level[v] = level[u] + 1 + q.put(v) + parent[0][v] = u + return level, parent + + +def main() -> None: + max_node = 13 + # initializing with 0 + parent = [[0 for _ in range(max_node + 10)] for _ in range(20)] + # initializing with -1 which means every node is unvisited + level = [-1 for _ in range(max_node + 10)] + graph: dict[int, list[int]] = { + 1: [2, 3, 4], + 2: [5], + 3: [6, 7], + 4: [8], + 5: [9, 10], + 6: [11], + 7: [], + 8: [12, 13], + 9: [], + 10: [], + 11: [], + 12: [], + 13: [], + } + level, parent = breadth_first_search(level, parent, max_node, graph, 1) + parent = create_sparse(max_node, parent) + print("LCA of node 1 and 3 is: ", lowest_common_ancestor(1, 3, level, parent)) + print("LCA of node 5 and 6 is: ", lowest_common_ancestor(5, 6, level, parent)) + print("LCA of node 7 and 11 is: ", lowest_common_ancestor(7, 11, level, parent)) + print("LCA of node 6 and 7 is: ", lowest_common_ancestor(6, 7, level, parent)) + print("LCA of node 4 and 12 is: ", lowest_common_ancestor(4, 12, level, parent)) + print("LCA of node 8 and 8 is: ", lowest_common_ancestor(8, 8, level, parent)) + + +if __name__ == "__main__": + main() diff --git a/data_structures/binary_tree/maximum_fenwick_tree.py b/data_structures/binary_tree/maximum_fenwick_tree.py new file mode 100644 index 000000000000..fd6351e288dd --- /dev/null +++ b/data_structures/binary_tree/maximum_fenwick_tree.py @@ -0,0 +1,189 @@ +class MaxFenwickTree: + """ + Maximum Fenwick Tree + + More info: https://cp-algorithms.com/data_structures/fenwick.html + --------- + >>> ft = MaxFenwickTree(5) + >>> ft.query(0, 5) + 0 + >>> ft.update(4, 100) + >>> ft.query(0, 5) + 100 + >>> ft.update(4, 0) + >>> ft.update(2, 20) + >>> ft.query(0, 5) + 20 + >>> ft.update(4, 10) + >>> ft.query(2, 5) + 20 + >>> ft.query(1, 5) + 20 + >>> ft.update(2, 0) + >>> ft.query(0, 5) + 10 + >>> ft = MaxFenwickTree(10000) + >>> ft.update(255, 30) + >>> ft.query(0, 10000) + 30 + >>> ft = MaxFenwickTree(6) + >>> ft.update(5, 1) + >>> ft.query(5, 6) + 1 + >>> ft = MaxFenwickTree(6) + >>> ft.update(0, 1000) + >>> ft.query(0, 1) + 1000 + + Updating a smaller sibling preserves the maximum, including after decreases. + >>> ft = MaxFenwickTree(8) + >>> ft.update(4, 20) + >>> ft.update(5, 1) + >>> ft.query(0, 6) + 20 + >>> ft.update(4, 0) + >>> ft.query(0, 6) + 1 + >>> ft.update(5, 0) + >>> ft.query(0, 6) + 0 + + Updating a child must also preserve the ancestor's own array value. + >>> ft = MaxFenwickTree(8) + >>> ft.update(5, 100) + >>> ft.update(4, 0) + >>> ft.query(0, 6) + 100 + """ + + def __init__(self, size: int) -> None: + """ + Create empty Maximum Fenwick Tree with specified size + + Parameters: + size: size of Array + + Returns: + None + """ + self.size = size + self.arr = [0] * size + self.tree = [0] * size + + @staticmethod + def get_next(index: int) -> int: + """ + Get next index in O(1) + """ + return index | (index + 1) + + @staticmethod + def get_prev(index: int) -> int: + """ + Get previous index in O(1) + """ + return (index & (index + 1)) - 1 + + def update(self, index: int, value: int) -> None: + """ + Set index to value in O(lg^2 N) + + Parameters: + index: index to update + value: value to set + + Returns: + None + """ + old_value = self.arr[index] + if value == old_value: + return + self.arr[index] = value + while index < self.size: + old_maximum = self.tree[index] + if value > old_maximum: + self.tree[index] = value + elif old_value == old_maximum: + current_left_border = self.get_prev(index) + 1 + maximum = self.arr[index] + if current_left_border != index: + maximum = max(0, maximum) + child = index - 1 + # These disjoint child buckets cover the rest of this bucket. + while child >= current_left_border: + maximum = max(maximum, self.tree[child]) + if maximum == old_maximum: + break + child = self.get_prev(child) + self.tree[index] = maximum + if maximum == old_maximum: + break + else: + # An unchanged bucket maximum leaves all its ancestors unchanged. + break + index = self.get_next(index) + + def query(self, left: int, right: int) -> int: + """ + Answer the query of maximum range [l, r) in O(lg^2 N) + + Parameters: + left: left index of query range (inclusive) + right: right index of query range (exclusive) + + Returns: + Maximum value of range [left, right) + """ + right -= 1 # Because of right is exclusive + result = 0 + while left <= right: + current_left = self.get_prev(right) + if left <= current_left: + result = max(result, self.tree[right]) + right = current_left + else: + result = max(result, self.arr[right]) + right -= 1 + return result + + +if __name__ == "__main__": + import doctest + import sys + from timeit import repeat + + doctest.testmod() + + # Run with --benchmark on each revision to compare the same 2,000-item workload. + if "--benchmark" in sys.argv: + size = 2000 + values = [(index * 97) % size for index in range(size)] + updates = [(index, (index * 37) % size) for index in range(size)] + updates += [(index, values[index]) for index in reversed(range(size))] + queries = [(left, size) for left in range(size)] + + # Build correct query buckets outside the timer, even on the buggy revision. + query_setup = """ +tree = MaxFenwickTree(size) +tree.arr = values[:] +tree.tree = [ + max(values[tree.get_prev(index) + 1 : index + 1]) + for index in range(size) +] +""" + for operation, statement, setup in ( + ( + "4000 updates", + "for index, value in updates: tree.update(index, value)", + "tree = MaxFenwickTree(size)", + ), + ( + "2000 queries", + "for left, right in queries: tree.query(left, right)", + query_setup, + ), + ): + timings = repeat( + statement, setup=setup, repeat=5, number=1, globals=globals() + ) + print(f"{size} items, {operation}: {min(timings):.6f} seconds (best of 5)") diff --git a/data_structures/binary_tree/maximum_sum_bst.py b/data_structures/binary_tree/maximum_sum_bst.py new file mode 100644 index 000000000000..7dadc7b95920 --- /dev/null +++ b/data_structures/binary_tree/maximum_sum_bst.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import sys +from dataclasses import dataclass + +INT_MIN = -sys.maxsize + 1 +INT_MAX = sys.maxsize - 1 + + +@dataclass +class TreeNode: + val: int = 0 + left: TreeNode | None = None + right: TreeNode | None = None + + +def max_sum_bst(root: TreeNode | None) -> int: + """ + The solution traverses a binary tree to find the maximum sum of + keys in any subtree that is a Binary Search Tree (BST). It uses + recursion to validate BST properties and calculates sums, returning + the highest sum found among all valid BST subtrees. + + >>> t1 = TreeNode(4) + >>> t1.left = TreeNode(3) + >>> t1.left.left = TreeNode(1) + >>> t1.left.right = TreeNode(2) + >>> print(max_sum_bst(t1)) + 2 + >>> t2 = TreeNode(-4) + >>> t2.left = TreeNode(-2) + >>> t2.right = TreeNode(-5) + >>> print(max_sum_bst(t2)) + 0 + >>> t3 = TreeNode(1) + >>> t3.left = TreeNode(4) + >>> t3.left.left = TreeNode(2) + >>> t3.left.right = TreeNode(4) + >>> t3.right = TreeNode(3) + >>> t3.right.left = TreeNode(2) + >>> t3.right.right = TreeNode(5) + >>> t3.right.right.left = TreeNode(4) + >>> t3.right.right.right = TreeNode(6) + >>> print(max_sum_bst(t3)) + 20 + """ + ans: int = 0 + + def solver(node: TreeNode | None) -> tuple[bool, int, int, int]: + """ + Returns the maximum sum by making recursive calls + >>> t1 = TreeNode(1) + >>> print(solver(t1)) + 1 + """ + nonlocal ans + + if not node: + return True, INT_MAX, INT_MIN, 0 # Valid BST, min, max, sum + + is_left_valid, min_left, max_left, sum_left = solver(node.left) + is_right_valid, min_right, max_right, sum_right = solver(node.right) + + if is_left_valid and is_right_valid and max_left < node.val < min_right: + total_sum = sum_left + sum_right + node.val + ans = max(ans, total_sum) + return True, min(min_left, node.val), max(max_right, node.val), total_sum + + return False, -1, -1, -1 # Not a valid BST + + solver(root) + return ans + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/binary_tree/merge_two_binary_trees.py b/data_structures/binary_tree/merge_two_binary_trees.py new file mode 100644 index 000000000000..6bbb30428704 --- /dev/null +++ b/data_structures/binary_tree/merge_two_binary_trees.py @@ -0,0 +1,94 @@ +#!/usr/local/bin/python3 +""" +Problem Description: Given two binary tree, return the merged tree. +The rule for merging is that if two nodes overlap, then put the value sum of +both nodes to the new value of the merged node. Otherwise, the NOT null node +will be used as the node of new tree. +""" + +from __future__ import annotations + + +class Node: + """ + A binary node has value variable and pointers to its left and right node. + """ + + def __init__(self, value: int = 0) -> None: + self.value = value + self.left: Node | None = None + self.right: Node | None = None + + +def merge_two_binary_trees(tree1: Node | None, tree2: Node | None) -> Node | None: + """ + Returns root node of the merged tree. + + >>> tree1 = Node(5) + >>> tree1.left = Node(6) + >>> tree1.right = Node(7) + >>> tree1.left.left = Node(2) + >>> tree2 = Node(4) + >>> tree2.left = Node(5) + >>> tree2.right = Node(8) + >>> tree2.left.right = Node(1) + >>> tree2.right.right = Node(4) + >>> merged_tree = merge_two_binary_trees(tree1, tree2) + >>> print_preorder(merged_tree) + 9 + 11 + 2 + 1 + 15 + 4 + """ + if tree1 is None: + return tree2 + if tree2 is None: + return tree1 + + tree1.value = tree1.value + tree2.value + tree1.left = merge_two_binary_trees(tree1.left, tree2.left) + tree1.right = merge_two_binary_trees(tree1.right, tree2.right) + return tree1 + + +def print_preorder(root: Node | None) -> None: + """ + Print pre-order traversal of the tree. + + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> print_preorder(root) + 1 + 2 + 3 + >>> print_preorder(root.right) + 3 + """ + if root: + print(root.value) + print_preorder(root.left) + print_preorder(root.right) + + +if __name__ == "__main__": + tree1 = Node(1) + tree1.left = Node(2) + tree1.right = Node(3) + tree1.left.left = Node(4) + + tree2 = Node(2) + tree2.left = Node(4) + tree2.right = Node(6) + tree2.left.right = Node(9) + tree2.right.right = Node(5) + + print("Tree1 is: ") + print_preorder(tree1) + print("Tree2 is: ") + print_preorder(tree2) + merged_tree = merge_two_binary_trees(tree1, tree2) + print("Merged Tree is: ") + print_preorder(merged_tree) diff --git a/data_structures/binary_tree/mirror_binary_tree.py b/data_structures/binary_tree/mirror_binary_tree.py new file mode 100644 index 000000000000..f6611d66d676 --- /dev/null +++ b/data_structures/binary_tree/mirror_binary_tree.py @@ -0,0 +1,160 @@ +""" +Given the root of a binary tree, mirror the tree, and return its root. + +Leetcode problem reference: https://leetcode.com/problems/mirror-binary-tree/ +""" + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass + + +@dataclass +class Node: + """ + A Node has value variable and pointers to Nodes to its left and right. + """ + + value: int + left: Node | None = None + right: Node | None = None + + def __iter__(self) -> Iterator[int]: + if self.left: + yield from self.left + yield self.value + if self.right: + yield from self.right + + def __len__(self) -> int: + return sum(1 for _ in self) + + def mirror(self) -> Node: + """ + Mirror the binary tree rooted at this node by swapping left and right children. + + >>> tree = Node(0) + >>> list(tree) + [0] + >>> list(tree.mirror()) + [0] + >>> tree = Node(1, Node(0), Node(3, Node(2), Node(4, None, Node(5)))) + >>> tuple(tree) + (0, 1, 2, 3, 4, 5) + >>> tuple(tree.mirror()) + (5, 4, 3, 2, 1, 0) + """ + self.left, self.right = self.right, self.left + if self.left: + self.left.mirror() + if self.right: + self.right.mirror() + return self + + +def make_tree_seven() -> Node: + r""" + Return a binary tree with 7 nodes that looks like this: + :: + + 1 + / \ + 2 3 + / \ / \ + 4 5 6 7 + + >>> tree_seven = make_tree_seven() + >>> len(tree_seven) + 7 + >>> list(tree_seven) + [4, 2, 5, 1, 6, 3, 7] + """ + tree = Node(1) + tree.left = Node(2) + tree.right = Node(3) + tree.left.left = Node(4) + tree.left.right = Node(5) + tree.right.left = Node(6) + tree.right.right = Node(7) + return tree + + +def make_tree_nine() -> Node: + r""" + Return a binary tree with 9 nodes that looks like this: + :: + + 1 + / \ + 2 3 + / \ \ + 4 5 6 + / \ \ + 7 8 9 + + >>> tree_nine = make_tree_nine() + >>> len(tree_nine) + 9 + >>> list(tree_nine) + [7, 4, 8, 2, 5, 9, 1, 3, 6] + """ + tree = Node(1) + tree.left = Node(2) + tree.right = Node(3) + tree.left.left = Node(4) + tree.left.right = Node(5) + tree.right.right = Node(6) + tree.left.left.left = Node(7) + tree.left.left.right = Node(8) + tree.left.right.right = Node(9) + return tree + + +def main() -> None: + r""" + Mirror binary trees with the given root and returns the root + + >>> tree = make_tree_nine() + >>> tuple(tree) + (7, 4, 8, 2, 5, 9, 1, 3, 6) + >>> tuple(tree.mirror()) + (6, 3, 1, 9, 5, 2, 8, 4, 7) + + nine_tree:: + + 1 + / \ + 2 3 + / \ \ + 4 5 6 + / \ \ + 7 8 9 + + The mirrored tree looks like this:: + + 1 + / \ + 3 2 + / / \ + 6 5 4 + / / \ + 9 8 7 + """ + trees = {"zero": Node(0), "seven": make_tree_seven(), "nine": make_tree_nine()} + for name, tree in trees.items(): + print(f" The {name} tree: {tuple(tree)}") + # (0,) + # (4, 2, 5, 1, 6, 3, 7) + # (7, 4, 8, 2, 5, 9, 1, 3, 6) + print(f"Mirror of {name} tree: {tuple(tree.mirror())}") + # (0,) + # (7, 3, 6, 1, 5, 2, 4) + # (6, 3, 1, 9, 5, 2, 8, 4, 7) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/data_structures/binary_tree/non_recursive_segment_tree.py b/data_structures/binary_tree/non_recursive_segment_tree.py new file mode 100644 index 000000000000..d2f36547ca69 --- /dev/null +++ b/data_structures/binary_tree/non_recursive_segment_tree.py @@ -0,0 +1,162 @@ +""" +A non-recursive Segment Tree implementation with range query and single element update, +works virtually with any list of the same type of elements with a "commutative" +combiner. + +Explanation: +https://www.geeksforgeeks.org/iterative-segment-tree-range-minimum-query/ +https://www.geeksforgeeks.org/segment-tree-efficient-implementation/ + +>>> SegmentTree([1, 2, 3], lambda a, b: a + b).query(0, 2) +6 +>>> SegmentTree([3, 1, 2], min).query(0, 2) +1 +>>> SegmentTree([2, 3, 1], max).query(0, 2) +3 +>>> st = SegmentTree([1, 5, 7, -1, 6], lambda a, b: a + b) +>>> st.update(1, -1) +>>> st.update(2, 3) +>>> st.query(1, 2) +2 +>>> st.query(1, 1) +-1 +>>> st.update(4, 1) +>>> st.query(3, 4) +0 +>>> st = SegmentTree([[1, 2, 3], [3, 2, 1], [1, 1, 1]], lambda a, b: [a[i] + b[i] for i +... in range(len(a))]) +>>> st.query(0, 1) +[4, 4, 4] +>>> st.query(1, 2) +[4, 3, 2] +>>> st.update(1, [-1, -1, -1]) +>>> st.query(1, 2) +[0, 0, 0] +>>> st.query(0, 2) +[1, 2, 3] +""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeVar, cast + +T = TypeVar("T") + + +class SegmentTree[T]: + def __init__(self, arr: list[T], fnc: Callable[[T, T], T]) -> None: + """ + Segment Tree constructor, it works just with commutative combiner. + :param arr: list of elements for the segment tree + :param fnc: commutative function for combine two elements + + >>> SegmentTree(['a', 'b', 'c'], lambda a, b: f'{a}{b}').query(0, 2) + 'abc' + >>> SegmentTree([(1, 2), (2, 3), (3, 4)], + ... lambda a, b: (a[0] + b[0], a[1] + b[1])).query(0, 2) + (6, 9) + """ + + self.N: int = len(arr) + self.st: list[T] = [cast(T, None) for _ in range(self.N)] + arr + self.fn = fnc + self.build() + + def build(self) -> None: + for p in range(self.N - 1, 0, -1): + self.st[p] = self.fn(self.st[p * 2], self.st[p * 2 + 1]) + + def update(self, p: int, v: T) -> None: + """ + Update an element in log(N) time + :param p: position to be update + :param v: new value + + >>> st = SegmentTree([3, 1, 2, 4], min) + >>> st.query(0, 3) + 1 + >>> st.update(2, -1) + >>> st.query(0, 3) + -1 + """ + p += self.N + self.st[p] = v + while p > 1: + p = p // 2 + self.st[p] = self.fn(self.st[p * 2], self.st[p * 2 + 1]) + + def query(self, left: int, right: int) -> T | None: + """ + Get range query value in log(N) time + :param left: left element index + :param right: right element index + :return: element combined in the range [left, right] + + >>> st = SegmentTree([1, 2, 3, 4], lambda a, b: a + b) + >>> st.query(0, 2) + 6 + >>> st.query(1, 2) + 5 + >>> st.query(0, 3) + 10 + >>> st.query(2, 3) + 7 + """ + left, right = left + self.N, right + self.N + + res: T | None = None + while left <= right: + if left % 2 == 1: + res = self.st[left] if res is None else self.fn(res, self.st[left]) + if right % 2 == 0: + res = self.st[right] if res is None else self.fn(res, self.st[right]) + left, right = (left + 1) // 2, (right - 1) // 2 + return res + + +if __name__ == "__main__": + from functools import reduce + + test_array = [1, 10, -2, 9, -3, 8, 4, -7, 5, 6, 11, -12] + + test_updates = { + 0: 7, + 1: 2, + 2: 6, + 3: -14, + 4: 5, + 5: 4, + 6: 7, + 7: -10, + 8: 9, + 9: 10, + 10: 12, + 11: 1, + } + + min_segment_tree = SegmentTree(test_array, min) + max_segment_tree = SegmentTree(test_array, max) + sum_segment_tree = SegmentTree(test_array, lambda a, b: a + b) + + def test_all_segments() -> None: + """ + Test all possible segments + """ + for i in range(len(test_array)): + for j in range(i, len(test_array)): + min_range = reduce(min, test_array[i : j + 1]) + max_range = reduce(max, test_array[i : j + 1]) + sum_range = reduce(lambda a, b: a + b, test_array[i : j + 1]) + assert min_range == min_segment_tree.query(i, j) + assert max_range == max_segment_tree.query(i, j) + assert sum_range == sum_segment_tree.query(i, j) + + test_all_segments() + + for index, value in test_updates.items(): + test_array[index] = value + min_segment_tree.update(index, value) + max_segment_tree.update(index, value) + sum_segment_tree.update(index, value) + test_all_segments() diff --git a/data_structures/binary_tree/number_of_possible_binary_trees.py b/data_structures/binary_tree/number_of_possible_binary_trees.py new file mode 100644 index 000000000000..b39cbafd0a61 --- /dev/null +++ b/data_structures/binary_tree/number_of_possible_binary_trees.py @@ -0,0 +1,102 @@ +""" +Hey, we are going to find an exciting number called Catalan number which is use to find +the number of possible binary search trees from tree of a given number of nodes. + +We will use the formula: t(n) = SUMMATION(i = 1 to n)t(i-1)t(n-i) + +Further details at Wikipedia: https://en.wikipedia.org/wiki/Catalan_number +""" + +""" +Our Contribution: +Basically we Create the 2 function: + 1. catalan_number(node_count: int) -> int + Returns the number of possible binary search trees for n nodes. + 2. binary_tree_count(node_count: int) -> int + Returns the number of possible binary trees for n nodes. +""" + + +def binomial_coefficient(n: int, k: int) -> int: + """ + Since Here we Find the Binomial Coefficient: + https://en.wikipedia.org/wiki/Binomial_coefficient + C(n,k) = n! / k!(n-k)! + :param n: 2 times of Number of nodes + :param k: Number of nodes + :return: Integer Value + + >>> binomial_coefficient(4, 2) + 6 + """ + result = 1 # To kept the Calculated Value + # Since C(n, k) = C(n, n-k) + k = min(k, n - k) + # Calculate C(n,k) + for i in range(k): + result *= n - i + result //= i + 1 + return result + + +def catalan_number(node_count: int) -> int: + """ + We can find Catalan number many ways but here we use Binomial Coefficient because it + does the job in O(n) + + return the Catalan number of n using 2nCn/(n+1). + :param n: number of nodes + :return: Catalan number of n nodes + + >>> catalan_number(5) + 42 + >>> catalan_number(6) + 132 + """ + return binomial_coefficient(2 * node_count, node_count) // (node_count + 1) + + +def factorial(n: int) -> int: + """ + Return the factorial of a number. + :param n: Number to find the Factorial of. + :return: Factorial of n. + + >>> import math + >>> all(factorial(i) == math.factorial(i) for i in range(10)) + True + >>> factorial(-5) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: factorial() not defined for negative values + """ + if n < 0: + raise ValueError("factorial() not defined for negative values") + result = 1 + for i in range(1, n + 1): + result *= i + return result + + +def binary_tree_count(node_count: int) -> int: + """ + Return the number of possible of binary trees. + :param n: number of nodes + :return: Number of possible binary trees + + >>> binary_tree_count(5) + 5040 + >>> binary_tree_count(6) + 95040 + """ + return catalan_number(node_count) * factorial(node_count) + + +if __name__ == "__main__": + node_count = int(input("Enter the number of nodes: ").strip() or 0) + if node_count <= 0: + raise ValueError("We need some nodes to work with.") + print( + f"Given {node_count} nodes, there are {binary_tree_count(node_count)} " + f"binary trees and {catalan_number(node_count)} binary search trees." + ) diff --git a/data_structures/binary_tree/persistent_segment_tree.py b/data_structures/binary_tree/persistent_segment_tree.py new file mode 100644 index 000000000000..8d52f4599484 --- /dev/null +++ b/data_structures/binary_tree/persistent_segment_tree.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class Node: + value: int + left: Node | None = None + right: Node | None = None + + +class PersistentSegmentTree: + def __init__(self, arr: list[int]) -> None: + self.n: int = len(arr) + self.roots: list[Node] = [] + self.roots.append(self._build(arr, 0, self.n - 1)) + + def _build(self, arr: list[int], start: int, end: int) -> Node: + """ + Builds a segment tree from the provided array. + + >>> pst = PersistentSegmentTree([1, 2, 3, 4]) + >>> root = pst._build([1, 2, 3, 4], 0, 3) + >>> root.value # Sum of the whole array + 10 + >>> root.left.value # Sum of the left half + 3 + >>> root.right.value # Sum of the right half + 7 + """ + if start == end: + return Node(arr[start]) + mid = (start + end) // 2 + left = self._build(arr, start, mid) + right = self._build(arr, mid + 1, end) + return Node(value=left.value + right.value, left=left, right=right) + + def update(self, version: int, index: int, value: int) -> int: + """ + Updates the value at the given index and returns the new version. + + >>> pst = PersistentSegmentTree([1, 2, 3, 4]) + >>> version_1 = pst.update(0, 1, 5) # Update index 1 to 5 + >>> pst.query(version_1, 0, 3) # Query sum of all elements in new version + 13 + >>> pst.query(0, 0, 3) # Original version remains unchanged + 10 + >>> version_2 = pst.update(version_1, 3, 6) # Update index 3 to 6 in version_1 + >>> pst.query(version_2, 0, 3) # Query sum of all elements in newest version + 15 + """ + new_root = self._update(self.roots[version], 0, self.n - 1, index, value) + self.roots.append(new_root) + return len(self.roots) - 1 + + def _update(self, node: Node, start: int, end: int, index: int, value: int) -> Node: + """ + Update the node for the specified index and value and return the new node. + + >>> pst = PersistentSegmentTree([1, 2, 3, 4]) + >>> old_root = pst.roots[0] + >>> new_root = pst._update(old_root, 0, 3, 1, 5) # Update index 1 to 5 + >>> new_root.value # New sum after update + 13 + >>> old_root.value # Old root remains unchanged + 10 + >>> new_root.left.value # Updated left child + 6 + >>> new_root.right.value # Right child remains the same + 7 + """ + if start == end: + return Node(value) + + mid = (start + end) // 2 + new_node = Node(0) + + if index <= mid: + new_node.left = self._update(node.left, start, mid, index, value) + new_node.right = node.right # Ensure right node is the same as the original + else: + new_node.left = node.left # Ensure left node is the same as the original + new_node.right = self._update(node.right, mid + 1, end, index, value) + + new_node.value = new_node.left.value + ( + new_node.right.value if new_node.right else 0 + ) + + return new_node + + def query(self, version: int, left: int, right: int) -> int: + """ + Queries the sum in the given range for the specified version. + + >>> pst = PersistentSegmentTree([1, 2, 3, 4]) + >>> pst.query(0, 0, 3) # Sum of all elements in original version + 10 + >>> pst.query(0, 1, 2) # Sum of elements at index 1 and 2 in original version + 5 + >>> version_1 = pst.update(0, 1, 5) # Update index 1 to 5 + >>> pst.query(version_1, 0, 3) # Sum of all elements in new version + 13 + >>> pst.query(version_1, 1, 2) # Sum of elements at index 1 and 2 + 8 + """ + return self._query(self.roots[version], 0, self.n - 1, left, right) + + def _query(self, node: Node, start: int, end: int, left: int, right: int) -> int: + """ + Queries the sum of values in the range [left, right] for the given node. + + >>> pst = PersistentSegmentTree([1, 2, 3, 4]) + >>> root = pst.roots[0] + >>> pst._query(root, 0, 3, 1, 2) # Sum of elements at index 1 and 2 + 5 + >>> pst._query(root, 0, 3, 0, 3) # Sum of all elements + 10 + >>> pst._query(root, 0, 3, 2, 3) # Sum of elements at index 2 and 3 + 7 + """ + if node is None or left > end or right < start: + return 0 + if left <= start and right >= end: + return node.value + mid = (start + end) // 2 + return self._query(node.left, start, mid, left, right) + self._query( + node.right, mid + 1, end, left, right + ) + + +# Running the doctests +if __name__ == "__main__": + import doctest + + print("Running doctests...") + result = doctest.testmod() + print(f"Ran {result.attempted} tests, {result.failed} failed.") diff --git a/data_structures/binary_tree/red_black_tree.py b/data_structures/binary_tree/red_black_tree.py new file mode 100644 index 000000000000..752db1e7026c --- /dev/null +++ b/data_structures/binary_tree/red_black_tree.py @@ -0,0 +1,716 @@ +from __future__ import annotations + +from collections.abc import Iterator + + +class RedBlackTree: + """ + A Red-Black tree, which is a self-balancing BST (binary search + tree). + This tree has similar performance to AVL trees, but the balancing is + less strict, so it will perform faster for writing/deleting nodes + and slower for reading in the average case, though, because they're + both balanced binary search trees, both will get the same asymptotic + performance. + To read more about them, https://en.wikipedia.org/wiki/Red-black_tree + Unless otherwise specified, all asymptotic runtimes are specified in + terms of the size of the tree. + """ + + def __init__( + self, + label: int | None = None, + color: int = 0, + parent: RedBlackTree | None = None, + left: RedBlackTree | None = None, + right: RedBlackTree | None = None, + ) -> None: + """Initialize a new Red-Black Tree node with the given values: + label: The value associated with this node + color: 0 if black, 1 if red + parent: The parent to this node + left: This node's left child + right: This node's right child + """ + self.label = label + self.parent = parent + self.left = left + self.right = right + self.color = color + + # Here are functions which are specific to red-black trees + + def rotate_left(self) -> RedBlackTree: + """Rotate the subtree rooted at this node to the left and + returns the new root to this subtree. + Performing one rotation can be done in O(1). + """ + parent = self.parent + right = self.right + if right is None: + return self + self.right = right.left + if self.right: + self.right.parent = self + self.parent = right + right.left = self + if parent is not None: + if parent.left == self: + parent.left = right + else: + parent.right = right + right.parent = parent + return right + + def rotate_right(self) -> RedBlackTree: + """Rotate the subtree rooted at this node to the right and + returns the new root to this subtree. + Performing one rotation can be done in O(1). + """ + if self.left is None: + return self + parent = self.parent + left = self.left + self.left = left.right + if self.left: + self.left.parent = self + self.parent = left + left.right = self + if parent is not None: + if parent.right is self: + parent.right = left + else: + parent.left = left + left.parent = parent + return left + + def insert(self, label: int) -> RedBlackTree: + """Inserts label into the subtree rooted at self, performs any + rotations necessary to maintain balance, and then returns the + new root to this subtree (likely self). + This is guaranteed to run in O(log(n)) time. + """ + if self.label is None: + # Only possible with an empty tree + self.label = label + return self + if self.label == label: + return self + elif self.label > label: + if self.left: + self.left.insert(label) + else: + self.left = RedBlackTree(label, 1, self) + self.left._insert_repair() + elif self.right: + self.right.insert(label) + else: + self.right = RedBlackTree(label, 1, self) + self.right._insert_repair() + return self.parent or self + + def _insert_repair(self) -> None: + """Repair the coloring from inserting into a tree.""" + if self.parent is None: + # This node is the root, so it just needs to be black + self.color = 0 + elif color(self.parent) == 0: + # If the parent is black, then it just needs to be red + self.color = 1 + else: + uncle = self.parent.sibling + if color(uncle) == 0: + if self.is_left() and self.parent.is_right(): + self.parent.rotate_right() + if self.right: + self.right._insert_repair() + elif self.is_right() and self.parent.is_left(): + self.parent.rotate_left() + if self.left: + self.left._insert_repair() + elif self.is_left(): + if self.grandparent: + self.grandparent.rotate_right() + self.parent.color = 0 + if self.parent.right: + self.parent.right.color = 1 + else: + if self.grandparent: + self.grandparent.rotate_left() + self.parent.color = 0 + if self.parent.left: + self.parent.left.color = 1 + else: + self.parent.color = 0 + if uncle and self.grandparent: + uncle.color = 0 + self.grandparent.color = 1 + self.grandparent._insert_repair() + + def remove(self, label: int) -> RedBlackTree: + """Remove label from this tree.""" + if self.label == label: + if self.left and self.right: + # It's easier to balance a node with at most one child, + # so we replace this node with the greatest one less than + # it and remove that. + value = self.left.get_max() + if value is not None: + self.label = value + self.left.remove(value) + else: + # This node has at most one non-None child, so we don't + # need to replace + child = self.left or self.right + if self.color == 1: + # This node is red, and its child is black + # The only way this happens to a node with one child + # is if both children are None leaves. + # We can just remove this node and call it a day. + if self.parent: + if self.is_left(): + self.parent.left = None + else: + self.parent.right = None + # The node is black + elif child is None: + # This node and its child are black + if self.parent is None: + # The tree is now empty + return RedBlackTree(None) + else: + self._remove_repair() + if self.is_left(): + self.parent.left = None + else: + self.parent.right = None + self.parent = None + else: + # This node is black and its child is red + # Move the child node here and make it black + self.label = child.label + self.left = child.left + self.right = child.right + if self.left: + self.left.parent = self + if self.right: + self.right.parent = self + elif self.label is not None and self.label > label: + if self.left: + self.left.remove(label) + elif self.right: + self.right.remove(label) + return self.parent or self + + def _remove_repair(self) -> None: + """Repair the coloring of the tree that may have been messed up.""" + if ( + self.parent is None + or self.sibling is None + or self.parent.sibling is None + or self.grandparent is None + ): + return + if color(self.sibling) == 1: + self.sibling.color = 0 + self.parent.color = 1 + if self.is_left(): + self.parent.rotate_left() + else: + self.parent.rotate_right() + if ( + color(self.parent) == 0 + and color(self.sibling) == 0 + and color(self.sibling.left) == 0 + and color(self.sibling.right) == 0 + ): + self.sibling.color = 1 + self.parent._remove_repair() + return + if ( + color(self.parent) == 1 + and color(self.sibling) == 0 + and color(self.sibling.left) == 0 + and color(self.sibling.right) == 0 + ): + self.sibling.color = 1 + self.parent.color = 0 + return + if ( + self.is_left() + and color(self.sibling) == 0 + and color(self.sibling.right) == 0 + and color(self.sibling.left) == 1 + ): + self.sibling.rotate_right() + self.sibling.color = 0 + if self.sibling.right: + self.sibling.right.color = 1 + if ( + self.is_right() + and color(self.sibling) == 0 + and color(self.sibling.right) == 1 + and color(self.sibling.left) == 0 + ): + self.sibling.rotate_left() + self.sibling.color = 0 + if self.sibling.left: + self.sibling.left.color = 1 + if ( + self.is_left() + and color(self.sibling) == 0 + and color(self.sibling.right) == 1 + ): + self.parent.rotate_left() + self.grandparent.color = self.parent.color + self.parent.color = 0 + self.parent.sibling.color = 0 + if ( + self.is_right() + and color(self.sibling) == 0 + and color(self.sibling.left) == 1 + ): + self.parent.rotate_right() + self.grandparent.color = self.parent.color + self.parent.color = 0 + self.parent.sibling.color = 0 + + def check_color_properties(self) -> bool: + """Check the coloring of the tree, and return True iff the tree + is colored in a way which matches these five properties: + (wording stolen from wikipedia article) + 1. Each node is either red or black. + 2. The root node is black. + 3. All leaves are black. + 4. If a node is red, then both its children are black. + 5. Every path from any node to all of its descendent NIL nodes + has the same number of black nodes. + This function runs in O(n) time, because properties 4 and 5 take + that long to check. + """ + # I assume property 1 to hold because there is nothing that can + # make the color be anything other than 0 or 1. + # Property 2 + if self.color: + # The root was red + print("Property 2") + return False + # Property 3 does not need to be checked, because None is assumed + # to be black and is all the leaves. + # Property 4 + if not self.check_coloring(): + print("Property 4") + return False + # Property 5 + if self.black_height() is None: + print("Property 5") + return False + # All properties were met + return True + + def check_coloring(self) -> bool: + """A helper function to recursively check Property 4 of a + Red-Black Tree. See check_color_properties for more info. + """ + if self.color == 1 and 1 in (color(self.left), color(self.right)): + return False + if self.left and not self.left.check_coloring(): + return False + return not (self.right and not self.right.check_coloring()) + + def black_height(self) -> int | None: + """Returns the number of black nodes from this node to the + leaves of the tree, or None if there isn't one such value (the + tree is color incorrectly). + """ + if self is None or self.left is None or self.right is None: + # If we're already at a leaf, there is no path + return 1 + left = RedBlackTree.black_height(self.left) + right = RedBlackTree.black_height(self.right) + if left is None or right is None: + # There are issues with coloring below children nodes + return None + if left != right: + # The two children have unequal depths + return None + # Return the black depth of children, plus one if this node is + # black + return left + (1 - self.color) + + # Here are functions which are general to all binary search trees + + def __contains__(self, label: int) -> bool: + """Search through the tree for label, returning True iff it is + found somewhere in the tree. + Guaranteed to run in O(log(n)) time. + """ + return self.search(label) is not None + + def search(self, label: int) -> RedBlackTree | None: + """Search through the tree for label, returning its node if + it's found, and None otherwise. + This method is guaranteed to run in O(log(n)) time. + """ + if self.label == label: + return self + elif self.label is not None and label > self.label: + if self.right is None: + return None + else: + return self.right.search(label) + elif self.left is None: + return None + else: + return self.left.search(label) + + def floor(self, label: int) -> int | None: + """Returns the largest element in this tree which is at most label. + This method is guaranteed to run in O(log(n)) time.""" + if self.label == label: + return self.label + elif self.label is not None and self.label > label: + if self.left: + return self.left.floor(label) + else: + return None + else: + if self.right: + attempt = self.right.floor(label) + if attempt is not None: + return attempt + return self.label + + def ceil(self, label: int) -> int | None: + """Returns the smallest element in this tree which is at least label. + This method is guaranteed to run in O(log(n)) time. + """ + if self.label == label: + return self.label + elif self.label is not None and self.label < label: + if self.right: + return self.right.ceil(label) + else: + return None + else: + if self.left: + attempt = self.left.ceil(label) + if attempt is not None: + return attempt + return self.label + + def get_max(self) -> int | None: + """Returns the largest element in this tree. + This method is guaranteed to run in O(log(n)) time. + """ + if self.right: + # Go as far right as possible + return self.right.get_max() + else: + return self.label + + def get_min(self) -> int | None: + """Returns the smallest element in this tree. + This method is guaranteed to run in O(log(n)) time. + """ + if self.left: + # Go as far left as possible + return self.left.get_min() + else: + return self.label + + @property + def grandparent(self) -> RedBlackTree | None: + """Get the current node's grandparent, or None if it doesn't exist.""" + if self.parent is None: + return None + else: + return self.parent.parent + + @property + def sibling(self) -> RedBlackTree | None: + """Get the current node's sibling, or None if it doesn't exist.""" + if self.parent is None: + return None + elif self.parent.left is self: + return self.parent.right + else: + return self.parent.left + + def is_left(self) -> bool: + """Returns true iff this node is the left child of its parent.""" + if self.parent is None: + return False + return self.parent.left is self + + def is_right(self) -> bool: + """Returns true iff this node is the right child of its parent.""" + if self.parent is None: + return False + return self.parent.right is self + + def __bool__(self) -> bool: + return True + + def __len__(self) -> int: + """ + Return the number of nodes in this tree. + """ + ln = 1 + if self.left: + ln += len(self.left) + if self.right: + ln += len(self.right) + return ln + + def preorder_traverse(self) -> Iterator[int | None]: + yield self.label + if self.left: + yield from self.left.preorder_traverse() + if self.right: + yield from self.right.preorder_traverse() + + def inorder_traverse(self) -> Iterator[int | None]: + if self.left: + yield from self.left.inorder_traverse() + yield self.label + if self.right: + yield from self.right.inorder_traverse() + + def postorder_traverse(self) -> Iterator[int | None]: + if self.left: + yield from self.left.postorder_traverse() + if self.right: + yield from self.right.postorder_traverse() + yield self.label + + def __repr__(self) -> str: + from pprint import pformat + + if self.left is None and self.right is None: + return f"'{self.label} {(self.color and 'red') or 'blk'}'" + return pformat( + { + f"{self.label} {(self.color and 'red') or 'blk'}": ( + self.left, + self.right, + ) + }, + indent=1, + ) + + def __eq__(self, other: object) -> bool: + """Test if two trees are equal.""" + if not isinstance(other, RedBlackTree): + return NotImplemented + if self.label == other.label: + return self.left == other.left and self.right == other.right + else: + return False + + +def color(node: RedBlackTree | None) -> int: + """Returns the color of a node, allowing for None leaves.""" + if node is None: + return 0 + else: + return node.color + + +""" +Code for testing the various +functions of the red-black tree. +""" + + +def test_rotations() -> bool: + """Test that the rotate_left and rotate_right functions work.""" + # Make a tree to test on + tree = RedBlackTree(0) + tree.left = RedBlackTree(-10, parent=tree) + tree.right = RedBlackTree(10, parent=tree) + tree.left.left = RedBlackTree(-20, parent=tree.left) + tree.left.right = RedBlackTree(-5, parent=tree.left) + tree.right.left = RedBlackTree(5, parent=tree.right) + tree.right.right = RedBlackTree(20, parent=tree.right) + # Make the right rotation + left_rot = RedBlackTree(10) + left_rot.left = RedBlackTree(0, parent=left_rot) + left_rot.left.left = RedBlackTree(-10, parent=left_rot.left) + left_rot.left.right = RedBlackTree(5, parent=left_rot.left) + left_rot.left.left.left = RedBlackTree(-20, parent=left_rot.left.left) + left_rot.left.left.right = RedBlackTree(-5, parent=left_rot.left.left) + left_rot.right = RedBlackTree(20, parent=left_rot) + tree = tree.rotate_left() + if tree != left_rot: + return False + tree = tree.rotate_right() + tree = tree.rotate_right() + # Make the left rotation + right_rot = RedBlackTree(-10) + right_rot.left = RedBlackTree(-20, parent=right_rot) + right_rot.right = RedBlackTree(0, parent=right_rot) + right_rot.right.left = RedBlackTree(-5, parent=right_rot.right) + right_rot.right.right = RedBlackTree(10, parent=right_rot.right) + right_rot.right.right.left = RedBlackTree(5, parent=right_rot.right.right) + right_rot.right.right.right = RedBlackTree(20, parent=right_rot.right.right) + return tree == right_rot + + +def test_insertion_speed() -> bool: + """Test that the tree balances inserts to O(log(n)) by doing a lot + of them. + """ + tree = RedBlackTree(-1) + for i in range(300000): + tree = tree.insert(i) + return True + + +def test_insert() -> bool: + """Test the insert() method of the tree correctly balances, colors, + and inserts. + """ + tree = RedBlackTree(0) + tree.insert(8) + tree.insert(-8) + tree.insert(4) + tree.insert(12) + tree.insert(10) + tree.insert(11) + ans = RedBlackTree(0, 0) + ans.left = RedBlackTree(-8, 0, ans) + ans.right = RedBlackTree(8, 1, ans) + ans.right.left = RedBlackTree(4, 0, ans.right) + ans.right.right = RedBlackTree(11, 0, ans.right) + ans.right.right.left = RedBlackTree(10, 1, ans.right.right) + ans.right.right.right = RedBlackTree(12, 1, ans.right.right) + return tree == ans + + +def test_insert_and_search() -> bool: + """Tests searching through the tree for values.""" + tree = RedBlackTree(0) + tree.insert(8) + tree.insert(-8) + tree.insert(4) + tree.insert(12) + tree.insert(10) + tree.insert(11) + if any(i in tree for i in (5, -6, -10, 13)): + # Found something not in there + return False + # Find all these things in there + return all(i in tree for i in (11, 12, -8, 0)) + + +def test_insert_delete() -> bool: + """Test the insert() and delete() method of the tree, verifying the + insertion and removal of elements, and the balancing of the tree. + """ + tree = RedBlackTree(0) + tree = tree.insert(-12) + tree = tree.insert(8) + tree = tree.insert(-8) + tree = tree.insert(15) + tree = tree.insert(4) + tree = tree.insert(12) + tree = tree.insert(10) + tree = tree.insert(9) + tree = tree.insert(11) + tree = tree.remove(15) + tree = tree.remove(-12) + tree = tree.remove(9) + if not tree.check_color_properties(): + return False + return list(tree.inorder_traverse()) == [-8, 0, 4, 8, 10, 11, 12] + + +def test_floor_ceil() -> bool: + """Tests the floor and ceiling functions in the tree.""" + tree = RedBlackTree(0) + tree.insert(-16) + tree.insert(16) + tree.insert(8) + tree.insert(24) + tree.insert(20) + tree.insert(22) + tuples = [(-20, None, -16), (-10, -16, 0), (8, 8, 8), (50, 24, None)] + for val, floor, ceil in tuples: + if tree.floor(val) != floor or tree.ceil(val) != ceil: + return False + return True + + +def test_min_max() -> bool: + """Tests the min and max functions in the tree.""" + tree = RedBlackTree(0) + tree.insert(-16) + tree.insert(16) + tree.insert(8) + tree.insert(24) + tree.insert(20) + tree.insert(22) + return not (tree.get_max() != 22 or tree.get_min() != -16) + + +def test_tree_traversal() -> bool: + """Tests the three different tree traversal functions.""" + tree = RedBlackTree(0) + tree = tree.insert(-16) + tree.insert(16) + tree.insert(8) + tree.insert(24) + tree.insert(20) + tree.insert(22) + if list(tree.inorder_traverse()) != [-16, 0, 8, 16, 20, 22, 24]: + return False + if list(tree.preorder_traverse()) != [0, -16, 16, 8, 22, 20, 24]: + return False + return list(tree.postorder_traverse()) == [-16, 8, 20, 24, 22, 16, 0] + + +def test_tree_chaining() -> bool: + """Tests the three different tree chaining functions.""" + tree = RedBlackTree(0) + tree = tree.insert(-16).insert(16).insert(8).insert(24).insert(20).insert(22) + if list(tree.inorder_traverse()) != [-16, 0, 8, 16, 20, 22, 24]: + return False + if list(tree.preorder_traverse()) != [0, -16, 16, 8, 22, 20, 24]: + return False + return list(tree.postorder_traverse()) == [-16, 8, 20, 24, 22, 16, 0] + + +def print_results(msg: str, passes: bool) -> None: + print(str(msg), "works!" if passes else "doesn't work :(") + + +def pytests() -> None: + assert test_rotations() + assert test_insert() + assert test_insert_and_search() + assert test_insert_delete() + assert test_floor_ceil() + assert test_tree_traversal() + assert test_tree_chaining() + + +def main() -> None: + """ + >>> pytests() + """ + print_results("Rotating right and left", test_rotations()) + print_results("Inserting", test_insert()) + print_results("Searching", test_insert_and_search()) + print_results("Deleting", test_insert_delete()) + print_results("Floor and ceil", test_floor_ceil()) + print_results("Tree traversal", test_tree_traversal()) + print_results("Tree traversal", test_tree_chaining()) + print("Testing tree balancing...") + print("This should only be a few seconds.") + test_insertion_speed() + print("Done!") + + +if __name__ == "__main__": + main() diff --git a/data_structures/binary_tree/segment_tree.py b/data_structures/binary_tree/segment_tree.py new file mode 100644 index 000000000000..b802a52157c1 --- /dev/null +++ b/data_structures/binary_tree/segment_tree.py @@ -0,0 +1,116 @@ +import math + + +class SegmentTree: + def __init__(self, a) -> None: + self.A = a + self.N = len(self.A) + self.st = [0] * ( + 4 * self.N + ) # approximate the overall size of segment tree with array N + if self.N: + self.build(1, 0, self.N - 1) + + def left(self, idx) -> int: + """ + Returns the left child index for a given index in a binary tree. + + >>> s = SegmentTree([1, 2, 3]) + >>> s.left(1) + 2 + >>> s.left(2) + 4 + """ + return idx * 2 + + def right(self, idx) -> int: + """ + Returns the right child index for a given index in a binary tree. + + >>> s = SegmentTree([1, 2, 3]) + >>> s.right(1) + 3 + >>> s.right(2) + 5 + """ + return idx * 2 + 1 + + def build(self, idx, left, right) -> None: + if left == right: + self.st[idx] = self.A[left] + else: + mid = (left + right) // 2 + self.build(self.left(idx), left, mid) + self.build(self.right(idx), mid + 1, right) + self.st[idx] = max(self.st[self.left(idx)], self.st[self.right(idx)]) + + def update(self, a, b, val) -> bool: + """ + Update the values in the segment tree in the range [a,b] with the given value. + + >>> s = SegmentTree([1, 2, 3, 4, 5]) + >>> s.update(2, 4, 10) + True + >>> s.query(1, 5) + 10 + """ + return self.update_recursive(1, 0, self.N - 1, a - 1, b - 1, val) + + def update_recursive(self, idx, left, right, a, b, val) -> bool: + """ + update(1, 1, N, a, b, v) for update val v to [a,b] + """ + if right < a or left > b: + return True + if left == right: + self.st[idx] = val + return True + mid = (left + right) // 2 + self.update_recursive(self.left(idx), left, mid, a, b, val) + self.update_recursive(self.right(idx), mid + 1, right, a, b, val) + self.st[idx] = max(self.st[self.left(idx)], self.st[self.right(idx)]) + return True + + def query(self, a, b) -> float: + """ + Query the maximum value in the range [a,b]. + + >>> s = SegmentTree([1, 2, 3, 4, 5]) + >>> s.query(1, 3) + 3 + >>> s.query(1, 5) + 5 + """ + return self.query_recursive(1, 0, self.N - 1, a - 1, b - 1) + + def query_recursive(self, idx, left, right, a, b) -> float: + """ + query(1, 1, N, a, b) for query max of [a,b] + """ + if right < a or left > b: + return -math.inf + if left >= a and right <= b: + return self.st[idx] + mid = (left + right) // 2 + q1 = self.query_recursive(self.left(idx), left, mid, a, b) + q2 = self.query_recursive(self.right(idx), mid + 1, right, a, b) + return max(q1, q2) + + def show_data(self) -> None: + show_list = [] + for i in range(1, self.N + 1): + show_list += [self.query(i, i)] + print(show_list) + + +if __name__ == "__main__": + A = [1, 2, -4, 7, 3, -5, 6, 11, -20, 9, 14, 15, 5, 2, -8] + N = 15 + segt = SegmentTree(A) + print(segt.query(4, 6)) + print(segt.query(7, 11)) + print(segt.query(7, 12)) + segt.update(1, 3, 111) + print(segt.query(1, 15)) + segt.update(7, 8, 235) + segt.show_data() diff --git a/data_structures/binary_tree/segment_tree_other.py b/data_structures/binary_tree/segment_tree_other.py new file mode 100644 index 000000000000..36f295382439 --- /dev/null +++ b/data_structures/binary_tree/segment_tree_other.py @@ -0,0 +1,237 @@ +""" +Segment_tree creates a segment tree with a given array and function, +allowing queries to be done later in log(N) time +function takes 2 values and returns a same type value +""" + +from collections.abc import Sequence +from queue import Queue + + +class SegmentTreeNode: + def __init__(self, start, end, val, left=None, right=None) -> None: + self.start = start + self.end = end + self.val = val + self.mid = (start + end) // 2 + self.left = left + self.right = right + + def __repr__(self) -> str: + return f"SegmentTreeNode(start={self.start}, end={self.end}, val={self.val})" + + +class SegmentTree: + """ + >>> import operator + >>> num_arr = SegmentTree([2, 1, 5, 3, 4], operator.add) + >>> tuple(num_arr.traverse()) # doctest: +NORMALIZE_WHITESPACE + (SegmentTreeNode(start=0, end=4, val=15), + SegmentTreeNode(start=0, end=2, val=8), + SegmentTreeNode(start=3, end=4, val=7), + SegmentTreeNode(start=0, end=1, val=3), + SegmentTreeNode(start=2, end=2, val=5), + SegmentTreeNode(start=3, end=3, val=3), + SegmentTreeNode(start=4, end=4, val=4), + SegmentTreeNode(start=0, end=0, val=2), + SegmentTreeNode(start=1, end=1, val=1)) + >>> + >>> num_arr.update(1, 5) + >>> tuple(num_arr.traverse()) # doctest: +NORMALIZE_WHITESPACE + (SegmentTreeNode(start=0, end=4, val=19), + SegmentTreeNode(start=0, end=2, val=12), + SegmentTreeNode(start=3, end=4, val=7), + SegmentTreeNode(start=0, end=1, val=7), + SegmentTreeNode(start=2, end=2, val=5), + SegmentTreeNode(start=3, end=3, val=3), + SegmentTreeNode(start=4, end=4, val=4), + SegmentTreeNode(start=0, end=0, val=2), + SegmentTreeNode(start=1, end=1, val=5)) + >>> + >>> num_arr.query_range(3, 4) + 7 + >>> num_arr.query_range(2, 2) + 5 + >>> num_arr.query_range(1, 3) + 13 + >>> + >>> max_arr = SegmentTree([2, 1, 5, 3, 4], max) + >>> for node in max_arr.traverse(): + ... print(node) + ... + SegmentTreeNode(start=0, end=4, val=5) + SegmentTreeNode(start=0, end=2, val=5) + SegmentTreeNode(start=3, end=4, val=4) + SegmentTreeNode(start=0, end=1, val=2) + SegmentTreeNode(start=2, end=2, val=5) + SegmentTreeNode(start=3, end=3, val=3) + SegmentTreeNode(start=4, end=4, val=4) + SegmentTreeNode(start=0, end=0, val=2) + SegmentTreeNode(start=1, end=1, val=1) + >>> + >>> max_arr.update(1, 5) + >>> for node in max_arr.traverse(): + ... print(node) + ... + SegmentTreeNode(start=0, end=4, val=5) + SegmentTreeNode(start=0, end=2, val=5) + SegmentTreeNode(start=3, end=4, val=4) + SegmentTreeNode(start=0, end=1, val=5) + SegmentTreeNode(start=2, end=2, val=5) + SegmentTreeNode(start=3, end=3, val=3) + SegmentTreeNode(start=4, end=4, val=4) + SegmentTreeNode(start=0, end=0, val=2) + SegmentTreeNode(start=1, end=1, val=5) + >>> + >>> max_arr.query_range(3, 4) + 4 + >>> max_arr.query_range(2, 2) + 5 + >>> max_arr.query_range(1, 3) + 5 + >>> + >>> min_arr = SegmentTree([2, 1, 5, 3, 4], min) + >>> for node in min_arr.traverse(): + ... print(node) + ... + SegmentTreeNode(start=0, end=4, val=1) + SegmentTreeNode(start=0, end=2, val=1) + SegmentTreeNode(start=3, end=4, val=3) + SegmentTreeNode(start=0, end=1, val=1) + SegmentTreeNode(start=2, end=2, val=5) + SegmentTreeNode(start=3, end=3, val=3) + SegmentTreeNode(start=4, end=4, val=4) + SegmentTreeNode(start=0, end=0, val=2) + SegmentTreeNode(start=1, end=1, val=1) + >>> + >>> min_arr.update(1, 5) + >>> for node in min_arr.traverse(): + ... print(node) + ... + SegmentTreeNode(start=0, end=4, val=2) + SegmentTreeNode(start=0, end=2, val=2) + SegmentTreeNode(start=3, end=4, val=3) + SegmentTreeNode(start=0, end=1, val=2) + SegmentTreeNode(start=2, end=2, val=5) + SegmentTreeNode(start=3, end=3, val=3) + SegmentTreeNode(start=4, end=4, val=4) + SegmentTreeNode(start=0, end=0, val=2) + SegmentTreeNode(start=1, end=1, val=5) + >>> + >>> min_arr.query_range(3, 4) + 3 + >>> min_arr.query_range(2, 2) + 5 + >>> min_arr.query_range(1, 3) + 3 + >>> + """ + + def __init__(self, collection: Sequence, function) -> None: + self.collection = collection + self.fn = function + if self.collection: + self.root = self._build_tree(0, len(collection) - 1) + + def update(self, i, val) -> None: + """ + Update an element in log(N) time + :param i: position to be update + :param val: new value + >>> import operator + >>> num_arr = SegmentTree([2, 1, 5, 3, 4], operator.add) + >>> num_arr.update(1, 5) + >>> num_arr.query_range(1, 3) + 13 + """ + self._update_tree(self.root, i, val) + + def query_range(self, i, j): + """ + Get range query value in log(N) time + :param i: left element index + :param j: right element index + :return: element combined in the range [i, j] + >>> import operator + >>> num_arr = SegmentTree([2, 1, 5, 3, 4], operator.add) + >>> num_arr.update(1, 5) + >>> num_arr.query_range(3, 4) + 7 + >>> num_arr.query_range(2, 2) + 5 + >>> num_arr.query_range(1, 3) + 13 + >>> + """ + return self._query_range(self.root, i, j) + + def _build_tree(self, start, end): + if start == end: + return SegmentTreeNode(start, end, self.collection[start]) + mid = (start + end) // 2 + left = self._build_tree(start, mid) + right = self._build_tree(mid + 1, end) + return SegmentTreeNode(start, end, self.fn(left.val, right.val), left, right) + + def _update_tree(self, node, i, val) -> None: + if node.start == i and node.end == i: + node.val = val + return + if i <= node.mid: + self._update_tree(node.left, i, val) + else: + self._update_tree(node.right, i, val) + node.val = self.fn(node.left.val, node.right.val) + + def _query_range(self, node, i, j): + if node.start == i and node.end == j: + return node.val + + if i <= node.mid: + if j <= node.mid: + # range in left child tree + return self._query_range(node.left, i, j) + else: + # range in left child tree and right child tree + return self.fn( + self._query_range(node.left, i, node.mid), + self._query_range(node.right, node.mid + 1, j), + ) + else: + # range in right child tree + return self._query_range(node.right, i, j) + + def traverse(self): + if self.root is not None: + queue = Queue() + queue.put(self.root) + while not queue.empty(): + node = queue.get() + yield node + + if node.left is not None: + queue.put(node.left) + + if node.right is not None: + queue.put(node.right) + + +if __name__ == "__main__": + import operator + + for fn in [operator.add, max, min]: + print("*" * 50) + arr = SegmentTree([2, 1, 5, 3, 4], fn) + for node in arr.traverse(): + print(node) + print() + + arr.update(1, 5) + for node in arr.traverse(): + print(node) + print() + + print(arr.query_range(3, 4)) # 7 + print(arr.query_range(2, 2)) # 5 + print(arr.query_range(1, 3)) # 13 + print() diff --git a/data_structures/binary_tree/serialize_deserialize_binary_tree.py b/data_structures/binary_tree/serialize_deserialize_binary_tree.py new file mode 100644 index 000000000000..bdbc3b46f584 --- /dev/null +++ b/data_structures/binary_tree/serialize_deserialize_binary_tree.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass + + +@dataclass +class TreeNode: + """ + A binary tree node has a value, left child, and right child. + + Props: + value: The value of the node. + left: The left child of the node. + right: The right child of the node. + """ + + value: int = 0 + left: TreeNode | None = None + right: TreeNode | None = None + + def __post_init__(self) -> None: + if not isinstance(self.value, int): + raise TypeError("Value must be an integer.") + + def __iter__(self) -> Iterator[TreeNode]: + """ + Iterate through the tree in preorder. + + Returns: + An iterator of the tree nodes. + + >>> list(TreeNode(1)) + [1,null,null] + >>> tuple(TreeNode(1, TreeNode(2), TreeNode(3))) + (1,2,null,null,3,null,null, 2,null,null, 3,null,null) + """ + yield self + yield from self.left or () + yield from self.right or () + + def __len__(self) -> int: + """ + Count the number of nodes in the tree. + + Returns: + The number of nodes in the tree. + + >>> len(TreeNode(1)) + 1 + >>> len(TreeNode(1, TreeNode(2), TreeNode(3))) + 3 + """ + return sum(1 for _ in self) + + def __repr__(self) -> str: + """ + Represent the tree as a string. + + Returns: + A string representation of the tree. + + >>> repr(TreeNode(1)) + '1,null,null' + >>> repr(TreeNode(1, TreeNode(2), TreeNode(3))) + '1,2,null,null,3,null,null' + >>> repr(TreeNode(1, TreeNode(2), TreeNode(3, TreeNode(4), TreeNode(5)))) + '1,2,null,null,3,4,null,null,5,null,null' + """ + return f"{self.value},{self.left!r},{self.right!r}".replace("None", "null") + + @classmethod + def five_tree(cls) -> TreeNode: + """ + >>> repr(TreeNode.five_tree()) + '1,2,null,null,3,4,null,null,5,null,null' + """ + root = TreeNode(1) + root.left = TreeNode(2) + root.right = TreeNode(3) + root.right.left = TreeNode(4) + root.right.right = TreeNode(5) + return root + + +def deserialize(data: str) -> TreeNode | None: + """ + Deserialize a string to a binary tree. + + Args: + data(str): The serialized string. + + Returns: + The root of the binary tree. + + >>> root = TreeNode.five_tree() + >>> serialzed_data = repr(root) + >>> deserialized = deserialize(serialzed_data) + >>> root == deserialized + True + >>> root is deserialized # two separate trees + False + >>> root.right.right.value = 6 + >>> root == deserialized + False + >>> serialzed_data = repr(root) + >>> deserialized = deserialize(serialzed_data) + >>> root == deserialized + True + >>> deserialize("") + Traceback (most recent call last): + ... + ValueError: Data cannot be empty. + """ + + if not data: + raise ValueError("Data cannot be empty.") + + # Split the serialized string by a comma to get node values + nodes = data.split(",") + + def build_tree() -> TreeNode | None: + # Get the next value from the list + value = nodes.pop(0) + + if value == "null": + return None + + node = TreeNode(int(value)) + node.left = build_tree() # Recursively build left subtree + node.right = build_tree() # Recursively build right subtree + return node + + return build_tree() + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/binary_tree/splay_tree.py b/data_structures/binary_tree/splay_tree.py new file mode 100644 index 000000000000..c200bad761a5 --- /dev/null +++ b/data_structures/binary_tree/splay_tree.py @@ -0,0 +1,237 @@ +""" +Splay Tree - a self-adjusting binary search tree. + +A splay tree is a binary search tree with the additional property that +recently accessed elements are quick to access again. Every access (search, +insert or delete) moves the target node to the root through a sequence of +rotations called "splaying". This gives an amortized time complexity of +O(log n) per operation and makes the tree very efficient when the access +pattern has locality of reference (a small subset of keys is touched often). + +Reference: https://en.wikipedia.org/wiki/Splay_tree +""" + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass, field + + +@dataclass +class Node: + """ + A single node of a splay tree. + + The ``left`` and ``right`` children are excluded from ``repr`` so that a + node prints compactly instead of recursively dumping the whole subtree. + + >>> Node(10) + Node(key=10) + """ + + key: int + left: Node | None = field(default=None, repr=False) + right: Node | None = field(default=None, repr=False) + + +class SplayTree: + """ + A self-adjusting binary search tree. + + >>> tree = SplayTree() + >>> tree.insert(10) + >>> tree.insert(20) + >>> tree.insert(30) + >>> tree.root.key # last inserted key is splayed to the root + 30 + >>> tree.search(10) + True + >>> tree.root.key # the searched key is now the root + 10 + >>> tree.search(99) + False + >>> list(tree) + [10, 20, 30] + """ + + def __init__(self) -> None: + self.root: Node | None = None + + def _rotate_right(self, node: Node) -> Node: + """ + Perform a right rotation around ``node`` and return the new subtree root. + + node left + / \\ / \\ + left c --> a node + / \\ / \\ + a b b c + """ + left = node.left + assert left is not None + node.left = left.right + left.right = node + return left + + def _rotate_left(self, node: Node) -> Node: + """ + Perform a left rotation around ``node`` and return the new subtree root. + + node right + / \\ / \\ + a right --> node c + / \\ / \\ + b c a b + """ + right = node.right + assert right is not None + node.right = right.left + right.left = node + return right + + def _splay(self, root: Node | None, key: int) -> Node | None: + """ + Splay the node with ``key`` (or the last node on the search path if + ``key`` is absent) to the root of the subtree and return the new root. + This uses the classic bottom-up recursive formulation. + """ + if root is None or root.key == key: + return root + + if key < root.key: + if root.left is None: + return root + if key < root.left.key: + # Zig-Zig (left left) + root.left.left = self._splay(root.left.left, key) + root = self._rotate_right(root) + elif key > root.left.key: + # Zig-Zag (left right) + root.left.right = self._splay(root.left.right, key) + if root.left.right is not None: + root.left = self._rotate_left(root.left) + return root if root.left is None else self._rotate_right(root) + else: + if root.right is None: + return root + if key > root.right.key: + # Zig-Zig (right right) + root.right.right = self._splay(root.right.right, key) + root = self._rotate_left(root) + elif key < root.right.key: + # Zig-Zag (right left) + root.right.left = self._splay(root.right.left, key) + if root.right.left is not None: + root.right = self._rotate_right(root.right) + return root if root.right is None else self._rotate_left(root) + + def insert(self, key: int) -> None: + """ + Insert ``key`` into the tree and splay it to the root. + + >>> tree = SplayTree() + >>> for key in (5, 3, 8, 3): # duplicate keys are ignored + ... tree.insert(key) + >>> list(tree) + [3, 5, 8] + >>> tree.root.key # the duplicate access splays 3 back to the root + 3 + """ + if self.root is None: + self.root = Node(key) + return + + self.root = self._splay(self.root, key) + assert self.root is not None + if self.root.key == key: + return # key already present, it is now at the root + + node = Node(key) + if key < self.root.key: + node.right = self.root + node.left = self.root.left + self.root.left = None + else: + node.left = self.root + node.right = self.root.right + self.root.right = None + self.root = node + + def search(self, key: int) -> bool: + """ + Return whether ``key`` is present and splay the last accessed node. + + >>> tree = SplayTree() + >>> tree.search(1) + False + >>> for key in (40, 20, 60): + ... tree.insert(key) + >>> tree.search(20) + True + >>> tree.root.key + 20 + """ + self.root = self._splay(self.root, key) + return self.root is not None and self.root.key == key + + def delete(self, key: int) -> None: + """ + Remove ``key`` from the tree if it is present. + + >>> tree = SplayTree() + >>> for key in (10, 20, 30, 40): + ... tree.insert(key) + >>> tree.delete(20) + >>> list(tree) + [10, 30, 40] + >>> tree.delete(99) # deleting an absent key is a no-op + >>> list(tree) + [10, 30, 40] + >>> for key in (10, 30, 40): + ... tree.delete(key) + >>> list(tree) + [] + """ + if self.root is None: + return + + self.root = self._splay(self.root, key) + assert self.root is not None + if self.root.key != key: + return # key not found + + left, right = self.root.left, self.root.right + if left is None: + self.root = right + else: + # Splay the maximum of the left subtree to its root; it has no + # right child, so the right subtree can be attached there. + left = self._splay(left, key) + assert left is not None + left.right = right + self.root = left + + def __iter__(self) -> Iterator[int]: + """ + Yield the keys of the tree in ascending (in-order) order. + + >>> tree = SplayTree() + >>> for key in (7, 2, 9, 4, 1): + ... tree.insert(key) + >>> list(tree) + [1, 2, 4, 7, 9] + """ + + def in_order(node: Node | None) -> Iterator[int]: + if node is not None: + yield from in_order(node.left) + yield node.key + yield from in_order(node.right) + + yield from in_order(self.root) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/binary_tree/symmetric_tree.py b/data_structures/binary_tree/symmetric_tree.py new file mode 100644 index 000000000000..2bfeac98b2c9 --- /dev/null +++ b/data_structures/binary_tree/symmetric_tree.py @@ -0,0 +1,159 @@ +""" +Given the root of a binary tree, check whether it is a mirror of itself +(i.e., symmetric around its center). + +Leetcode reference: https://leetcode.com/problems/symmetric-tree/ +""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class Node: + """ + A Node represents an element of a binary tree, which contains: + + Attributes: + data: The value stored in the node (int). + left: Pointer to the left child node (Node or None). + right: Pointer to the right child node (Node or None). + + Example: + >>> node = Node(1, Node(2), Node(3)) + >>> node.data + 1 + >>> node.left.data + 2 + >>> node.right.data + 3 + """ + + data: int + left: Node | None = None + right: Node | None = None + + +def make_symmetric_tree() -> Node: + r""" + Create a symmetric tree for testing. + + The tree looks like this: + 1 + / \ + 2 2 + / \ / \ + 3 4 4 3 + + Returns: + Node: Root node of a symmetric tree. + + Example: + >>> tree = make_symmetric_tree() + >>> tree.data + 1 + >>> tree.left.data == tree.right.data + True + >>> tree.left.left.data == tree.right.right.data + True + """ + root = Node(1) + root.left = Node(2) + root.right = Node(2) + root.left.left = Node(3) + root.left.right = Node(4) + root.right.left = Node(4) + root.right.right = Node(3) + return root + + +def make_asymmetric_tree() -> Node: + r""" + Create an asymmetric tree for testing. + + The tree looks like this: + 1 + / \ + 2 2 + / \ / \ + 3 4 3 4 + + Returns: + Node: Root node of an asymmetric tree. + + Example: + >>> tree = make_asymmetric_tree() + >>> tree.data + 1 + >>> tree.left.data == tree.right.data + True + >>> tree.left.left.data == tree.right.right.data + False + """ + root = Node(1) + root.left = Node(2) + root.right = Node(2) + root.left.left = Node(3) + root.left.right = Node(4) + root.right.left = Node(3) + root.right.right = Node(4) + return root + + +def is_symmetric_tree(tree: Node) -> bool: + """ + Check if a binary tree is symmetric (i.e., a mirror of itself). + + Parameters: + tree: The root node of the binary tree. + + Returns: + bool: True if the tree is symmetric, False otherwise. + + Example: + >>> is_symmetric_tree(make_symmetric_tree()) + True + >>> is_symmetric_tree(make_asymmetric_tree()) + False + """ + if tree: + return is_mirror(tree.left, tree.right) + return True # An empty tree is considered symmetric. + + +def is_mirror(left: Node | None, right: Node | None) -> bool: + """ + Check if two subtrees are mirror images of each other. + + Parameters: + left: The root node of the left subtree. + right: The root node of the right subtree. + + Returns: + bool: True if the two subtrees are mirrors of each other, False otherwise. + + Example: + >>> tree1 = make_symmetric_tree() + >>> is_mirror(tree1.left, tree1.right) + True + >>> tree2 = make_asymmetric_tree() + >>> is_mirror(tree2.left, tree2.right) + False + """ + if left is None and right is None: + # Both sides are empty, which is symmetric. + return True + if left is None or right is None: + # One side is empty while the other is not, which is not symmetric. + return False + if left.data == right.data: + # The values match, so check the subtrees recursively. + return is_mirror(left.left, right.right) and is_mirror(left.right, right.left) + return False + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/data_structures/binary_tree/treap.py b/data_structures/binary_tree/treap.py new file mode 100644 index 000000000000..7f105328ccea --- /dev/null +++ b/data_structures/binary_tree/treap.py @@ -0,0 +1,180 @@ +from __future__ import annotations + +from random import random + + +class Node: + """ + Treap's node + Treap is a binary tree by value and heap by priority + """ + + def __init__(self, value: int | None = None) -> None: + self.value = value + self.prior = random() + self.left: Node | None = None + self.right: Node | None = None + + def __repr__(self) -> str: + from pprint import pformat + + if self.left is None and self.right is None: + return f"'{self.value}: {self.prior:.5}'" + else: + return pformat( + {f"{self.value}: {self.prior:.5}": (self.left, self.right)}, indent=1 + ) + + def __str__(self) -> str: + value = str(self.value) + " " + left = str(self.left or "") + right = str(self.right or "") + return value + left + right + + +def split(root: Node | None, value: int) -> tuple[Node | None, Node | None]: + """ + We split current tree into 2 trees with value: + + Left tree contains all values less than split value. + Right tree contains all values greater or equal, than split value + """ + if root is None or root.value is None: # None tree is split into 2 Nones + return None, None + elif value <= root.value: + """ + Right tree's root will be current node. + Now we split(with the same value) current node's left son + Left tree: left part of that split + Right tree's left son: right part of that split + """ + left, root.left = split(root.left, value) + return left, root + else: + """ + Just symmetric to previous case + """ + root.right, right = split(root.right, value) + return root, right + + +def merge(left: Node | None, right: Node | None) -> Node | None: + """ + We merge 2 trees into one. + Note: all left tree's values must be less than all right tree's + """ + if (not left) or (not right): # If one node is None, return the other + return left or right + elif left.prior > right.prior: + """ + Left will be root because it has more priority + Now we need to merge left's right son and right tree + """ + left.right = merge(left.right, right) + return left + else: + """ + Symmetric as well + """ + right.left = merge(left, right.left) + return right + + +def insert(root: Node | None, value: int) -> Node | None: + """ + Insert element + + Split current tree with a value into left, right, + Insert new node into the middle + Merge left, node, right into root + """ + node = Node(value) + left, right = split(root, value) + return merge(merge(left, node), right) + + +def erase(root: Node | None, value: int) -> Node | None: + """ + Erase element + + Split all nodes with values less into left, + Split all nodes with values greater into right. + Merge left, right + """ + left, right = split(root, value) + _, right = split(right, value + 1) + return merge(left, right) + + +def inorder(root: Node | None) -> None: + """ + Just recursive print of a tree + """ + if not root: # None + return + else: + inorder(root.left) + print(root.value, end=",") + inorder(root.right) + + +def interact_treap(root: Node | None, args: str) -> Node | None: + """ + Commands: + + value to add value into treap + - value to erase all nodes with value + + >>> root = interact_treap(None, "+1") + >>> inorder(root) + 1, + >>> root = interact_treap(root, "+3 +5 +17 +19 +2 +16 +4 +0") + >>> inorder(root) + 0,1,2,3,4,5,16,17,19, + >>> root = interact_treap(root, "+4 +4 +4") + >>> inorder(root) + 0,1,2,3,4,4,4,4,5,16,17,19, + >>> root = interact_treap(root, "-0") + >>> inorder(root) + 1,2,3,4,4,4,4,5,16,17,19, + >>> root = interact_treap(root, "-4") + >>> inorder(root) + 1,2,3,5,16,17,19, + >>> root = interact_treap(root, "=0") + Unknown command + """ + for arg in args.split(): + match arg[0]: + case "+": + root = insert(root, int(arg[1:])) + + case "-": + root = erase(root, int(arg[1:])) + + case _: + print("Unknown command") + + return root + + +def main() -> None: + """After each command, program prints treap""" + root = None + print( + "enter numbers to create a tree, + value to add value into treap, " + "- value to erase all nodes with value. 'q' to quit. " + ) + + args = input() + while args != "q": + root = interact_treap(root, args) + print(root) + args = input() + + print("good by!") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/data_structures/binary_tree/wavelet_tree.py b/data_structures/binary_tree/wavelet_tree.py new file mode 100644 index 000000000000..2da571e8d326 --- /dev/null +++ b/data_structures/binary_tree/wavelet_tree.py @@ -0,0 +1,210 @@ +""" +Wavelet tree is a data-structure designed to efficiently answer various range queries +for arrays. Wavelets trees are different from other binary trees in the sense that +the nodes are split based on the actual values of the elements and not on indices, +such as the with segment trees or fenwick trees. You can read more about them here: +1. https://users.dcc.uchile.cl/~jperez/papers/ioiconf16.pdf +2. https://www.youtube.com/watch?v=4aSv9PcecDw&t=811s +3. https://www.youtube.com/watch?v=CybAgVF-MMc&t=1178s +""" + +from __future__ import annotations + +test_array = [2, 1, 4, 5, 6, 0, 8, 9, 1, 2, 0, 6, 4, 2, 0, 6, 5, 3, 2, 7] + + +class Node: + def __init__(self, length: int) -> None: + self.minn: int = -1 + self.maxx: int = -1 + self.map_left: list[int] = [-1] * length + self.left: Node | None = None + self.right: Node | None = None + + def __repr__(self) -> str: + """ + >>> node = Node(length=27) + >>> repr(node) + 'Node(min_value=-1 max_value=-1)' + >>> repr(node) == str(node) + True + """ + return f"Node(min_value={self.minn} max_value={self.maxx})" + + +def build_tree(arr: list[int]) -> Node | None: + """ + Builds the tree for arr and returns the root + of the constructed tree + + >>> build_tree(test_array) + Node(min_value=0 max_value=9) + """ + root = Node(len(arr)) + root.minn, root.maxx = min(arr), max(arr) + # Leaf node case where the node contains only one unique value + if root.minn == root.maxx: + return root + """ + Take the mean of min and max element of arr as the pivot and + partition arr into left_arr and right_arr with all elements <= pivot in the + left_arr and the rest in right_arr, maintaining the order of the elements, + then recursively build trees for left_arr and right_arr + """ + pivot = (root.minn + root.maxx) // 2 + + left_arr: list[int] = [] + right_arr: list[int] = [] + + for index, num in enumerate(arr): + if num <= pivot: + left_arr.append(num) + else: + right_arr.append(num) + root.map_left[index] = len(left_arr) + root.left = build_tree(left_arr) + root.right = build_tree(right_arr) + return root + + +def rank_till_index(node: Node | None, num: int, index: int) -> int: + """ + Returns the number of occurrences of num in interval [0, index] in the list + + >>> root = build_tree(test_array) + >>> rank_till_index(root, 6, 6) + 1 + >>> rank_till_index(root, 2, 0) + 1 + >>> rank_till_index(root, 1, 10) + 2 + >>> rank_till_index(root, 17, 7) + 0 + >>> rank_till_index(root, 0, 9) + 1 + """ + if index < 0 or node is None: + return 0 + # Leaf node cases + if node.minn == node.maxx: + return index + 1 if node.minn == num else 0 + pivot = (node.minn + node.maxx) // 2 + if num <= pivot: + # go the left subtree and map index to the left subtree + return rank_till_index(node.left, num, node.map_left[index] - 1) + else: + # go to the right subtree and map index to the right subtree + return rank_till_index(node.right, num, index - node.map_left[index]) + + +def rank(node: Node | None, num: int, start: int, end: int) -> int: + """ + Returns the number of occurrences of num in interval [start, end] in the list + + >>> root = build_tree(test_array) + >>> rank(root, 6, 3, 13) + 2 + >>> rank(root, 2, 0, 19) + 4 + >>> rank(root, 9, 2 ,2) + 0 + >>> rank(root, 0, 5, 10) + 2 + """ + if start > end: + return 0 + rank_till_end = rank_till_index(node, num, end) + rank_before_start = rank_till_index(node, num, start - 1) + return rank_till_end - rank_before_start + + +def quantile(node: Node | None, index: int, start: int, end: int) -> int: + """ + Returns the index'th smallest element in interval [start, end] in the list + index is 0-indexed + + >>> root = build_tree(test_array) + >>> quantile(root, 2, 2, 5) + 5 + >>> quantile(root, 5, 2, 13) + 4 + >>> quantile(root, 0, 6, 6) + 8 + >>> quantile(root, 4, 2, 5) + -1 + """ + if index > (end - start) or start > end or node is None: + return -1 + # Leaf node case + if node.minn == node.maxx: + return node.minn + # Number of elements in the left subtree in interval [start, end] + num_elements_in_left_tree = node.map_left[end] - ( + node.map_left[start - 1] if start else 0 + ) + if num_elements_in_left_tree > index: + return quantile( + node.left, + index, + (node.map_left[start - 1] if start else 0), + node.map_left[end] - 1, + ) + else: + return quantile( + node.right, + index - num_elements_in_left_tree, + start - (node.map_left[start - 1] if start else 0), + end - node.map_left[end], + ) + + +def range_counting( + node: Node | None, start: int, end: int, start_num: int, end_num: int +) -> int: + """ + Returns the number of elements in range [start_num, end_num] + in interval [start, end] in the list + + >>> root = build_tree(test_array) + >>> range_counting(root, 1, 10, 3, 7) + 3 + >>> range_counting(root, 2, 2, 1, 4) + 1 + >>> range_counting(root, 0, 19, 0, 100) + 20 + >>> range_counting(root, 1, 0, 1, 100) + 0 + >>> range_counting(root, 0, 17, 100, 1) + 0 + """ + if ( + start > end + or node is None + or start_num > end_num + or node.minn > end_num + or node.maxx < start_num + ): + return 0 + if start_num <= node.minn and node.maxx <= end_num: + return end - start + 1 + left = range_counting( + node.left, + (node.map_left[start - 1] if start else 0), + node.map_left[end] - 1, + start_num, + end_num, + ) + right = range_counting( + node.right, + start - (node.map_left[start - 1] if start else 0), + end - node.map_left[end], + start_num, + end_num, + ) + return left + right + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/disjoint_set/__init__.py b/data_structures/disjoint_set/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/disjoint_set/alternate_disjoint_set.py b/data_structures/disjoint_set/alternate_disjoint_set.py new file mode 100644 index 000000000000..5103335bc80a --- /dev/null +++ b/data_structures/disjoint_set/alternate_disjoint_set.py @@ -0,0 +1,68 @@ +""" +Implements a disjoint set using Lists and some added heuristics for efficiency +Union by Rank Heuristic and Path Compression +""" + + +class DisjointSet: + def __init__(self, set_counts: list) -> None: + """ + Initialize with a list of the number of items in each set + and with rank = 1 for each set + """ + self.set_counts = set_counts + self.max_set = max(set_counts) + num_sets = len(set_counts) + self.ranks = [1] * num_sets + self.parents = list(range(num_sets)) + + def merge(self, src: int, dst: int) -> bool: + """ + Merge two sets together using Union by rank heuristic + Return True if successful + Merge two disjoint sets + >>> A = DisjointSet([1, 1, 1]) + >>> A.merge(1, 2) + True + >>> A.merge(0, 2) + True + >>> A.merge(0, 1) + False + """ + src_parent = self.get_parent(src) + dst_parent = self.get_parent(dst) + + if src_parent == dst_parent: + return False + + if self.ranks[dst_parent] >= self.ranks[src_parent]: + self.set_counts[dst_parent] += self.set_counts[src_parent] + self.set_counts[src_parent] = 0 + self.parents[src_parent] = dst_parent + if self.ranks[dst_parent] == self.ranks[src_parent]: + self.ranks[dst_parent] += 1 + joined_set_size = self.set_counts[dst_parent] + else: + self.set_counts[src_parent] += self.set_counts[dst_parent] + self.set_counts[dst_parent] = 0 + self.parents[dst_parent] = src_parent + joined_set_size = self.set_counts[src_parent] + + self.max_set = max(self.max_set, joined_set_size) + return True + + def get_parent(self, disj_set: int) -> int: + """ + Find the Parent of a given set + >>> A = DisjointSet([1, 1, 1]) + >>> A.merge(1, 2) + True + >>> A.get_parent(0) + 0 + >>> A.get_parent(1) + 2 + """ + if self.parents[disj_set] == disj_set: + return disj_set + self.parents[disj_set] = self.get_parent(self.parents[disj_set]) + return self.parents[disj_set] diff --git a/data_structures/disjoint_set/disjoint_set.py b/data_structures/disjoint_set/disjoint_set.py new file mode 100644 index 000000000000..0a0f0181b27e --- /dev/null +++ b/data_structures/disjoint_set/disjoint_set.py @@ -0,0 +1,145 @@ +""" +Disjoint set. +Reference: https://en.wikipedia.org/wiki/Disjoint-set_data_structure +""" + +from dataclasses import dataclass + + +@dataclass +class Node: + data: int + rank: int = 0 + parent: Node | None = None + + +def make_set(x: Node) -> None: + """ + Make x as a set. + + >>> node = Node(1) + >>> make_set(node) + >>> node.parent == node + True + >>> node.rank + 0 + >>> node.data + 1 + """ + # rank is the distance from x to its' parent + # root's rank is 0 + x.rank = 0 + x.parent = x + + +def union_set(x: Node, y: Node) -> None: + """ + Union of two sets. + set with bigger rank should be parent, so that the + disjoint set tree will be more flat. + + >>> node1 = Node(1) + >>> node2 = Node(2) + >>> make_set(node1) + >>> make_set(node2) + >>> union_set(node1, node2) + >>> find_set(node1) == find_set(node2) + True + >>> # Test union of already connected nodes + >>> node3 = Node(3) + >>> make_set(node3) + >>> union_set(node1, node3) + >>> find_set(node1) == find_set(node3) + True + >>> find_set(node2) == find_set(node3) + True + """ + x, y = find_set(x), find_set(y) + if x == y: + return + + elif x.rank > y.rank: + y.parent = x + else: + x.parent = y + if x.rank == y.rank: + y.rank += 1 + + +def find_set(x: Node) -> Node: + """ + Return the parent of x + + >>> node = Node(1) + >>> make_set(node) + >>> find_set(node) == node + True + >>> node1 = Node(1) + >>> node2 = Node(2) + >>> make_set(node1) + >>> make_set(node2) + >>> union_set(node1, node2) + >>> find_set(node1) == find_set(node2) + True + >>> # Test path compression + >>> node3 = Node(3) + >>> make_set(node3) + >>> union_set(node1, node3) + >>> find_set(node1) == find_set(node3) + True + """ + if x != x.parent: + x.parent = find_set(x.parent) + return x.parent + + +def find_python_set(node: Node) -> set: + """ + Return a Python Standard Library set that contains i. + + >>> node = Node(1) + >>> find_python_set(node) + {0, 1, 2} + >>> node = Node(4) + >>> find_python_set(node) + {3, 4, 5} + >>> node = Node(6) + >>> find_python_set(node) + Traceback (most recent call last): + ... + ValueError: 6 is not in ({0, 1, 2}, {3, 4, 5}) + """ + sets = ({0, 1, 2}, {3, 4, 5}) + for s in sets: + if node.data in s: + return s + msg = f"{node.data} is not in {sets}" + raise ValueError(msg) + + +def test_disjoint_set() -> None: + """ + Test the disjoint set operations with a comprehensive example. + Creates two disjoint sets: {0, 1, 2} and {3, 4, 5} + + >>> test_disjoint_set() + """ + vertex = [Node(i) for i in range(6)] + for v in vertex: + make_set(v) + + union_set(vertex[0], vertex[1]) + union_set(vertex[1], vertex[2]) + union_set(vertex[3], vertex[4]) + union_set(vertex[3], vertex[5]) + + for node0 in vertex: + for node1 in vertex: + if find_python_set(node0).isdisjoint(find_python_set(node1)): + assert find_set(node0) != find_set(node1) + else: + assert find_set(node0) == find_set(node1) + + +if __name__ == "__main__": + test_disjoint_set() diff --git a/data_structures/hashing/__init__.py b/data_structures/hashing/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/hashing/bloom_filter.py b/data_structures/hashing/bloom_filter.py new file mode 100644 index 000000000000..eb2cb4b79c46 --- /dev/null +++ b/data_structures/hashing/bloom_filter.py @@ -0,0 +1,106 @@ +""" +See https://en.wikipedia.org/wiki/Bloom_filter + +The use of this data structure is to test membership in a set. +Compared to Python's built-in set() it is more space-efficient. +In the following example, only 8 bits of memory will be used: +>>> bloom = Bloom(size=8) + +Initially, the filter contains all zeros: +>>> bloom.bitstring +'00000000' + +When an element is added, two bits are set to 1 +since there are 2 hash functions in this implementation: +>>> "Titanic" in bloom +False +>>> bloom.add("Titanic") +>>> bloom.bitstring +'01100000' +>>> "Titanic" in bloom +True + +However, sometimes only one bit is added +because both hash functions return the same value +>>> bloom.add("Avatar") +>>> "Avatar" in bloom +True +>>> bloom.format_hash("Avatar") +'00000100' +>>> bloom.bitstring +'01100100' + +Not added elements should return False ... +>>> not_present_films = ("The Godfather", "Interstellar", "Parasite", "Pulp Fiction") +>>> { +... film: bloom.format_hash(film) for film in not_present_films +... } # doctest: +NORMALIZE_WHITESPACE +{'The Godfather': '00000101', + 'Interstellar': '00000011', + 'Parasite': '00010010', + 'Pulp Fiction': '10000100'} +>>> any(film in bloom for film in not_present_films) +False + +but sometimes there are false positives: +>>> "Ratatouille" in bloom +True +>>> bloom.format_hash("Ratatouille") +'01100000' + +The probability increases with the number of elements added. +The probability decreases with the number of bits in the bitarray. +>>> bloom.estimated_error_rate +0.140625 +>>> bloom.add("The Godfather") +>>> bloom.estimated_error_rate +0.25 +>>> bloom.bitstring +'01100101' +""" + +from hashlib import md5, sha256 + +HASH_FUNCTIONS = (sha256, md5) + + +class Bloom: + def __init__(self, size: int = 8) -> None: + self.bitarray = 0b0 + self.size = size + + def add(self, value: str) -> None: + h = self.hash_(value) + self.bitarray |= h + + def exists(self, value: str) -> bool: + h = self.hash_(value) + return (h & self.bitarray) == h + + def __contains__(self, other: str) -> bool: + return self.exists(other) + + def format_bin(self, bitarray: int) -> str: + res = bin(bitarray)[2:] + return res.zfill(self.size) + + @property + def bitstring(self) -> str: + return self.format_bin(self.bitarray) + + def hash_(self, value: str) -> int: + res = 0b0 + for func in HASH_FUNCTIONS: + position = ( + int.from_bytes(func(value.encode()).digest(), "little") % self.size + ) + res |= 2**position + return res + + def format_hash(self, value: str) -> str: + return self.format_bin(self.hash_(value)) + + @property + def estimated_error_rate(self) -> float: + n_ones = bin(self.bitarray).count("1") + return (n_ones / self.size) ** len(HASH_FUNCTIONS) diff --git a/data_structures/hashing/double_hash.py b/data_structures/hashing/double_hash.py new file mode 100644 index 000000000000..d4b46679ffbb --- /dev/null +++ b/data_structures/hashing/double_hash.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +""" +Double hashing is a collision resolving technique in Open Addressed Hash tables. +Double hashing uses the idea of applying a second hash function to key when a collision +occurs. The advantage of Double hashing is that it is one of the best form of probing, +producing a uniform distribution of records throughout a hash table. This technique +does not yield any clusters. It is one of effective method for resolving collisions. + +Double hashing can be done using: (hash1(key) + i * hash2(key)) % TABLE_SIZE +Where hash1() and hash2() are hash functions and TABLE_SIZE is size of hash table. + +Reference: https://en.wikipedia.org/wiki/Double_hashing +""" + +from .hash_table import HashTable +from .number_theory.prime_numbers import is_prime, next_prime + + +class DoubleHash(HashTable): + """ + Hash Table example with open addressing and Double Hash + """ + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + def __hash_function_2(self, value, data): + next_prime_gt = ( + next_prime(value % self.size_table) + if not is_prime(value % self.size_table) + else value % self.size_table + ) # gt = bigger than + return next_prime_gt - (data % next_prime_gt) + + def __hash_double_function(self, key, data, increment): + return (increment * self.__hash_function_2(key, data)) % self.size_table + + def _collision_resolution(self, key, data=None): + """ + Examples: + + 1. Try to add three data elements when the size is three + >>> dh = DoubleHash(3) + >>> dh.insert_data(10) + >>> dh.insert_data(20) + >>> dh.insert_data(30) + >>> dh.keys() + {1: 10, 2: 20, 0: 30} + + 2. Try to add three data elements when the size is two + >>> dh = DoubleHash(2) + >>> dh.insert_data(10) + >>> dh.insert_data(20) + >>> dh.insert_data(30) + >>> dh.keys() + {10: 10, 9: 20, 8: 30} + + 3. Try to add three data elements when the size is four + >>> dh = DoubleHash(4) + >>> dh.insert_data(10) + >>> dh.insert_data(20) + >>> dh.insert_data(30) + >>> dh.keys() + {9: 20, 10: 10, 8: 30} + """ + i = 1 + new_key = self.hash_function(data) + + while self.values[new_key] is not None and self.values[new_key] != key: + new_key = ( + self.__hash_double_function(key, data, i) + if self.balanced_factor() >= self.lim_charge + else None + ) + if new_key is None: + break + i += 1 + + return new_key + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/hashing/hash_map.py b/data_structures/hashing/hash_map.py new file mode 100644 index 000000000000..0d99e578b73e --- /dev/null +++ b/data_structures/hashing/hash_map.py @@ -0,0 +1,327 @@ +""" +Hash map with open addressing. + +https://en.wikipedia.org/wiki/Hash_table + +Another hash map implementation, with a good explanation. +Modern Dictionaries by Raymond Hettinger +https://www.youtube.com/watch?v=p33CVV29OG8 +""" + +from collections.abc import Iterator, MutableMapping +from dataclasses import dataclass +from typing import TypeVar + +KEY = TypeVar("KEY") +VAL = TypeVar("VAL") + + +@dataclass(slots=True) +class _Item[KEY, VAL]: + key: KEY + val: VAL + + +class _DeletedItem(_Item): + def __init__(self) -> None: + super().__init__(None, None) + + def __bool__(self) -> bool: + return False + + +_deleted = _DeletedItem() + + +class HashMap(MutableMapping[KEY, VAL]): + """ + Hash map with open addressing. + """ + + def __init__( + self, initial_block_size: int = 8, capacity_factor: float = 0.75 + ) -> None: + self._initial_block_size = initial_block_size + self._buckets: list[_Item | None] = [None] * initial_block_size + assert 0.0 < capacity_factor < 1.0 + self._capacity_factor = capacity_factor + self._len = 0 + + def _get_bucket_index(self, key: KEY) -> int: + return hash(key) % len(self._buckets) + + def _get_next_ind(self, ind: int) -> int: + """ + Get next index. + + Implements linear open addressing. + >>> HashMap(5)._get_next_ind(3) + 4 + >>> HashMap(5)._get_next_ind(5) + 1 + >>> HashMap(5)._get_next_ind(6) + 2 + >>> HashMap(5)._get_next_ind(9) + 0 + """ + return (ind + 1) % len(self._buckets) + + def _try_set(self, ind: int, key: KEY, val: VAL) -> bool: + """ + Try to add value to the bucket. + + If bucket is empty or key is the same, does insert and return True. + + If bucket has another key that means that we need to check next bucket. + """ + stored = self._buckets[ind] + if not stored: + # A falsy item means that bucket was never used (None) + # or was deleted (_deleted). + self._buckets[ind] = _Item(key, val) + self._len += 1 + return True + elif stored.key == key: + stored.val = val + return True + else: + return False + + def _is_full(self) -> bool: + """ + Return true if we have reached safe capacity. + + So we need to increase the number of buckets to avoid collisions. + + >>> hm = HashMap(2) + >>> hm._add_item(1, 10) + >>> hm._add_item(2, 20) + >>> hm._is_full() + True + >>> HashMap(2)._is_full() + False + """ + limit = len(self._buckets) * self._capacity_factor + return len(self) >= int(limit) + + def _is_sparse(self) -> bool: + """Return true if we need twice fewer buckets when we have now.""" + if len(self._buckets) <= self._initial_block_size: + return False + limit = len(self._buckets) * self._capacity_factor / 2 + return len(self) < limit + + def _resize(self, new_size: int) -> None: + old_buckets = self._buckets + self._buckets = [None] * new_size + self._len = 0 + for item in old_buckets: + if item: + self._add_item(item.key, item.val) + + def _size_up(self) -> None: + self._resize(len(self._buckets) * 2) + + def _size_down(self) -> None: + self._resize(len(self._buckets) // 2) + + def _iterate_buckets(self, key: KEY) -> Iterator[int]: + ind = self._get_bucket_index(key) + for _ in range(len(self._buckets)): + yield ind + ind = self._get_next_ind(ind) + + def _add_item(self, key: KEY, val: VAL) -> None: + """ + Try to add 3 elements when the size is 5 + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm._add_item(2, 20) + >>> hm._add_item(3, 30) + >>> hm + HashMap(1: 10, 2: 20, 3: 30) + + Try to add 3 elements when the size is 5 + >>> hm = HashMap(5) + >>> hm._add_item(-5, 10) + >>> hm._add_item(6, 30) + >>> hm._add_item(-7, 20) + >>> hm + HashMap(-5: 10, 6: 30, -7: 20) + + Try to add 3 elements when size is 1 + >>> hm = HashMap(1) + >>> hm._add_item(10, 13.2) + >>> hm._add_item(6, 5.26) + >>> hm._add_item(7, 5.155) + >>> hm + HashMap(10: 13.2) + + Trying to add an element with a key that is a floating point value + >>> hm = HashMap(5) + >>> hm._add_item(1.5, 10) + >>> hm + HashMap(1.5: 10) + + 5. Trying to add an item with the same key + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm._add_item(1, 20) + >>> hm + HashMap(1: 20) + """ + for ind in self._iterate_buckets(key): + if self._try_set(ind, key, val): + break + + def __setitem__(self, key: KEY, val: VAL) -> None: + """ + 1. Changing value of item whose key is present + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm.__setitem__(1, 20) + >>> hm + HashMap(1: 20) + + 2. Changing value of item whose key is not present + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm.__setitem__(0, 20) + >>> hm + HashMap(0: 20, 1: 10) + + 3. Changing the value of the same item multiple times + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm.__setitem__(1, 20) + >>> hm.__setitem__(1, 30) + >>> hm + HashMap(1: 30) + """ + if self._is_full(): + self._size_up() + + self._add_item(key, val) + + def __delitem__(self, key: KEY) -> None: + """ + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm._add_item(2, 20) + >>> hm._add_item(3, 30) + >>> hm.__delitem__(3) + >>> hm + HashMap(1: 10, 2: 20) + >>> hm = HashMap(5) + >>> hm._add_item(-5, 10) + >>> hm._add_item(6, 30) + >>> hm._add_item(-7, 20) + >>> hm.__delitem__(-5) + >>> hm + HashMap(6: 30, -7: 20) + + # Trying to remove a non-existing item + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm._add_item(2, 20) + >>> hm._add_item(3, 30) + >>> hm.__delitem__(4) + Traceback (most recent call last): + ... + KeyError: 4 + + # Test resize down when sparse + ## Setup: resize up + >>> hm = HashMap(initial_block_size=100, capacity_factor=0.75) + >>> len(hm._buckets) + 100 + >>> for i in range(75): + ... hm[i] = i + >>> len(hm._buckets) + 100 + >>> hm[75] = 75 + >>> len(hm._buckets) + 200 + + ## Resize down + >>> del hm[75] + >>> len(hm._buckets) + 200 + >>> del hm[74] + >>> len(hm._buckets) + 100 + """ + for ind in self._iterate_buckets(key): + item = self._buckets[ind] + if item is None: + raise KeyError(key) + if item is _deleted: + continue + if item.key == key: + self._buckets[ind] = _deleted + self._len -= 1 + break + if self._is_sparse(): + self._size_down() + + def __getitem__(self, key: KEY) -> VAL: + """ + Returns the item at the given key + + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm.__getitem__(1) + 10 + + >>> hm = HashMap(5) + >>> hm._add_item(10, -10) + >>> hm._add_item(20, -20) + >>> hm.__getitem__(20) + -20 + + >>> hm = HashMap(5) + >>> hm._add_item(-1, 10) + >>> hm.__getitem__(-1) + 10 + """ + for ind in self._iterate_buckets(key): + item = self._buckets[ind] + if item is None: + break + if item is _deleted: + continue + if item.key == key: + return item.val + raise KeyError(key) + + def __len__(self) -> int: + """ + Returns the number of items present in hashmap + + >>> hm = HashMap(5) + >>> hm._add_item(1, 10) + >>> hm._add_item(2, 20) + >>> hm._add_item(3, 30) + >>> hm.__len__() + 3 + + >>> hm = HashMap(5) + >>> hm.__len__() + 0 + """ + return self._len + + def __iter__(self) -> Iterator[KEY]: + yield from (item.key for item in self._buckets if item) + + def __repr__(self) -> str: + val_string = ", ".join( + f"{item.key}: {item.val}" for item in self._buckets if item + ) + return f"HashMap({val_string})" + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/hashing/hash_table.py b/data_structures/hashing/hash_table.py new file mode 100644 index 000000000000..cfc1db4feea4 --- /dev/null +++ b/data_structures/hashing/hash_table.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +from abc import abstractmethod + +from .number_theory.prime_numbers import next_prime + + +class HashTable: + """ + Basic Hash Table example with open addressing and linear probing + """ + + def __init__( + self, + size_table: int, + charge_factor: int | None = None, + lim_charge: float | None = None, + ) -> None: + self.size_table = size_table + self.values = [None] * self.size_table + self.lim_charge = 0.75 if lim_charge is None else lim_charge + self.charge_factor = 1 if charge_factor is None else charge_factor + self.__aux_list: list = [] + self._keys: dict = {} + + def keys(self) -> dict: + """ + The keys function returns a dictionary containing the key value pairs. + key being the index number in hash table and value being the data value. + + Examples: + 1. creating HashTable with size 10 and inserting 3 elements + >>> ht = HashTable(10) + >>> ht.insert_data(10) + >>> ht.insert_data(20) + >>> ht.insert_data(30) + >>> ht.keys() + {0: 10, 1: 20, 2: 30} + + 2. creating HashTable with size 5 and inserting 5 elements + >>> ht = HashTable(5) + >>> ht.insert_data(5) + >>> ht.insert_data(4) + >>> ht.insert_data(3) + >>> ht.insert_data(2) + >>> ht.insert_data(1) + >>> ht.keys() + {0: 5, 4: 4, 3: 3, 2: 2, 1: 1} + """ + return self._keys + + def balanced_factor(self) -> float: + return sum(1 for slot in self.values if slot is not None) / ( + self.size_table * self.charge_factor + ) + + def hash_function(self, key) -> int: + """ + Generates hash for the given key value + + Examples: + + Creating HashTable with size 5 + >>> ht = HashTable(5) + >>> ht.hash_function(10) + 0 + >>> ht.hash_function(20) + 0 + >>> ht.hash_function(4) + 4 + >>> ht.hash_function(18) + 3 + >>> ht.hash_function(-18) + 2 + >>> ht.hash_function(18.5) + 3.5 + >>> ht.hash_function(0) + 0 + >>> ht.hash_function(-0) + 0 + """ + return key % self.size_table + + def _step_by_step(self, step_ord) -> None: + print(f"step {step_ord}") + print(list(range(len(self.values)))) + print(self.values) + + def bulk_insert(self, values) -> None: + """ + bulk_insert is used for entering more than one element at a time + in the HashTable. + + Examples: + 1. + >>> ht = HashTable(5) + >>> ht.bulk_insert((10,20,30)) + step 1 + [0, 1, 2, 3, 4] + [10, None, None, None, None] + step 2 + [0, 1, 2, 3, 4] + [10, 20, None, None, None] + step 3 + [0, 1, 2, 3, 4] + [10, 20, 30, None, None] + + 2. + >>> ht = HashTable(5) + >>> ht.bulk_insert([5,4,3,2,1]) + step 1 + [0, 1, 2, 3, 4] + [5, None, None, None, None] + step 2 + [0, 1, 2, 3, 4] + [5, None, None, None, 4] + step 3 + [0, 1, 2, 3, 4] + [5, None, None, 3, 4] + step 4 + [0, 1, 2, 3, 4] + [5, None, 2, 3, 4] + step 5 + [0, 1, 2, 3, 4] + [5, 1, 2, 3, 4] + """ + i = 1 + self.__aux_list = values + for value in values: + self.insert_data(value) + self._step_by_step(i) + i += 1 + + def _set_value(self, key, data) -> None: + """ + _set_value functions allows to update value at a particular hash + + Examples: + 1. _set_value in HashTable of size 5 + >>> ht = HashTable(5) + >>> ht.insert_data(10) + >>> ht.insert_data(20) + >>> ht.insert_data(30) + >>> ht._set_value(0,15) + >>> ht.keys() + {0: 15, 1: 20, 2: 30} + + 2. _set_value in HashTable of size 2 + >>> ht = HashTable(2) + >>> ht.insert_data(17) + >>> ht.insert_data(18) + >>> ht.insert_data(99) + >>> ht._set_value(3,15) + >>> ht.keys() + {3: 15, 2: 17, 4: 99} + + 3. _set_value in HashTable when hash is not present + >>> ht = HashTable(2) + >>> ht.insert_data(17) + >>> ht.insert_data(18) + >>> ht.insert_data(99) + >>> ht._set_value(0,15) + >>> ht.keys() + {3: 18, 2: 17, 4: 99, 0: 15} + + 4. _set_value in HashTable when multiple hash are not present + >>> ht = HashTable(2) + >>> ht.insert_data(17) + >>> ht.insert_data(18) + >>> ht.insert_data(99) + >>> ht._set_value(0,15) + >>> ht._set_value(1,20) + >>> ht.keys() + {3: 18, 2: 17, 4: 99, 0: 15, 1: 20} + """ + self.values[key] = data + self._keys[key] = data + + @abstractmethod + def _collision_resolution(self, key, data=None): + """ + This method is a type of open addressing which is used for handling collision. + + In this implementation the concept of linear probing has been used. + + The hash table is searched sequentially from the original location of the + hash, if the new hash/location we get is already occupied we check for the next + hash/location. + + references: + - https://en.wikipedia.org/wiki/Linear_probing + + Examples: + 1. The collision will be with keys 18 & 99, so new hash will be created for 99 + >>> ht = HashTable(3) + >>> ht.insert_data(17) + >>> ht.insert_data(18) + >>> ht.insert_data(99) + >>> ht.keys() + {2: 17, 0: 18, 1: 99} + + 2. The collision will be with keys 17 & 101, so new hash + will be created for 101 + >>> ht = HashTable(4) + >>> ht.insert_data(17) + >>> ht.insert_data(18) + >>> ht.insert_data(99) + >>> ht.insert_data(101) + >>> ht.keys() + {1: 17, 2: 18, 3: 99, 0: 101} + + 2. The collision will be with all keys, so new hash will be created for all + >>> ht = HashTable(1) + >>> ht.insert_data(17) + >>> ht.insert_data(18) + >>> ht.insert_data(99) + >>> ht.keys() + {2: 17, 3: 18, 4: 99} + + 3. Trying to insert float key in hash + >>> ht = HashTable(1) + >>> ht.insert_data(17) + >>> ht.insert_data(18) + >>> ht.insert_data(99.99) + Traceback (most recent call last): + ... + TypeError: list indices must be integers or slices, not float + """ + new_key = self.hash_function(key + 1) + + while self.values[new_key] is not None and self.values[new_key] != key: + if self.values.count(None) > 0: + new_key = self.hash_function(new_key + 1) + else: + new_key = None + break + + return new_key + + def rehashing(self) -> None: + survivor_values = [value for value in self.values if value is not None] + self.size_table = next_prime(self.size_table, factor=2) + self._keys.clear() + self.values = [None] * self.size_table # hell's pointers D: don't DRY ;/ + for value in survivor_values: + self.insert_data(value) + + def insert_data(self, data) -> None: + """ + insert_data is used for inserting a single element at a time in the HashTable. + + Examples: + + >>> ht = HashTable(3) + >>> ht.insert_data(5) + >>> ht.keys() + {2: 5} + >>> ht = HashTable(5) + >>> ht.insert_data(30) + >>> ht.insert_data(50) + >>> ht.keys() + {0: 30, 1: 50} + """ + key = self.hash_function(data) + + if self.values[key] is None: + self._set_value(key, data) + + elif self.values[key] == data: + pass + + else: + collision_resolution = self._collision_resolution(key, data) + if collision_resolution is not None: + self._set_value(collision_resolution, data) + else: + self.rehashing() + self.insert_data(data) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/hashing/hash_table_with_linked_list.py b/data_structures/hashing/hash_table_with_linked_list.py new file mode 100644 index 000000000000..4903db73a782 --- /dev/null +++ b/data_structures/hashing/hash_table_with_linked_list.py @@ -0,0 +1,27 @@ +from collections import deque + +from .hash_table import HashTable + + +class HashTableWithLinkedList(HashTable): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + def _set_value(self, key, data) -> None: + self.values[key] = deque() if self.values[key] is None else self.values[key] + self.values[key].appendleft(data) + self._keys[key] = self.values[key] + + def balanced_factor(self) -> float: + return ( + sum(self.charge_factor - len(slot) for slot in self.values) + / self.size_table + * self.charge_factor + ) + + def _collision_resolution(self, key, data=None): + if not ( + len(self.values[key]) == self.charge_factor and self.values.count(None) == 0 + ): + return key + return super()._collision_resolution(key, data) diff --git a/data_structures/hashing/number_theory/__init__.py b/data_structures/hashing/number_theory/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/hashing/number_theory/prime_numbers.py b/data_structures/hashing/number_theory/prime_numbers.py new file mode 100644 index 000000000000..82071b5e9f09 --- /dev/null +++ b/data_structures/hashing/number_theory/prime_numbers.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +""" +module to operations with prime numbers +""" + +import math + + +def is_prime(number: int) -> bool: + """Checks to see if a number is a prime in O(sqrt(n)). + + A number is prime if it has exactly two factors: 1 and itself. + + >>> is_prime(0) + False + >>> is_prime(1) + False + >>> is_prime(2) + True + >>> is_prime(3) + True + >>> is_prime(27) + False + >>> is_prime(87) + False + >>> is_prime(563) + True + >>> is_prime(2999) + True + >>> is_prime(67483) + False + """ + + # precondition + assert isinstance(number, int) and (number >= 0), ( + "'number' must been an int and positive" + ) + + if 1 < number < 4: + # 2 and 3 are primes + return True + elif number < 2 or not number % 2: + # Negatives, 0, 1 and all even numbers are not primes + return False + + odd_numbers = range(3, int(math.sqrt(number) + 1), 2) + return not any(not number % i for i in odd_numbers) + + +def next_prime(value, factor=1, **kwargs): + value = factor * value + first_value_val = value + + while not is_prime(value): + value += 1 if not ("desc" in kwargs and kwargs["desc"] is True) else -1 + + if value == first_value_val: + return next_prime(value + 1, **kwargs) + return value diff --git a/data_structures/hashing/quadratic_probing.py b/data_structures/hashing/quadratic_probing.py new file mode 100644 index 000000000000..8d8b4457232a --- /dev/null +++ b/data_structures/hashing/quadratic_probing.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +from .hash_table import HashTable + + +class QuadraticProbing(HashTable): + """ + Basic Hash Table example with open addressing using Quadratic Probing + """ + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + def _collision_resolution(self, key, data=None): # noqa: ARG002 + """ + Quadratic probing is an open addressing scheme used for resolving + collisions in hash table. + + It works by taking the original hash index and adding successive + values of an arbitrary quadratic polynomial until open slot is found. + + Hash + 1², Hash + 2², Hash + 3² .... Hash + n² + + reference: + - https://en.wikipedia.org/wiki/Quadratic_probing + e.g: + 1. Create hash table with size 7 + >>> qp = QuadraticProbing(7) + >>> qp.insert_data(90) + >>> qp.insert_data(340) + >>> qp.insert_data(24) + >>> qp.insert_data(45) + >>> qp.insert_data(99) + >>> qp.insert_data(73) + >>> qp.insert_data(7) + >>> qp.keys() + {11: 45, 14: 99, 7: 24, 0: 340, 5: 73, 6: 90, 8: 7} + + 2. Create hash table with size 8 + >>> qp = QuadraticProbing(8) + >>> qp.insert_data(0) + >>> qp.insert_data(999) + >>> qp.insert_data(111) + >>> qp.keys() + {0: 0, 7: 999, 3: 111} + + 3. Try to add three data elements when the size is two + >>> qp = QuadraticProbing(2) + >>> qp.insert_data(0) + >>> qp.insert_data(999) + >>> qp.insert_data(111) + >>> qp.keys() + {0: 0, 4: 999, 1: 111} + + 4. Try to add three data elements when the size is one + >>> qp = QuadraticProbing(1) + >>> qp.insert_data(0) + >>> qp.insert_data(999) + >>> qp.insert_data(111) + >>> qp.keys() + {4: 999, 1: 111} + """ + + i = 1 + new_key = self.hash_function(key + i * i) + + while self.values[new_key] is not None and self.values[new_key] != key: + i += 1 + new_key = ( + self.hash_function(key + i * i) + if not self.balanced_factor() >= self.lim_charge + else None + ) + + if new_key is None: + break + + return new_key + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/hashing/tests/__init__.py b/data_structures/hashing/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/hashing/tests/test_hash_map.py b/data_structures/hashing/tests/test_hash_map.py new file mode 100644 index 000000000000..0ec55595965e --- /dev/null +++ b/data_structures/hashing/tests/test_hash_map.py @@ -0,0 +1,97 @@ +from operator import delitem, getitem, setitem + +import pytest + +from data_structures.hashing.hash_map import HashMap + + +def _get(k): + return getitem, k + + +def _set(k, v): + return setitem, k, v + + +def _del(k): + return delitem, k + + +def _run_operation(obj, fun, *args): + try: + return fun(obj, *args), None + except Exception as e: + return None, e + + +_add_items = ( + _set("key_a", "val_a"), + _set("key_b", "val_b"), +) + +_overwrite_items = [ + _set("key_a", "val_a"), + _set("key_a", "val_b"), +] + +_delete_items = [ + _set("key_a", "val_a"), + _set("key_b", "val_b"), + _del("key_a"), + _del("key_b"), + _set("key_a", "val_a"), + _del("key_a"), +] + +_access_absent_items = [ + _get("key_a"), + _del("key_a"), + _set("key_a", "val_a"), + _del("key_a"), + _del("key_a"), + _get("key_a"), +] + +_add_with_resize_up = [ + *[_set(x, x) for x in range(5)], # guaranteed upsize +] + +_add_with_resize_down = [ + *[_set(x, x) for x in range(5)], # guaranteed upsize + *[_del(x) for x in range(5)], + _set("key_a", "val_b"), +] + + +@pytest.mark.parametrize( + "operations", + [ + pytest.param(_add_items, id="add items"), + pytest.param(_overwrite_items, id="overwrite items"), + pytest.param(_delete_items, id="delete items"), + pytest.param(_access_absent_items, id="access absent items"), + pytest.param(_add_with_resize_up, id="add with resize up"), + pytest.param(_add_with_resize_down, id="add with resize down"), + ], +) +def test_hash_map_is_the_same_as_dict(operations) -> None: + my = HashMap(initial_block_size=4) + py = {} + for _, (fun, *args) in enumerate(operations): + my_res, my_exc = _run_operation(my, fun, *args) + py_res, py_exc = _run_operation(py, fun, *args) + assert my_res == py_res + assert str(my_exc) == str(py_exc) + assert set(py) == set(my) + assert len(py) == len(my) + assert set(my.items()) == set(py.items()) + + +def test_no_new_methods_was_added_to_api() -> None: + def is_public(name: str) -> bool: + return not name.startswith("_") + + dict_public_names = {name for name in dir({}) if is_public(name)} + hash_public_names = {name for name in dir(HashMap()) if is_public(name)} + + assert dict_public_names > hash_public_names diff --git a/data_structures/heap/__init__.py b/data_structures/heap/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/heap/binomial_heap.py b/data_structures/heap/binomial_heap.py new file mode 100644 index 000000000000..489355ee7271 --- /dev/null +++ b/data_structures/heap/binomial_heap.py @@ -0,0 +1,403 @@ +""" +Binomial Heap +Reference: Advanced Data Structures, Peter Brass +""" + + +class Node: + """ + Node in a doubly-linked binomial tree, containing: + - value + - size of left subtree + - link to left, right and parent nodes + """ + + def __init__(self, val) -> None: + self.val = val + # Number of nodes in left subtree + self.left_tree_size = 0 + self.left = None + self.right = None + self.parent = None + + def merge_trees(self, other): + """ + In-place merge of two binomial trees of equal size. + Returns the root of the resulting tree + """ + assert self.left_tree_size == other.left_tree_size, "Unequal Sizes of Blocks" + + if self.val < other.val: + other.left = self.right + other.parent = None + if self.right: + self.right.parent = other + self.right = other + self.left_tree_size = self.left_tree_size * 2 + 1 + return self + else: + self.left = other.right + self.parent = None + if other.right: + other.right.parent = self + other.right = self + other.left_tree_size = other.left_tree_size * 2 + 1 + return other + + +class BinomialHeap: + r""" + Min-oriented priority queue implemented with the Binomial Heap data + structure implemented with the BinomialHeap class. It supports: + - Insert element in a heap with n elements: Guaranteed logn, amoratized 1 + - Merge (meld) heaps of size m and n: O(logn + logm) + - Delete Min: O(logn) + - Peek (return min without deleting it): O(1) + + Example: + + Create a random permutation of 30 integers to be inserted and 19 of them deleted + >>> import numpy as np + >>> permutation = np.random.permutation(list(range(30))) + + Create a Heap and insert the 30 integers + __init__() test + >>> first_heap = BinomialHeap() + + 30 inserts - insert() test + >>> for number in permutation: + ... first_heap.insert(number) + + Size test + >>> first_heap.size + 30 + + Deleting - delete() test + >>> [int(first_heap.delete_min()) for _ in range(20)] + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] + + Create a new Heap + >>> second_heap = BinomialHeap() + >>> vals = [17, 20, 31, 34] + >>> for value in vals: + ... second_heap.insert(value) + + + The heap should have the following structure: + + 17 + / \ + # 31 + / \ + 20 34 + / \ / \ + # # # # + + preOrder() test + >>> " ".join(str(x) for x in second_heap.pre_order()) + "(17, 0) ('#', 1) (31, 1) (20, 2) ('#', 3) ('#', 3) (34, 2) ('#', 3) ('#', 3)" + + printing Heap - __str__() test + >>> print(second_heap) + 17 + -# + -31 + --20 + ---# + ---# + --34 + ---# + ---# + + mergeHeaps() test + >>> + >>> merged = second_heap.merge_heaps(first_heap) + >>> merged.peek() + 17 + + values in merged heap; (merge is inplace) + >>> results = [] + >>> while not first_heap.is_empty(): + ... results.append(int(first_heap.delete_min())) + >>> results + [17, 20, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 31, 34] + """ + + def __init__(self, bottom_root=None, min_node=None, heap_size=0) -> None: + self.size = heap_size + self.bottom_root = bottom_root + self.min_node = min_node + + def merge_heaps(self, other): + """ + In-place merge of two binomial heaps. + Both of them become the resulting merged heap + """ + + # Empty heaps corner cases + if other.size == 0: + return None + if self.size == 0: + self.size = other.size + self.bottom_root = other.bottom_root + self.min_node = other.min_node + return None + # Update size + self.size = self.size + other.size + + # Update min.node + if self.min_node.val > other.min_node.val: + self.min_node = other.min_node + # Merge + + # Order roots by left_subtree_size + combined_roots_list = [] + i, j = self.bottom_root, other.bottom_root + while i or j: + if i and ((not j) or i.left_tree_size < j.left_tree_size): + combined_roots_list.append((i, True)) + i = i.parent + else: + combined_roots_list.append((j, False)) + j = j.parent + # Insert links between them + for i in range(len(combined_roots_list) - 1): + if combined_roots_list[i][1] != combined_roots_list[i + 1][1]: + combined_roots_list[i][0].parent = combined_roots_list[i + 1][0] + combined_roots_list[i + 1][0].left = combined_roots_list[i][0] + # Consecutively merge roots with same left_tree_size + i = combined_roots_list[0][0] + while i.parent: + if ( + (i.left_tree_size == i.parent.left_tree_size) and (not i.parent.parent) + ) or ( + i.left_tree_size == i.parent.left_tree_size + and i.left_tree_size != i.parent.parent.left_tree_size + ): + # Neighbouring Nodes + previous_node = i.left + next_node = i.parent.parent + + # Merging trees + i = i.merge_trees(i.parent) + + # Updating links + i.left = previous_node + i.parent = next_node + if previous_node: + previous_node.parent = i + if next_node: + next_node.left = i + else: + i = i.parent + # Updating self.bottom_root + while i.left: + i = i.left + self.bottom_root = i + + # Update other + other.size = self.size + other.bottom_root = self.bottom_root + other.min_node = self.min_node + + # Return the merged heap + return self + + def insert(self, val) -> None: + """ + insert a value in the heap + """ + if self.size == 0: + self.bottom_root = Node(val) + self.size = 1 + self.min_node = self.bottom_root + else: + # Create new node + new_node = Node(val) + + # Update size + self.size += 1 + + # update min_node + if val < self.min_node.val: + self.min_node = new_node + # Put new_node as a bottom_root in heap + assert self.bottom_root is not None + self.bottom_root.left = new_node + new_node.parent = self.bottom_root + self.bottom_root = new_node + + # Consecutively merge roots with same left_tree_size + while ( + self.bottom_root.parent + and self.bottom_root.left_tree_size + == self.bottom_root.parent.left_tree_size + ): + # Next node + next_node = self.bottom_root.parent.parent + + # Merge + self.bottom_root = self.bottom_root.merge_trees(self.bottom_root.parent) + + # Update Links + self.bottom_root.parent = next_node + self.bottom_root.left = None + if next_node: + next_node.left = self.bottom_root + + def peek(self): + """ + return min element without deleting it + """ + return self.min_node.val + + def is_empty(self) -> bool: + return self.size == 0 + + def delete_min(self): + """ + delete min element and return it + """ + # assert not self.isEmpty(), "Empty Heap" + + # Save minimal value + min_value = self.min_node.val + + # Last element in heap corner case + if self.size == 1: + # Update size + self.size = 0 + + # Update bottom root + self.bottom_root = None + + # Update min_node + self.min_node = None + + return min_value + # No right subtree corner case + # The structure of the tree implies that this should be the bottom root + # and there is at least one other root + if self.min_node.right is None: + # Update size + self.size -= 1 + + # Update bottom root + self.bottom_root = self.bottom_root.parent + assert self.bottom_root is not None + self.bottom_root.left = None + + # Update min_node + self.min_node = self.bottom_root + i = self.bottom_root.parent + while i: + if i.val < self.min_node.val: + self.min_node = i + i = i.parent + return min_value + # General case + # Find the BinomialHeap of the right subtree of min_node + bottom_of_new = self.min_node.right + bottom_of_new.parent = None + min_of_new = bottom_of_new + size_of_new = 1 + + # Size, min_node and bottom_root + while bottom_of_new.left: + size_of_new = size_of_new * 2 + 1 + bottom_of_new = bottom_of_new.left + if bottom_of_new.val < min_of_new.val: + min_of_new = bottom_of_new + # Corner case of single root on top left path + if (not self.min_node.left) and (not self.min_node.parent): + self.size = size_of_new + self.bottom_root = bottom_of_new + self.min_node = min_of_new + # print("Single root, multiple nodes case") + return min_value + # Remaining cases + # Construct heap of right subtree + new_heap = BinomialHeap( + bottom_root=bottom_of_new, min_node=min_of_new, heap_size=size_of_new + ) + + # Update size + self.size = self.size - 1 - size_of_new + + # Neighbour nodes + previous_node = self.min_node.left + next_node = self.min_node.parent + + # Initialize new bottom_root and min_node + self.min_node = previous_node or next_node + self.bottom_root = next_node + + # Update links of previous_node and search below for new min_node and + # bottom_root + if previous_node: + previous_node.parent = next_node + + # Update bottom_root and search for min_node below + self.bottom_root = previous_node + self.min_node = previous_node + while self.bottom_root.left: + self.bottom_root = self.bottom_root.left + if self.bottom_root.val < self.min_node.val: + self.min_node = self.bottom_root + if next_node: + next_node.left = previous_node + + # Search for new min_node above min_node + i = next_node + while i: + if i.val < self.min_node.val: + self.min_node = i + i = i.parent + # Merge heaps + self.merge_heaps(new_heap) + + return int(min_value) + + def pre_order(self): + """ + Returns the Pre-order representation of the heap including + values of nodes plus their level distance from the root; + Empty nodes appear as # + """ + # Find top root + top_root = self.bottom_root + while top_root.parent: + top_root = top_root.parent + # preorder + heap_pre_order = [] + self.__traversal(top_root, heap_pre_order) + return heap_pre_order + + def __traversal(self, curr_node, preorder, level=0) -> None: + """ + Pre-order traversal of nodes + """ + if curr_node: + preorder.append((curr_node.val, level)) + self.__traversal(curr_node.left, preorder, level + 1) + self.__traversal(curr_node.right, preorder, level + 1) + else: + preorder.append(("#", level)) + + def __str__(self) -> str: + """ + Overwriting str for a pre-order print of nodes in heap; + Performance is poor, so use only for small examples + """ + if self.is_empty(): + return "" + preorder_heap = self.pre_order() + + return "\n".join(("-" * level + str(value)) for value, level in preorder_heap) + + +# Unit Tests +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/heap/heap.py b/data_structures/heap/heap.py new file mode 100644 index 000000000000..bf2c14c40dc9 --- /dev/null +++ b/data_structures/heap/heap.py @@ -0,0 +1,264 @@ +from __future__ import annotations + +from abc import abstractmethod +from collections.abc import Iterable +from typing import Protocol + + +class Comparable(Protocol): + @abstractmethod + def __gt__(self, other: object, /) -> bool: + pass + + +class Heap[T: Comparable]: + """A Max Heap Implementation + + >>> unsorted = [103, 9, 1, 7, 11, 15, 25, 201, 209, 107, 5] + >>> h = Heap() + >>> h.build_max_heap(unsorted) + >>> h + [209, 201, 25, 103, 107, 15, 1, 9, 7, 11, 5] + >>> + >>> h.extract_max() + 209 + >>> h + [201, 107, 25, 103, 11, 15, 1, 9, 7, 5] + >>> + >>> h.insert(100) + >>> h + [201, 107, 25, 103, 100, 15, 1, 9, 7, 5, 11] + >>> + >>> h.heap_sort() + >>> h + [1, 5, 7, 9, 11, 15, 25, 100, 103, 107, 201] + """ + + def __init__(self) -> None: + self.h: list[T] = [] + self.heap_size: int = 0 + + def __repr__(self) -> str: + return str(self.h) + + def parent_index(self, child_idx: int) -> int | None: + """ + Returns the parent index based on the given child index + + >>> h = Heap() + >>> h.build_max_heap([103, 9, 1, 7, 11, 15, 25, 201, 209, 107, 5]) + >>> h + [209, 201, 25, 103, 107, 15, 1, 9, 7, 11, 5] + + >>> h.parent_index(-1) # returns none if index is <=0 + + >>> h.parent_index(0) # returns none if index is <=0 + + >>> h.parent_index(1) + 0 + >>> h.parent_index(2) + 0 + >>> h.parent_index(3) + 1 + >>> h.parent_index(4) + 1 + >>> h.parent_index(5) + 2 + >>> h.parent_index(10.5) + 4.0 + >>> h.parent_index(209.0) + 104.0 + >>> h.parent_index("Test") + Traceback (most recent call last): + ... + TypeError: '>' not supported between instances of 'str' and 'int' + """ + if child_idx > 0: + return (child_idx - 1) // 2 + return None + + def left_child_idx(self, parent_idx: int) -> int | None: + """ + Return the left child index if the left child exists. + If not, return None. + """ + left_child_index = 2 * parent_idx + 1 + if left_child_index < self.heap_size: + return left_child_index + return None + + def right_child_idx(self, parent_idx: int) -> int | None: + """ + Return the right child index if the right child exists. + If not, return None. + """ + right_child_index = 2 * parent_idx + 2 + if right_child_index < self.heap_size: + return right_child_index + return None + + def max_heapify(self, index: int) -> None: + """ + Correct a single violation of the heap property in a subtree's root. + + It is the function that is responsible for restoring the property + of a max heap, i.e the maximum element is always at the top. + """ + if index < self.heap_size: + violation: int = index + left_child = self.left_child_idx(index) + right_child = self.right_child_idx(index) + # check which child is larger than its parent + if left_child is not None and self.h[left_child] > self.h[violation]: + violation = left_child + if right_child is not None and self.h[right_child] > self.h[violation]: + violation = right_child + # if violation indeed exists + if violation != index: + # swap to fix the violation + self.h[violation], self.h[index] = self.h[index], self.h[violation] + # fix the subsequent violation recursively if any + self.max_heapify(violation) + + def build_max_heap(self, collection: Iterable[T]) -> None: + """ + Build a max heap from an unsorted array + + >>> h = Heap() + >>> h.build_max_heap([20,40,50,20,10]) + >>> h + [50, 40, 20, 20, 10] + + >>> h = Heap() + >>> h.build_max_heap([1,2,3,4,5,6,7,8,9,0]) + >>> h + [9, 8, 7, 4, 5, 6, 3, 2, 1, 0] + + >>> h = Heap() + >>> h.build_max_heap([514,5,61,57,8,99,105]) + >>> h + [514, 57, 105, 5, 8, 99, 61] + + >>> h = Heap() + >>> h.build_max_heap([514,5,61.6,57,8,9.9,105]) + >>> h + [514, 57, 105, 5, 8, 9.9, 61.6] + """ + self.h = list(collection) + self.heap_size = len(self.h) + if self.heap_size > 1: + # max_heapify from right to left but exclude leaves (last level) + for i in range(self.heap_size // 2 - 1, -1, -1): + self.max_heapify(i) + + def extract_max(self) -> T: + """ + Get and remove max from heap + + >>> h = Heap() + >>> h.build_max_heap([20,40,50,20,10]) + >>> h.extract_max() + 50 + + >>> h = Heap() + >>> h.build_max_heap([514,5,61,57,8,99,105]) + >>> h.extract_max() + 514 + + >>> h = Heap() + >>> h.build_max_heap([1,2,3,4,5,6,7,8,9,0]) + >>> h.extract_max() + 9 + """ + if self.heap_size >= 2: + me = self.h[0] + self.h[0] = self.h.pop(-1) + self.heap_size -= 1 + self.max_heapify(0) + return me + elif self.heap_size == 1: + self.heap_size -= 1 + return self.h.pop(-1) + else: + raise Exception("Empty heap") + + def insert(self, value: T) -> None: + """ + Insert a new value into the max heap + + >>> h = Heap() + >>> h.insert(10) + >>> h + [10] + + >>> h = Heap() + >>> h.insert(10) + >>> h.insert(10) + >>> h + [10, 10] + + >>> h = Heap() + >>> h.insert(10) + >>> h.insert(10.1) + >>> h + [10.1, 10] + + >>> h = Heap() + >>> h.insert(0.1) + >>> h.insert(0) + >>> h.insert(9) + >>> h.insert(5) + >>> h + [9, 5, 0.1, 0] + """ + self.h.append(value) + idx = (self.heap_size - 1) // 2 + self.heap_size += 1 + while idx >= 0: + self.max_heapify(idx) + idx = (idx - 1) // 2 + + def heap_sort(self) -> None: + size = self.heap_size + for j in range(size - 1, 0, -1): + self.h[0], self.h[j] = self.h[j], self.h[0] + self.heap_size -= 1 + self.max_heapify(0) + self.heap_size = size + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + # demo + for unsorted in [ + [0], + [2], + [3, 5], + [5, 3], + [5, 5], + [0, 0, 0, 0], + [1, 1, 1, 1], + [2, 2, 3, 5], + [0, 2, 2, 3, 5], + [2, 5, 3, 0, 2, 3, 0, 3], + [6, 1, 2, 7, 9, 3, 4, 5, 10, 8], + [103, 9, 1, 7, 11, 15, 25, 201, 209, 107, 5], + [-45, -2, -5], + ]: + print(f"unsorted array: {unsorted}") + + heap: Heap[int] = Heap() + heap.build_max_heap(unsorted) + print(f"after build heap: {heap}") + + print(f"max value: {heap.extract_max()}") + print(f"after max value removed: {heap}") + + heap.insert(100) + print(f"after new value 100 inserted: {heap}") + + heap.heap_sort() + print(f"heap-sorted array: {heap}\n") diff --git a/data_structures/heap/heap_generic.py b/data_structures/heap/heap_generic.py new file mode 100644 index 000000000000..ee92149e25a9 --- /dev/null +++ b/data_structures/heap/heap_generic.py @@ -0,0 +1,174 @@ +from collections.abc import Callable + + +class Heap: + """ + A generic Heap class, can be used as min or max by passing the key function + accordingly. + """ + + def __init__(self, key: Callable | None = None) -> None: + # Stores actual heap items. + self.arr: list = [] + # Stores indexes of each item for supporting updates and deletion. + self.pos_map: dict = {} + # Stores current size of heap. + self.size = 0 + # Stores function used to evaluate the score of an item on which basis ordering + # will be done. + self.key = key or (lambda x: x) + + def _parent(self, i: int) -> int | None: + """Returns parent index of given index if exists else None""" + return int((i - 1) / 2) if i > 0 else None + + def _left(self, i: int) -> int | None: + """Returns left-child-index of given index if exists else None""" + left = int(2 * i + 1) + return left if 0 < left < self.size else None + + def _right(self, i: int) -> int | None: + """Returns right-child-index of given index if exists else None""" + right = int(2 * i + 2) + return right if 0 < right < self.size else None + + def _swap(self, i: int, j: int) -> None: + """Performs changes required for swapping two elements in the heap""" + # First update the indexes of the items in index map. + self.pos_map[self.arr[i][0]], self.pos_map[self.arr[j][0]] = ( + self.pos_map[self.arr[j][0]], + self.pos_map[self.arr[i][0]], + ) + # Then swap the items in the list. + self.arr[i], self.arr[j] = self.arr[j], self.arr[i] + + def _cmp(self, i: int, j: int) -> bool: + """Compares the two items using default comparison""" + return self.arr[i][1] < self.arr[j][1] + + def _get_valid_parent(self, i: int) -> int: + """ + Returns index of valid parent as per desired ordering among given index and + both it's children + """ + left = self._left(i) + right = self._right(i) + valid_parent = i + + if left is not None and not self._cmp(left, valid_parent): + valid_parent = left + if right is not None and not self._cmp(right, valid_parent): + valid_parent = right + + return valid_parent + + def _heapify_up(self, index: int) -> None: + """Fixes the heap in upward direction of given index""" + parent = self._parent(index) + while parent is not None and not self._cmp(index, parent): + self._swap(index, parent) + index, parent = parent, self._parent(parent) + + def _heapify_down(self, index: int) -> None: + """Fixes the heap in downward direction of given index""" + valid_parent = self._get_valid_parent(index) + while valid_parent != index: + self._swap(index, valid_parent) + index, valid_parent = valid_parent, self._get_valid_parent(valid_parent) + + def update_item(self, item: int, item_value: int) -> None: + """Updates given item value in heap if present""" + if item not in self.pos_map: + return + index = self.pos_map[item] + self.arr[index] = [item, self.key(item_value)] + # Make sure heap is right in both up and down direction. + # Ideally only one of them will make any change. + self._heapify_up(index) + self._heapify_down(index) + + def delete_item(self, item: int) -> None: + """Deletes given item from heap if present""" + if item not in self.pos_map: + return + index = self.pos_map[item] + del self.pos_map[item] + self.arr[index] = self.arr[self.size - 1] + self.pos_map[self.arr[self.size - 1][0]] = index + self.size -= 1 + # Make sure heap is right in both up and down direction. Ideally only one + # of them will make any change- so no performance loss in calling both. + if self.size > index: + self._heapify_up(index) + self._heapify_down(index) + + def insert_item(self, item: int, item_value: int) -> None: + """Inserts given item with given value in heap""" + arr_len = len(self.arr) + if arr_len == self.size: + self.arr.append([item, self.key(item_value)]) + else: + self.arr[self.size] = [item, self.key(item_value)] + self.pos_map[item] = self.size + self.size += 1 + self._heapify_up(self.size - 1) + + def get_top(self) -> tuple | None: + """Returns top item tuple (Calculated value, item) from heap if present""" + return self.arr[0] if self.size else None + + def extract_top(self) -> tuple | None: + """ + Return top item tuple (Calculated value, item) from heap and removes it as well + if present + """ + top_item_tuple = self.get_top() + if top_item_tuple: + self.delete_item(top_item_tuple[0]) + return top_item_tuple + + +def test_heap() -> None: + """ + >>> h = Heap() # Max-heap + >>> h.insert_item(5, 34) + >>> h.insert_item(6, 31) + >>> h.insert_item(7, 37) + >>> h.get_top() + [7, 37] + >>> h.extract_top() + [7, 37] + >>> h.extract_top() + [5, 34] + >>> h.extract_top() + [6, 31] + >>> h = Heap(key=lambda x: -x) # Min heap + >>> h.insert_item(5, 34) + >>> h.insert_item(6, 31) + >>> h.insert_item(7, 37) + >>> h.get_top() + [6, -31] + >>> h.extract_top() + [6, -31] + >>> h.extract_top() + [5, -34] + >>> h.extract_top() + [7, -37] + >>> h.insert_item(8, 45) + >>> h.insert_item(9, 40) + >>> h.insert_item(10, 50) + >>> h.get_top() + [9, -40] + >>> h.update_item(10, 30) + >>> h.get_top() + [10, -30] + >>> h.delete_item(10) + >>> h.get_top() + [9, -40] + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/heap/max_heap.py b/data_structures/heap/max_heap.py new file mode 100644 index 000000000000..ae322616c9fd --- /dev/null +++ b/data_structures/heap/max_heap.py @@ -0,0 +1,86 @@ +class BinaryHeap: + """ + A max-heap implementation in Python + >>> binary_heap = BinaryHeap() + >>> binary_heap.insert(6) + >>> binary_heap.insert(10) + >>> binary_heap.insert(15) + >>> binary_heap.insert(12) + >>> binary_heap.pop() + 15 + >>> binary_heap.pop() + 12 + >>> binary_heap.get_list + [10, 6] + >>> len(binary_heap) + 2 + """ + + def __init__(self) -> None: + self.__heap = [0] + self.__size = 0 + + def __swap_up(self, i: int) -> None: + """Swap the element up""" + temporary = self.__heap[i] + while i // 2 > 0: + if self.__heap[i] > self.__heap[i // 2]: + self.__heap[i] = self.__heap[i // 2] + self.__heap[i // 2] = temporary + i //= 2 + + def insert(self, value: int) -> None: + """Insert new element""" + self.__heap.append(value) + self.__size += 1 + self.__swap_up(self.__size) + + def __swap_down(self, i: int) -> None: + """Swap the element down""" + while self.__size >= 2 * i: + if 2 * i + 1 > self.__size: # noqa: SIM114 + bigger_child = 2 * i + elif self.__heap[2 * i] > self.__heap[2 * i + 1]: + bigger_child = 2 * i + else: + bigger_child = 2 * i + 1 + temporary = self.__heap[i] + if self.__heap[i] < self.__heap[bigger_child]: + self.__heap[i] = self.__heap[bigger_child] + self.__heap[bigger_child] = temporary + i = bigger_child + + def pop(self) -> int: + """Pop the root element""" + max_value = self.__heap[1] + self.__heap[1] = self.__heap[self.__size] + self.__size -= 1 + self.__heap.pop() + self.__swap_down(1) + return max_value + + @property + def get_list(self) -> list: + return self.__heap[1:] + + def __len__(self) -> int: + """Length of the array""" + return self.__size + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + # create an instance of BinaryHeap + binary_heap = BinaryHeap() + binary_heap.insert(6) + binary_heap.insert(10) + binary_heap.insert(15) + binary_heap.insert(12) + # pop root(max-values because it is max heap) + print(binary_heap.pop()) # 15 + print(binary_heap.pop()) # 12 + # get the list and size after operations + print(binary_heap.get_list) + print(len(binary_heap)) diff --git a/data_structures/heap/min_heap.py b/data_structures/heap/min_heap.py new file mode 100644 index 000000000000..10ecb37ade71 --- /dev/null +++ b/data_structures/heap/min_heap.py @@ -0,0 +1,170 @@ +# Min heap data structure +# with decrease key functionality - in O(log(n)) time + + +class Node: + def __init__(self, name, val) -> None: + self.name = name + self.val = val + + def __str__(self) -> str: + return f"{self.__class__.__name__}({self.name}, {self.val})" + + def __lt__(self, other) -> bool: + return self.val < other.val + + +class MinHeap: + """ + >>> r = Node("R", -1) + >>> b = Node("B", 6) + >>> a = Node("A", 3) + >>> x = Node("X", 1) + >>> e = Node("E", 4) + >>> print(b) + Node(B, 6) + >>> myMinHeap = MinHeap([r, b, a, x, e]) + >>> myMinHeap.decrease_key(b, -17) + >>> print(b) + Node(B, -17) + >>> myMinHeap["B"] + -17 + """ + + def __init__(self, array) -> None: + self.idx_of_element = {} + self.heap_dict = {} + self.heap = self.build_heap(array) + + def __getitem__(self, key): + return self.get_value(key) + + def get_parent_idx(self, idx) -> int: + return (idx - 1) // 2 + + def get_left_child_idx(self, idx) -> int: + return idx * 2 + 1 + + def get_right_child_idx(self, idx) -> int: + return idx * 2 + 2 + + def get_value(self, key): + return self.heap_dict[key] + + def build_heap(self, array) -> list: + last_idx = len(array) - 1 + start_from = self.get_parent_idx(last_idx) + + for idx, i in enumerate(array): + self.idx_of_element[i] = idx + self.heap_dict[i.name] = i.val + + for i in range(start_from, -1, -1): + self.sift_down(i, array) + return array + + # this is min-heapify method + def sift_down(self, idx, array) -> None: + while True: + left = self.get_left_child_idx(idx) + right = self.get_right_child_idx(idx) + + smallest = idx + if left < len(array) and array[left] < array[idx]: + smallest = left + if right < len(array) and array[right] < array[smallest]: + smallest = right + + if smallest != idx: + array[idx], array[smallest] = array[smallest], array[idx] + ( + self.idx_of_element[array[idx]], + self.idx_of_element[array[smallest]], + ) = ( + self.idx_of_element[array[smallest]], + self.idx_of_element[array[idx]], + ) + idx = smallest + else: + break + + def sift_up(self, idx) -> None: + p = self.get_parent_idx(idx) + while p >= 0 and self.heap[p] > self.heap[idx]: + self.heap[p], self.heap[idx] = self.heap[idx], self.heap[p] + self.idx_of_element[self.heap[p]], self.idx_of_element[self.heap[idx]] = ( + self.idx_of_element[self.heap[idx]], + self.idx_of_element[self.heap[p]], + ) + idx = p + p = self.get_parent_idx(idx) + + def peek(self): + return self.heap[0] + + def remove(self): + self.heap[0], self.heap[-1] = self.heap[-1], self.heap[0] + self.idx_of_element[self.heap[0]], self.idx_of_element[self.heap[-1]] = ( + self.idx_of_element[self.heap[-1]], + self.idx_of_element[self.heap[0]], + ) + + x = self.heap.pop() + del self.idx_of_element[x] + self.sift_down(0, self.heap) + return x + + def insert(self, node) -> None: + self.heap.append(node) + self.idx_of_element[node] = len(self.heap) - 1 + self.heap_dict[node.name] = node.val + self.sift_up(len(self.heap) - 1) + + def is_empty(self) -> bool: + return len(self.heap) == 0 + + def decrease_key(self, node, new_value) -> None: + assert self.heap[self.idx_of_element[node]].val > new_value, ( + "newValue must be less that current value" + ) + node.val = new_value + self.heap_dict[node.name] = new_value + self.sift_up(self.idx_of_element[node]) + + +# USAGE + +r = Node("R", -1) +b = Node("B", 6) +a = Node("A", 3) +x = Node("X", 1) +e = Node("E", 4) + +# Use one of these two ways to generate Min-Heap + +# Generating Min-Heap from array +my_min_heap = MinHeap([r, b, a, x, e]) + +# Generating Min-Heap by Insert method +# myMinHeap.insert(a) +# myMinHeap.insert(b) +# myMinHeap.insert(x) +# myMinHeap.insert(r) +# myMinHeap.insert(e) + +# Before +print("Min Heap - before decrease key") +for i in my_min_heap.heap: + print(i) + +print("Min Heap - After decrease key of node [B -> -17]") +my_min_heap.decrease_key(b, -17) + +# After +for i in my_min_heap.heap: + print(i) + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/heap/randomized_heap.py b/data_structures/heap/randomized_heap.py new file mode 100644 index 000000000000..9668d3ac2a45 --- /dev/null +++ b/data_structures/heap/randomized_heap.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import random +from collections.abc import Iterable +from typing import Any, TypeVar + +T = TypeVar("T", bound=bool) + + +class RandomizedHeapNode[T: bool]: + """ + One node of the randomized heap. Contains the value and references to + two children. + """ + + def __init__(self, value: T) -> None: + self._value: T = value + self.left: RandomizedHeapNode[T] | None = None + self.right: RandomizedHeapNode[T] | None = None + + @property + def value(self) -> T: + """ + Return the value of the node. + + >>> rhn = RandomizedHeapNode(10) + >>> rhn.value + 10 + >>> rhn = RandomizedHeapNode(-10) + >>> rhn.value + -10 + """ + return self._value + + @staticmethod + def merge( + root1: RandomizedHeapNode[T] | None, root2: RandomizedHeapNode[T] | None + ) -> RandomizedHeapNode[T] | None: + """ + Merge 2 nodes together. + + >>> rhn1 = RandomizedHeapNode(10) + >>> rhn2 = RandomizedHeapNode(20) + >>> RandomizedHeapNode.merge(rhn1, rhn2).value + 10 + + >>> rhn1 = RandomizedHeapNode(20) + >>> rhn2 = RandomizedHeapNode(10) + >>> RandomizedHeapNode.merge(rhn1, rhn2).value + 10 + + >>> rhn1 = RandomizedHeapNode(5) + >>> rhn2 = RandomizedHeapNode(0) + >>> RandomizedHeapNode.merge(rhn1, rhn2).value + 0 + """ + if not root1: + return root2 + + if not root2: + return root1 + + if root1.value > root2.value: + root1, root2 = root2, root1 + + if random.choice([True, False]): + root1.left, root1.right = root1.right, root1.left + + root1.left = RandomizedHeapNode.merge(root1.left, root2) + + return root1 + + +class RandomizedHeap[T: bool]: + """ + A data structure that allows inserting a new value and to pop the smallest + values. Both operations take O(logN) time where N is the size of the + structure. + Wiki: https://en.wikipedia.org/wiki/Randomized_meldable_heap + + >>> RandomizedHeap([2, 3, 1, 5, 1, 7]).to_sorted_list() + [1, 1, 2, 3, 5, 7] + + >>> rh = RandomizedHeap() + >>> rh.pop() + Traceback (most recent call last): + ... + IndexError: Can't get top element for the empty heap. + + >>> rh.insert(1) + >>> rh.insert(-1) + >>> rh.insert(0) + >>> rh.to_sorted_list() + [-1, 0, 1] + """ + + def __init__(self, data: Iterable[T] | None = ()) -> None: + """ + >>> rh = RandomizedHeap([3, 1, 3, 7]) + >>> rh.to_sorted_list() + [1, 3, 3, 7] + """ + self._root: RandomizedHeapNode[T] | None = None + + if data: + for item in data: + self.insert(item) + + def insert(self, value: T) -> None: + """ + Insert the value into the heap. + + >>> rh = RandomizedHeap() + >>> rh.insert(3) + >>> rh.insert(1) + >>> rh.insert(3) + >>> rh.insert(7) + >>> rh.to_sorted_list() + [1, 3, 3, 7] + """ + self._root = RandomizedHeapNode.merge(self._root, RandomizedHeapNode(value)) + + def pop(self) -> T | None: + """ + Pop the smallest value from the heap and return it. + + >>> rh = RandomizedHeap([3, 1, 3, 7]) + >>> rh.pop() + 1 + >>> rh.pop() + 3 + >>> rh.pop() + 3 + >>> rh.pop() + 7 + >>> rh.pop() + Traceback (most recent call last): + ... + IndexError: Can't get top element for the empty heap. + """ + + result = self.top() + + if self._root is None: + return None + + self._root = RandomizedHeapNode.merge(self._root.left, self._root.right) + + return result + + def top(self) -> T: + """ + Return the smallest value from the heap. + + >>> rh = RandomizedHeap() + >>> rh.insert(3) + >>> rh.top() + 3 + >>> rh.insert(1) + >>> rh.top() + 1 + >>> rh.insert(3) + >>> rh.top() + 1 + >>> rh.insert(7) + >>> rh.top() + 1 + """ + if not self._root: + raise IndexError("Can't get top element for the empty heap.") + return self._root.value + + def clear(self) -> None: + """ + Clear the heap. + + >>> rh = RandomizedHeap([3, 1, 3, 7]) + >>> rh.clear() + >>> rh.pop() + Traceback (most recent call last): + ... + IndexError: Can't get top element for the empty heap. + """ + self._root = None + + def to_sorted_list(self) -> list[Any]: + """ + Returns sorted list containing all the values in the heap. + + >>> rh = RandomizedHeap([3, 1, 3, 7]) + >>> rh.to_sorted_list() + [1, 3, 3, 7] + """ + result = [] + while self: + result.append(self.pop()) + + return result + + def __bool__(self) -> bool: + """ + Check if the heap is not empty. + + >>> rh = RandomizedHeap() + >>> bool(rh) + False + >>> rh.insert(1) + >>> bool(rh) + True + >>> rh.clear() + >>> bool(rh) + False + """ + return self._root is not None + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/heap/skew_heap.py b/data_structures/heap/skew_heap.py new file mode 100644 index 000000000000..4e82569444fa --- /dev/null +++ b/data_structures/heap/skew_heap.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +from collections.abc import Iterable, Iterator +from typing import Any, TypeVar + +T = TypeVar("T", bound=bool) + + +class SkewNode[T: bool]: + """ + One node of the skew heap. Contains the value and references to + two children. + """ + + def __init__(self, value: T) -> None: + self._value: T = value + self.left: SkewNode[T] | None = None + self.right: SkewNode[T] | None = None + + @property + def value(self) -> T: + """ + Return the value of the node. + + >>> SkewNode(0).value + 0 + >>> SkewNode(3.14159).value + 3.14159 + >>> SkewNode("hello").value + 'hello' + >>> SkewNode(None).value + + >>> SkewNode(True).value + True + >>> SkewNode([]).value + [] + >>> SkewNode({}).value + {} + >>> SkewNode(set()).value + set() + >>> SkewNode(0.0).value + 0.0 + >>> SkewNode(-1e-10).value + -1e-10 + >>> SkewNode(10).value + 10 + >>> SkewNode(-10.5).value + -10.5 + >>> SkewNode().value + Traceback (most recent call last): + ... + TypeError: SkewNode.__init__() missing 1 required positional argument: 'value' + """ + return self._value + + @staticmethod + def merge( + root1: SkewNode[T] | None, root2: SkewNode[T] | None + ) -> SkewNode[T] | None: + """ + Merge 2 nodes together. + >>> SkewNode.merge(SkewNode(10),SkewNode(-10.5)).value + -10.5 + >>> SkewNode.merge(SkewNode(10),SkewNode(10.5)).value + 10 + >>> SkewNode.merge(SkewNode(10),SkewNode(10)).value + 10 + >>> SkewNode.merge(SkewNode(-100),SkewNode(-10.5)).value + -100 + """ + if not root1: + return root2 + + if not root2: + return root1 + + if root1.value > root2.value: + root1, root2 = root2, root1 + + result = root1 + temp = root1.right + result.right = root1.left + result.left = SkewNode.merge(temp, root2) + + return result + + +class SkewHeap[T: bool]: + """ + A data structure that allows inserting a new value and to pop the smallest + values. Both operations take O(logN) time where N is the size of the + structure. + Wiki: https://en.wikipedia.org/wiki/Skew_heap + Visualization: https://www.cs.usfca.edu/~galles/visualization/SkewHeap.html + + >>> list(SkewHeap([2, 3, 1, 5, 1, 7])) + [1, 1, 2, 3, 5, 7] + + >>> sh = SkewHeap() + >>> sh.pop() + Traceback (most recent call last): + ... + IndexError: Can't get top element for the empty heap. + + >>> sh.insert(1) + >>> sh.insert(-1) + >>> sh.insert(0) + >>> list(sh) + [-1, 0, 1] + """ + + def __init__(self, data: Iterable[T] | None = ()) -> None: + """ + >>> sh = SkewHeap([3, 1, 3, 7]) + >>> list(sh) + [1, 3, 3, 7] + """ + self._root: SkewNode[T] | None = None + if data: + for item in data: + self.insert(item) + + def __bool__(self) -> bool: + """ + Check if the heap is not empty. + + >>> sh = SkewHeap() + >>> bool(sh) + False + >>> sh.insert(1) + >>> bool(sh) + True + >>> sh.clear() + >>> bool(sh) + False + """ + return self._root is not None + + def __iter__(self) -> Iterator[T]: + """ + Returns sorted list containing all the values in the heap. + + >>> sh = SkewHeap([3, 1, 3, 7]) + >>> list(sh) + [1, 3, 3, 7] + """ + result: list[Any] = [] + while self: + result.append(self.pop()) + + # Pushing items back to the heap not to clear it. + for item in result: + self.insert(item) + + return iter(result) + + def insert(self, value: T) -> None: + """ + Insert the value into the heap. + + >>> sh = SkewHeap() + >>> sh.insert(3) + >>> sh.insert(1) + >>> sh.insert(3) + >>> sh.insert(7) + >>> list(sh) + [1, 3, 3, 7] + """ + self._root = SkewNode.merge(self._root, SkewNode(value)) + + def pop(self) -> T | None: + """ + Pop the smallest value from the heap and return it. + + >>> sh = SkewHeap([3, 1, 3, 7]) + >>> sh.pop() + 1 + >>> sh.pop() + 3 + >>> sh.pop() + 3 + >>> sh.pop() + 7 + >>> sh.pop() + Traceback (most recent call last): + ... + IndexError: Can't get top element for the empty heap. + """ + result = self.top() + self._root = ( + SkewNode.merge(self._root.left, self._root.right) if self._root else None + ) + + return result + + def top(self) -> T: + """ + Return the smallest value from the heap. + + >>> sh = SkewHeap() + >>> sh.insert(3) + >>> sh.top() + 3 + >>> sh.insert(1) + >>> sh.top() + 1 + >>> sh.insert(3) + >>> sh.top() + 1 + >>> sh.insert(7) + >>> sh.top() + 1 + """ + if not self._root: + raise IndexError("Can't get top element for the empty heap.") + return self._root.value + + def clear(self) -> None: + """ + Clear the heap. + + >>> sh = SkewHeap([3, 1, 3, 7]) + >>> sh.clear() + >>> sh.pop() + Traceback (most recent call last): + ... + IndexError: Can't get top element for the empty heap. + """ + self._root = None + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/kd_tree/__init__.py b/data_structures/kd_tree/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/kd_tree/build_kdtree.py b/data_structures/kd_tree/build_kdtree.py new file mode 100644 index 000000000000..074a5dac4d42 --- /dev/null +++ b/data_structures/kd_tree/build_kdtree.py @@ -0,0 +1,43 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11532 +# https://github.com/TheAlgorithms/Python/pull/11532 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +from data_structures.kd_tree.kd_node import KDNode + + +def build_kdtree(points: list[list[float]], depth: int = 0) -> KDNode | None: + """ + Builds a KD-Tree from a list of points. + + Args: + points: The list of points to build the KD-Tree from. + depth: The current depth in the tree + (used to determine axis for splitting). + + Returns: + The root node of the KD-Tree, + or None if no points are provided. + """ + if not points: + return None + + k = len(points[0]) # Dimensionality of the points + axis = depth % k + + # Sort point list and choose median as pivot element + points.sort(key=lambda point: point[axis]) + median_idx = len(points) // 2 + + # Create node and construct subtrees + left_points = points[:median_idx] + right_points = points[median_idx + 1 :] + + return KDNode( + point=points[median_idx], + left=build_kdtree(left_points, depth + 1), + right=build_kdtree(right_points, depth + 1), + ) diff --git a/data_structures/kd_tree/example/__init__.py b/data_structures/kd_tree/example/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/kd_tree/example/example_usage.py b/data_structures/kd_tree/example/example_usage.py new file mode 100644 index 000000000000..892c3b8c4a2a --- /dev/null +++ b/data_structures/kd_tree/example/example_usage.py @@ -0,0 +1,46 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11532 +# https://github.com/TheAlgorithms/Python/pull/11532 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +import numpy as np + +from data_structures.kd_tree.build_kdtree import build_kdtree +from data_structures.kd_tree.example.hypercube_points import hypercube_points +from data_structures.kd_tree.nearest_neighbour_search import nearest_neighbour_search + + +def main() -> None: + """ + Demonstrates the use of KD-Tree by building it from random points + in a 10-dimensional hypercube and performing a nearest neighbor search. + """ + num_points: int = 5000 + cube_size: float = 10.0 # Size of the hypercube (edge length) + num_dimensions: int = 10 + + # Generate random points within the hypercube + points: np.ndarray = hypercube_points(num_points, cube_size, num_dimensions) + hypercube_kdtree = build_kdtree(points.tolist()) + + # Generate a random query point within the same space + rng = np.random.default_rng() + query_point: list[float] = rng.random(num_dimensions).tolist() + + # Perform nearest neighbor search + nearest_point, nearest_dist, nodes_visited = nearest_neighbour_search( + hypercube_kdtree, query_point + ) + + # Print the results + print(f"Query point: {query_point}") + print(f"Nearest point: {nearest_point}") + print(f"Distance: {nearest_dist:.4f}") + print(f"Nodes visited: {nodes_visited}") + + +if __name__ == "__main__": + main() diff --git a/data_structures/kd_tree/example/hypercube_points.py b/data_structures/kd_tree/example/hypercube_points.py new file mode 100644 index 000000000000..66744856e6d5 --- /dev/null +++ b/data_structures/kd_tree/example/hypercube_points.py @@ -0,0 +1,29 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11532 +# https://github.com/TheAlgorithms/Python/pull/11532 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +import numpy as np + + +def hypercube_points( + num_points: int, hypercube_size: float, num_dimensions: int +) -> np.ndarray: + """ + Generates random points uniformly distributed within an n-dimensional hypercube. + + Args: + num_points: Number of points to generate. + hypercube_size: Size of the hypercube. + num_dimensions: Number of dimensions of the hypercube. + + Returns: + An array of shape (num_points, num_dimensions) + with generated points. + """ + rng = np.random.default_rng() + shape = (num_points, num_dimensions) + return hypercube_size * rng.random(shape) diff --git a/data_structures/kd_tree/kd_node.py b/data_structures/kd_tree/kd_node.py new file mode 100644 index 000000000000..5a22ef609077 --- /dev/null +++ b/data_structures/kd_tree/kd_node.py @@ -0,0 +1,38 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11532 +# https://github.com/TheAlgorithms/Python/pull/11532 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +from __future__ import annotations + + +class KDNode: + """ + Represents a node in a KD-Tree. + + Attributes: + point: The point stored in this node. + left: The left child node. + right: The right child node. + """ + + def __init__( + self, + point: list[float], + left: KDNode | None = None, + right: KDNode | None = None, + ) -> None: + """ + Initializes a KDNode with the given point and child nodes. + + Args: + point (list[float]): The point stored in this node. + left (Optional[KDNode]): The left child node. + right (Optional[KDNode]): The right child node. + """ + self.point = point + self.left = left + self.right = right diff --git a/data_structures/kd_tree/nearest_neighbour_search.py b/data_structures/kd_tree/nearest_neighbour_search.py new file mode 100644 index 000000000000..8104944c08f0 --- /dev/null +++ b/data_structures/kd_tree/nearest_neighbour_search.py @@ -0,0 +1,79 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11532 +# https://github.com/TheAlgorithms/Python/pull/11532 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +from data_structures.kd_tree.kd_node import KDNode + + +def nearest_neighbour_search( + root: KDNode | None, query_point: list[float] +) -> tuple[list[float] | None, float, int]: + """ + Performs a nearest neighbor search in a KD-Tree for a given query point. + + Args: + root (KDNode | None): The root node of the KD-Tree. + query_point (list[float]): The point for which the nearest neighbor + is being searched. + + Returns: + tuple[list[float] | None, float, int]: + - The nearest point found in the KD-Tree to the query point, + or None if no point is found. + - The squared distance to the nearest point. + - The number of nodes visited during the search. + """ + nearest_point: list[float] | None = None + nearest_dist: float = float("inf") + nodes_visited: int = 0 + + def search(node: KDNode | None, depth: int = 0) -> None: + """ + Recursively searches for the nearest neighbor in the KD-Tree. + + Args: + node: The current node in the KD-Tree. + depth: The current depth in the KD-Tree. + """ + nonlocal nearest_point, nearest_dist, nodes_visited + if node is None: + return + + nodes_visited += 1 + + # Calculate the current distance (squared distance) + current_point = node.point + current_dist = sum( + (query_coord - point_coord) ** 2 + for query_coord, point_coord in zip(query_point, current_point) + ) + + # Update nearest point if the current node is closer + if nearest_point is None or current_dist < nearest_dist: + nearest_point = current_point + nearest_dist = current_dist + + # Determine which subtree to search first (based on axis and query point) + k = len(query_point) # Dimensionality of points + axis = depth % k + + if query_point[axis] <= current_point[axis]: + nearer_subtree = node.left + further_subtree = node.right + else: + nearer_subtree = node.right + further_subtree = node.left + + # Search the nearer subtree first + search(nearer_subtree, depth + 1) + + # If the further subtree has a closer point + if (query_point[axis] - current_point[axis]) ** 2 < nearest_dist: + search(further_subtree, depth + 1) + + search(root, 0) + return nearest_point, nearest_dist, nodes_visited diff --git a/data_structures/kd_tree/tests/__init__.py b/data_structures/kd_tree/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/kd_tree/tests/test_kdtree.py b/data_structures/kd_tree/tests/test_kdtree.py new file mode 100644 index 000000000000..4722aeb477c6 --- /dev/null +++ b/data_structures/kd_tree/tests/test_kdtree.py @@ -0,0 +1,110 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11532 +# https://github.com/TheAlgorithms/Python/pull/11532 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +import numpy as np +import pytest + +from data_structures.kd_tree.build_kdtree import build_kdtree +from data_structures.kd_tree.example.hypercube_points import hypercube_points +from data_structures.kd_tree.kd_node import KDNode +from data_structures.kd_tree.nearest_neighbour_search import nearest_neighbour_search + + +@pytest.mark.parametrize( + ("num_points", "cube_size", "num_dimensions", "depth", "expected_result"), + [ + (0, 10.0, 2, 0, None), # Empty points list + (10, 10.0, 2, 2, KDNode), # Depth = 2, 2D points + (10, 10.0, 3, -2, KDNode), # Depth = -2, 3D points + ], +) +def test_build_kdtree( + num_points, cube_size, num_dimensions, depth, expected_result +) -> None: + """ + Test that KD-Tree is built correctly. + + Cases: + - Empty points list. + - Positive depth value. + - Negative depth value. + """ + points = ( + hypercube_points(num_points, cube_size, num_dimensions).tolist() + if num_points > 0 + else [] + ) + + kdtree = build_kdtree(points, depth=depth) + + if expected_result is None: + # Empty points list case + assert kdtree is None, f"Expected None for empty points list, got {kdtree}" + else: + # Check if root node is not None + assert kdtree is not None, "Expected a KDNode, got None" + + # Check if root has correct dimensions + assert len(kdtree.point) == num_dimensions, ( + f"Expected point dimension {num_dimensions}, got {len(kdtree.point)}" + ) + + # Check that the tree is balanced to some extent (simplistic check) + assert isinstance(kdtree, KDNode), ( + f"Expected KDNode instance, got {type(kdtree)}" + ) + + +def test_nearest_neighbour_search() -> None: + """ + Test the nearest neighbor search function. + """ + num_points = 10 + cube_size = 10.0 + num_dimensions = 2 + points = hypercube_points(num_points, cube_size, num_dimensions) + kdtree = build_kdtree(points.tolist()) + + rng = np.random.default_rng() + query_point = rng.random(num_dimensions).tolist() + + nearest_point, nearest_dist, nodes_visited = nearest_neighbour_search( + kdtree, query_point + ) + + # Check that nearest point is not None + assert nearest_point is not None + + # Check that distance is a non-negative number + assert nearest_dist >= 0 + + # Check that nodes visited is a non-negative integer + assert nodes_visited >= 0 + + +def test_edge_cases() -> None: + """ + Test edge cases such as an empty KD-Tree. + """ + empty_kdtree = build_kdtree([]) + query_point = [0.0] * 2 # Using a default 2D query point + + nearest_point, nearest_dist, nodes_visited = nearest_neighbour_search( + empty_kdtree, query_point + ) + + # With an empty KD-Tree, nearest_point should be None + assert nearest_point is None + assert nearest_dist == float("inf") + assert nodes_visited == 0 + + +if __name__ == "__main__": + import pytest + + pytest.main() diff --git a/data_structures/linked_list/__init__.py b/data_structures/linked_list/__init__.py new file mode 100644 index 000000000000..00ef337a1211 --- /dev/null +++ b/data_structures/linked_list/__init__.py @@ -0,0 +1,133 @@ +""" +Linked Lists consists of Nodes. +Nodes contain data and also may link to other nodes: + - Head Node: First node, the address of the + head node gives us access of the complete list + - Last node: points to null +""" + +from __future__ import annotations + +from typing import Any + + +class Node: + def __init__(self, item: Any, next: Any) -> None: # noqa: A002 + self.item = item + self.next = next + + +class LinkedList: + def __init__(self) -> None: + self.head: Node | None = None + self.size = 0 + + def add(self, item: Any, position: int = 0) -> None: + """ + Add an item to the LinkedList at the specified position. + Default position is 0 (the head). + + Args: + item (Any): The item to add to the LinkedList. + position (int, optional): The position at which to add the item. + Defaults to 0. + + Raises: + ValueError: If the position is negative or out of bounds. + + >>> linked_list = LinkedList() + >>> linked_list.add(1) + >>> linked_list.add(2) + >>> linked_list.add(3) + >>> linked_list.add(4, 2) + >>> print(linked_list) + 3 --> 2 --> 4 --> 1 + + # Test adding to a negative position + >>> linked_list.add(5, -3) + Traceback (most recent call last): + ... + ValueError: Position must be non-negative + + # Test adding to an out-of-bounds position + >>> linked_list.add(5,7) + Traceback (most recent call last): + ... + ValueError: Out of bounds + >>> linked_list.add(5, 4) + >>> print(linked_list) + 3 --> 2 --> 4 --> 1 --> 5 + """ + if position < 0: + raise ValueError("Position must be non-negative") + + if position == 0 or self.head is None: + new_node = Node(item, self.head) + self.head = new_node + else: + current = self.head + for _ in range(position - 1): + current = current.next + if current is None: + raise ValueError("Out of bounds") + new_node = Node(item, current.next) + current.next = new_node + self.size += 1 + + def remove(self) -> Any: + # Switched 'self.is_empty()' to 'self.head is None' + # because mypy was considering the possibility that 'self.head' + # can be None in below else part and giving error + if self.head is None: + return None + else: + item = self.head.item + self.head = self.head.next + self.size -= 1 + return item + + def is_empty(self) -> bool: + return self.head is None + + def __str__(self) -> str: + """ + >>> linked_list = LinkedList() + >>> linked_list.add(23) + >>> linked_list.add(14) + >>> linked_list.add(9) + >>> print(linked_list) + 9 --> 14 --> 23 + """ + if self.is_empty(): + return "" + else: + iterate = self.head + item_str = "" + item_list: list[str] = [] + while iterate: + item_list.append(str(iterate.item)) + iterate = iterate.next + + item_str = " --> ".join(item_list) + + return item_str + + def __len__(self) -> int: + """ + >>> linked_list = LinkedList() + >>> len(linked_list) + 0 + >>> linked_list.add("a") + >>> len(linked_list) + 1 + >>> linked_list.add("b") + >>> len(linked_list) + 2 + >>> _ = linked_list.remove() + >>> len(linked_list) + 1 + >>> _ = linked_list.remove() + >>> len(linked_list) + 0 + """ + return self.size diff --git a/data_structures/linked_list/circular_linked_list.py b/data_structures/linked_list/circular_linked_list.py new file mode 100644 index 000000000000..bb64441d4560 --- /dev/null +++ b/data_structures/linked_list/circular_linked_list.py @@ -0,0 +1,210 @@ +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass +from typing import Any + + +@dataclass +class Node: + data: Any + next_node: Node | None = None + + +@dataclass +class CircularLinkedList: + head: Node | None = None # Reference to the head (first node) + tail: Node | None = None # Reference to the tail (last node) + + def __iter__(self) -> Iterator[Any]: + """ + Iterate through all nodes in the Circular Linked List yielding their data. + Yields: + The data of each node in the linked list. + """ + node = self.head + while node: + yield node.data + node = node.next_node + if node == self.head: + break + + def __len__(self) -> int: + """ + Get the length (number of nodes) in the Circular Linked List. + """ + return sum(1 for _ in self) + + def __repr__(self) -> str: + """ + Generate a string representation of the Circular Linked List. + Returns: + A string of the format "1->2->....->N". + """ + return "->".join(str(item) for item in iter(self)) + + def insert_tail(self, data: Any) -> None: + """ + Insert a node with the given data at the end of the Circular Linked List. + """ + self.insert_nth(len(self), data) + + def insert_head(self, data: Any) -> None: + """ + Insert a node with the given data at the beginning of the Circular Linked List. + """ + self.insert_nth(0, data) + + def insert_nth(self, index: int, data: Any) -> None: + """ + Insert the data of the node at the nth pos in the Circular Linked List. + Args: + index: The index at which the data should be inserted. + data: The data to be inserted. + + Raises: + IndexError: If the index is out of range. + """ + if index < 0 or index > len(self): + raise IndexError("list index out of range.") + new_node: Node = Node(data) + if self.head is None: + new_node.next_node = new_node # First node points to itself + self.tail = self.head = new_node + elif index == 0: # Insert at the head + new_node.next_node = self.head + assert self.tail is not None # List is not empty, tail exists + self.head = self.tail.next_node = new_node + else: + temp: Node | None = self.head + for _ in range(index - 1): + assert temp is not None + temp = temp.next_node + assert temp is not None + new_node.next_node = temp.next_node + temp.next_node = new_node + if index == len(self) - 1: # Insert at the tail + self.tail = new_node + + def delete_front(self) -> Any: + """ + Delete and return the data of the node at the front of the Circular Linked List. + Raises: + IndexError: If the list is empty. + """ + return self.delete_nth(0) + + def delete_tail(self) -> Any: + """ + Delete and return the data of the node at the end of the Circular Linked List. + Returns: + Any: The data of the deleted node. + Raises: + IndexError: If the index is out of range. + """ + return self.delete_nth(len(self) - 1) + + def delete_nth(self, index: int = 0) -> Any: + """ + Delete and return the data of the node at the nth pos in Circular Linked List. + Args: + index (int): The index of the node to be deleted. Defaults to 0. + Returns: + Any: The data of the deleted node. + Raises: + IndexError: If the index is out of range. + """ + if not 0 <= index < len(self): + raise IndexError("list index out of range.") + + assert self.head is not None + assert self.tail is not None + delete_node: Node = self.head + if self.head == self.tail: # Just one node + self.head = self.tail = None + elif index == 0: # Delete head node + assert self.tail.next_node is not None + self.tail.next_node = self.tail.next_node.next_node + self.head = self.head.next_node + else: + temp: Node | None = self.head + for _ in range(index - 1): + assert temp is not None + temp = temp.next_node + assert temp is not None + assert temp.next_node is not None + delete_node = temp.next_node + temp.next_node = temp.next_node.next_node + if index == len(self) - 1: # Delete at tail + self.tail = temp + return delete_node.data + + def is_empty(self) -> bool: + """ + Check if the Circular Linked List is empty. + Returns: + bool: True if the list is empty, False otherwise. + """ + return len(self) == 0 + + +def test_circular_linked_list() -> None: + """ + Test cases for the CircularLinkedList class. + >>> test_circular_linked_list() + """ + circular_linked_list = CircularLinkedList() + assert len(circular_linked_list) == 0 + assert circular_linked_list.is_empty() is True + assert str(circular_linked_list) == "" + + try: + circular_linked_list.delete_front() + raise AssertionError # This should not happen + except IndexError: + assert True # This should happen + + try: + circular_linked_list.delete_tail() + raise AssertionError # This should not happen + except IndexError: + assert True # This should happen + + try: + circular_linked_list.delete_nth(-1) + raise AssertionError + except IndexError: + assert True + + try: + circular_linked_list.delete_nth(0) + raise AssertionError + except IndexError: + assert True + + assert circular_linked_list.is_empty() is True + for i in range(5): + assert len(circular_linked_list) == i + circular_linked_list.insert_nth(i, i + 1) + assert str(circular_linked_list) == "->".join(str(i) for i in range(1, 6)) + + circular_linked_list.insert_tail(6) + assert str(circular_linked_list) == "->".join(str(i) for i in range(1, 7)) + circular_linked_list.insert_head(0) + assert str(circular_linked_list) == "->".join(str(i) for i in range(7)) + + assert circular_linked_list.delete_front() == 0 + assert circular_linked_list.delete_tail() == 6 + assert str(circular_linked_list) == "->".join(str(i) for i in range(1, 6)) + assert circular_linked_list.delete_nth(2) == 3 + + circular_linked_list.insert_nth(2, 3) + assert str(circular_linked_list) == "->".join(str(i) for i in range(1, 6)) + + assert circular_linked_list.is_empty() is False + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/linked_list/deque_doubly.py b/data_structures/linked_list/deque_doubly.py new file mode 100644 index 000000000000..34378e86de2e --- /dev/null +++ b/data_structures/linked_list/deque_doubly.py @@ -0,0 +1,143 @@ +""" +Implementing Deque using DoublyLinkedList ... +Operations: + 1. insertion in the front -> O(1) + 2. insertion in the end -> O(1) + 3. remove from the front -> O(1) + 4. remove from the end -> O(1) +""" + + +class _DoublyLinkedBase: + """A Private class (to be inherited)""" + + class _Node: + __slots__ = "_data", "_next", "_prev" + + def __init__(self, link_p, element, link_n) -> None: + self._prev = link_p + self._data = element + self._next = link_n + + def has_next_and_prev(self) -> str: + return ( + f" Prev -> {self._prev is not None}, Next -> {self._next is not None}" + ) + + def __init__(self) -> None: + self._header = self._Node(None, None, None) + self._trailer = self._Node(None, None, None) + self._header._next = self._trailer + self._trailer._prev = self._header + self._size = 0 + + def __len__(self) -> int: + return self._size + + def is_empty(self): + return self.__len__() == 0 + + def _insert(self, predecessor, e, successor): + # Create new_node by setting it's prev.link -> header + # setting it's next.link -> trailer + new_node = self._Node(predecessor, e, successor) + predecessor._next = new_node + successor._prev = new_node + self._size += 1 + return self + + def _delete(self, node): + predecessor = node._prev + successor = node._next + + predecessor._next = successor + successor._prev = predecessor + self._size -= 1 + temp = node._data + node._prev = node._next = node._data = None + del node + return temp + + +class LinkedDeque(_DoublyLinkedBase): + def first(self): + """return first element + >>> d = LinkedDeque() + >>> d.add_first('A').first() + 'A' + >>> d.add_first('B').first() + 'B' + """ + if self.is_empty(): + raise Exception("List is empty") + return self._header._next._data + + def last(self): + """return last element + >>> d = LinkedDeque() + >>> d.add_last('A').last() + 'A' + >>> d.add_last('B').last() + 'B' + """ + if self.is_empty(): + raise Exception("List is empty") + return self._trailer._prev._data + + # DEque Insert Operations (At the front, At the end) + + def add_first(self, element): + """insertion in the front + >>> LinkedDeque().add_first('AV').first() + 'AV' + """ + return self._insert(self._header, element, self._header._next) + + def add_last(self, element): + """insertion in the end + >>> LinkedDeque().add_last('B').last() + 'B' + """ + return self._insert(self._trailer._prev, element, self._trailer) + + # DEqueu Remove Operations (At the front, At the end) + + def remove_first(self): + """removal from the front + >>> d = LinkedDeque() + >>> d.is_empty() + True + >>> d.remove_first() + Traceback (most recent call last): + ... + IndexError: remove_first from empty list + >>> d.add_first('A') # doctest: +ELLIPSIS + >> d.remove_first() + 'A' + >>> d.is_empty() + True + """ + if self.is_empty(): + raise IndexError("remove_first from empty list") + return self._delete(self._header._next) + + def remove_last(self): + """removal in the end + >>> d = LinkedDeque() + >>> d.is_empty() + True + >>> d.remove_last() + Traceback (most recent call last): + ... + IndexError: remove_first from empty list + >>> d.add_first('A') # doctest: +ELLIPSIS + >> d.remove_last() + 'A' + >>> d.is_empty() + True + """ + if self.is_empty(): + raise IndexError("remove_first from empty list") + return self._delete(self._trailer._prev) diff --git a/data_structures/linked_list/doubly_linked_list.py b/data_structures/linked_list/doubly_linked_list.py new file mode 100644 index 000000000000..0b4ac65ac2b9 --- /dev/null +++ b/data_structures/linked_list/doubly_linked_list.py @@ -0,0 +1,250 @@ +""" +https://en.wikipedia.org/wiki/Doubly_linked_list +""" + +from __future__ import annotations + +from typing import Any + + +class Node: + def __init__(self, data: Any) -> None: + self.data = data + self.previous: Node | None = None + self.next: Node | None = None + + def __str__(self) -> str: + return f"{self.data}" + + +class DoublyLinkedList: + def __init__(self) -> None: + self.head: Node | None = None + self.tail: Node | None = None + + def __iter__(self): + """ + >>> linked_list = DoublyLinkedList() + >>> linked_list.insert_at_head('b') + >>> linked_list.insert_at_head('a') + >>> linked_list.insert_at_tail('c') + >>> tuple(linked_list) + ('a', 'b', 'c') + """ + node = self.head + while node: + yield node.data + node = node.next + + def __str__(self) -> str: + """ + >>> linked_list = DoublyLinkedList() + >>> linked_list.insert_at_tail('a') + >>> linked_list.insert_at_tail('b') + >>> linked_list.insert_at_tail('c') + >>> str(linked_list) + 'a->b->c' + """ + return "->".join([str(item) for item in self]) + + def __len__(self) -> int: + """ + >>> linked_list = DoublyLinkedList() + >>> for i in range(0, 5): + ... linked_list.insert_at_nth(i, i + 1) + >>> len(linked_list) == 5 + True + """ + return sum(1 for _ in self) + + def insert_at_head(self, data) -> None: + self.insert_at_nth(0, data) + + def insert_at_tail(self, data) -> None: + self.insert_at_nth(len(self), data) + + def insert_at_nth(self, index: int, data) -> None: + """ + >>> linked_list = DoublyLinkedList() + >>> linked_list.insert_at_nth(-1, 666) + Traceback (most recent call last): + .... + IndexError: list index out of range + >>> linked_list.insert_at_nth(1, 666) + Traceback (most recent call last): + .... + IndexError: list index out of range + >>> linked_list.insert_at_nth(0, 2) + >>> linked_list.insert_at_nth(0, 1) + >>> linked_list.insert_at_nth(2, 4) + >>> linked_list.insert_at_nth(2, 3) + >>> str(linked_list) + '1->2->3->4' + >>> linked_list.insert_at_nth(5, 5) + Traceback (most recent call last): + .... + IndexError: list index out of range + """ + length = len(self) + + if not 0 <= index <= length: + raise IndexError("list index out of range") + new_node = Node(data) + if self.head is None: + self.head = self.tail = new_node + elif index == 0: + self.head.previous = new_node + new_node.next = self.head + self.head = new_node + elif index == length: + assert self.tail is not None + self.tail.next = new_node + assert self.tail is not None + new_node.previous = self.tail + self.tail = new_node + else: + temp = self.head + assert temp is not None + for _ in range(index): + temp = temp.next + assert temp is not None + assert temp.previous is not None + temp.previous.next = new_node + new_node.previous = temp.previous + new_node.next = temp + temp.previous = new_node + + def delete_head(self): + return self.delete_at_nth(0) + + def delete_tail(self): + return self.delete_at_nth(len(self) - 1) + + def delete_at_nth(self, index: int): + """ + >>> linked_list = DoublyLinkedList() + >>> linked_list.delete_at_nth(0) + Traceback (most recent call last): + .... + IndexError: list index out of range + >>> for i in range(0, 5): + ... linked_list.insert_at_nth(i, i + 1) + >>> linked_list.delete_at_nth(0) == 1 + True + >>> linked_list.delete_at_nth(3) == 5 + True + >>> linked_list.delete_at_nth(1) == 3 + True + >>> str(linked_list) + '2->4' + >>> linked_list.delete_at_nth(2) + Traceback (most recent call last): + .... + IndexError: list index out of range + """ + length = len(self) + + if not 0 <= index <= length - 1: + raise IndexError("list index out of range") + delete_node = self.head # default first node + if length == 1: + self.head = self.tail = None + elif index == 0: + assert self.head is not None + self.head = self.head.next + assert self.head is not None + self.head.previous = None + elif index == length - 1: + assert self.tail is not None + delete_node = self.tail + self.tail = self.tail.previous + assert self.tail is not None + self.tail.next = None + else: + temp = self.head + assert temp is not None + for _ in range(index): + temp = temp.next + assert temp is not None + delete_node = temp + assert temp.next is not None + assert temp.previous is not None + temp.next.previous = temp.previous + temp.previous.next = temp.next + return delete_node.data + + def delete(self, data) -> str: + current = self.head + assert current is not None + + while current.data != data: # Find the position to delete + if current.next: + current = current.next + else: # We have reached the end an no value matches + raise ValueError("No data matching given value") + + if current == self.head: + self.delete_head() + + elif current == self.tail: + self.delete_tail() + + else: # Before: 1 <--> 2(current) <--> 3 + assert current.previous is not None + assert current.next is not None + current.previous.next = current.next # 1 --> 3 + current.next.previous = current.previous # 1 <--> 3 + return data + + def is_empty(self) -> bool: + """ + >>> linked_list = DoublyLinkedList() + >>> linked_list.is_empty() + True + >>> linked_list.insert_at_tail(1) + >>> linked_list.is_empty() + False + """ + return len(self) == 0 + + +def test_doubly_linked_list() -> None: + """ + >>> test_doubly_linked_list() + """ + linked_list = DoublyLinkedList() + assert linked_list.is_empty() is True + assert str(linked_list) == "" + + try: + linked_list.delete_head() + raise AssertionError # This should not happen. + except IndexError: + assert True # This should happen. + + try: + linked_list.delete_tail() + raise AssertionError # This should not happen. + except IndexError: + assert True # This should happen. + + for i in range(10): + assert len(linked_list) == i + linked_list.insert_at_nth(i, i + 1) + assert str(linked_list) == "->".join(str(i) for i in range(1, 11)) + + linked_list.insert_at_head(0) + linked_list.insert_at_tail(11) + assert str(linked_list) == "->".join(str(i) for i in range(12)) + + assert linked_list.delete_head() == 0 + assert linked_list.delete_at_nth(9) == 10 + assert linked_list.delete_tail() == 11 + assert len(linked_list) == 9 + assert str(linked_list) == "->".join(str(i) for i in range(1, 10)) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/data_structures/linked_list/doubly_linked_list_two.py b/data_structures/linked_list/doubly_linked_list_two.py new file mode 100644 index 000000000000..9d963537c25d --- /dev/null +++ b/data_structures/linked_list/doubly_linked_list_two.py @@ -0,0 +1,262 @@ +""" +- A linked list is similar to an array, it holds values. However, links in a linked + list do not have indexes. +- This is an example of a double ended, doubly linked list. +- Each link references the next link and the previous one. +- A Doubly Linked List (DLL) contains an extra pointer, typically called previous + pointer, together with next pointer and data which are there in singly linked list. + - Advantages over SLL - It can be traversed in both forward and backward direction. + Delete operation is more efficient +""" + +from dataclasses import dataclass +from typing import Self, TypeVar + +DataType = TypeVar("DataType") + + +@dataclass +class Node[DataType]: + data: DataType + previous: Self | None = None + next: Self | None = None + + def __str__(self) -> str: + return f"{self.data}" + + +class LinkedListIterator: + def __init__(self, head) -> None: + self.current = head + + def __iter__(self): + return self + + def __next__(self): + if not self.current: + raise StopIteration + value = self.current.data + self.current = self.current.next + return value + + +@dataclass +class LinkedList: + head: Node | None = None # First node in list + tail: Node | None = None # Last node in list + + def __str__(self) -> str: + current = self.head + nodes = [] + while current is not None: + nodes.append(current.data) + current = current.next + return " ".join(str(node) for node in nodes) + + def __contains__(self, value: DataType) -> bool: + current = self.head + while current: + if current.data == value: + return True + current = current.next + return False + + def __iter__(self): + return LinkedListIterator(self.head) + + def get_head_data(self): + if self.head: + return self.head.data + return None + + def get_tail_data(self): + if self.tail: + return self.tail.data + return None + + def set_head(self, node: Node) -> None: + if self.head is None: + self.head = node + self.tail = node + else: + self.insert_before_node(self.head, node) + + def set_tail(self, node: Node) -> None: + if self.tail is None: + self.head = node + self.tail = node + else: + self.insert_after_node(self.tail, node) + + def insert(self, value: DataType) -> None: + node = Node(value) + if self.head is None: + self.set_head(node) + else: + self.set_tail(node) + + def insert_before_node(self, node: Node, node_to_insert: Node) -> None: + node_to_insert.next = node + node_to_insert.previous = node.previous + + if node.previous is None: + self.head = node_to_insert + else: + node.previous.next = node_to_insert + + node.previous = node_to_insert + + def insert_after_node(self, node: Node, node_to_insert: Node) -> None: + node_to_insert.previous = node + node_to_insert.next = node.next + + if node.next is None: + self.tail = node_to_insert + else: + node.next.previous = node_to_insert + + node.next = node_to_insert + + def insert_at_position(self, position: int, value: DataType) -> None: + current_position = 1 + new_node = Node(value) + node = self.head + while node: + if current_position == position: + self.insert_before_node(node, new_node) + return + current_position += 1 + node = node.next + self.set_tail(new_node) + + def get_node(self, item: DataType) -> Node: + node = self.head + while node: + if node.data == item: + return node + node = node.next + raise Exception("Node not found") + + def delete_value(self, value) -> None: + if (node := self.get_node(value)) is not None: + if node == self.head: + self.head = self.head.next + + if node == self.tail: + self.tail = self.tail.previous + + self.remove_node_pointers(node) + + @staticmethod + def remove_node_pointers(node: Node) -> None: + if node.next: + node.next.previous = node.previous + + if node.previous: + node.previous.next = node.next + + node.next = None + node.previous = None + + def is_empty(self) -> bool: + return self.head is None + + +def create_linked_list() -> None: + """ + >>> new_linked_list = LinkedList() + >>> new_linked_list.get_head_data() is None + True + >>> new_linked_list.get_tail_data() is None + True + >>> new_linked_list.is_empty() + True + >>> new_linked_list.insert(10) + >>> new_linked_list.get_head_data() + 10 + >>> new_linked_list.get_tail_data() + 10 + >>> new_linked_list.insert_at_position(position=3, value=20) + >>> new_linked_list.get_head_data() + 10 + >>> new_linked_list.get_tail_data() + 20 + >>> new_linked_list.set_head(Node(1000)) + >>> new_linked_list.get_head_data() + 1000 + >>> new_linked_list.get_tail_data() + 20 + >>> new_linked_list.set_tail(Node(2000)) + >>> new_linked_list.get_head_data() + 1000 + >>> new_linked_list.get_tail_data() + 2000 + >>> for value in new_linked_list: + ... print(value) + 1000 + 10 + 20 + 2000 + >>> new_linked_list.is_empty() + False + >>> for value in new_linked_list: + ... print(value) + 1000 + 10 + 20 + 2000 + >>> 10 in new_linked_list + True + >>> new_linked_list.delete_value(value=10) + >>> 10 in new_linked_list + False + >>> new_linked_list.delete_value(value=2000) + >>> new_linked_list.get_tail_data() + 20 + >>> new_linked_list.delete_value(value=1000) + >>> new_linked_list.get_tail_data() + 20 + >>> new_linked_list.get_head_data() + 20 + >>> for value in new_linked_list: + ... print(value) + 20 + >>> new_linked_list.delete_value(value=20) + >>> for value in new_linked_list: + ... print(value) + >>> for value in range(1,10): + ... new_linked_list.insert(value=value) + >>> for value in new_linked_list: + ... print(value) + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + >>> linked_list = LinkedList() + >>> linked_list.insert_at_position(position=1, value=10) + >>> str(linked_list) + '10' + >>> linked_list.insert_at_position(position=2, value=20) + >>> str(linked_list) + '10 20' + >>> linked_list.insert_at_position(position=1, value=30) + >>> str(linked_list) + '30 10 20' + >>> linked_list.insert_at_position(position=3, value=40) + >>> str(linked_list) + '30 10 40 20' + >>> linked_list.insert_at_position(position=5, value=50) + >>> str(linked_list) + '30 10 40 20 50' + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/linked_list/floyds_cycle_detection.py b/data_structures/linked_list/floyds_cycle_detection.py new file mode 100644 index 000000000000..6c3f13760260 --- /dev/null +++ b/data_structures/linked_list/floyds_cycle_detection.py @@ -0,0 +1,150 @@ +""" +Floyd's cycle detection algorithm is a popular algorithm used to detect cycles +in a linked list. It uses two pointers, a slow pointer and a fast pointer, +to traverse the linked list. The slow pointer moves one node at a time while the fast +pointer moves two nodes at a time. If there is a cycle in the linked list, +the fast pointer will eventually catch up to the slow pointer and they will +meet at the same node. If there is no cycle, the fast pointer will reach the end of +the linked list and the algorithm will terminate. + +For more information: https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_tortoise_and_hare +""" + +from collections.abc import Iterator +from dataclasses import dataclass +from typing import Any, Self + + +@dataclass +class Node: + """ + A class representing a node in a singly linked list. + """ + + data: Any + next_node: Self | None = None + + +@dataclass +class LinkedList: + """ + A class representing a singly linked list. + """ + + head: Node | None = None + + def __iter__(self) -> Iterator: + """ + Iterates through the linked list. + + Returns: + Iterator: An iterator over the linked list. + + Examples: + >>> linked_list = LinkedList() + >>> list(linked_list) + [] + >>> linked_list.add_node(1) + >>> tuple(linked_list) + (1,) + """ + visited = [] + node = self.head + while node: + # Avoid infinite loop in there's a cycle + if node in visited: + return + visited.append(node) + yield node.data + node = node.next_node + + def add_node(self, data: Any) -> None: + """ + Adds a new node to the end of the linked list. + + Args: + data (Any): The data to be stored in the new node. + + Examples: + >>> linked_list = LinkedList() + >>> linked_list.add_node(1) + >>> linked_list.add_node(2) + >>> linked_list.add_node(3) + >>> linked_list.add_node(4) + >>> tuple(linked_list) + (1, 2, 3, 4) + """ + new_node = Node(data) + + if self.head is None: + self.head = new_node + return + + current_node = self.head + while current_node.next_node is not None: + current_node = current_node.next_node + + current_node.next_node = new_node + + def detect_cycle(self) -> bool: + """ + Detects if there is a cycle in the linked list using + Floyd's cycle detection algorithm. + + Returns: + bool: True if there is a cycle, False otherwise. + + Examples: + >>> linked_list = LinkedList() + >>> linked_list.add_node(1) + >>> linked_list.add_node(2) + >>> linked_list.add_node(3) + >>> linked_list.add_node(4) + + >>> linked_list.detect_cycle() + False + + # Create a cycle in the linked list + >>> linked_list.head.next_node.next_node.next_node = linked_list.head.next_node + + >>> linked_list.detect_cycle() + True + """ + if self.head is None: + return False + + slow_pointer: Node | None = self.head + fast_pointer: Node | None = self.head + + while fast_pointer is not None and fast_pointer.next_node is not None: + slow_pointer = slow_pointer.next_node if slow_pointer else None + fast_pointer = fast_pointer.next_node.next_node + if slow_pointer == fast_pointer: + return True + + return False + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + linked_list = LinkedList() + linked_list.add_node(1) + linked_list.add_node(2) + linked_list.add_node(3) + linked_list.add_node(4) + + # Create a cycle in the linked list + # It first checks if the head, next_node, and next_node.next_node attributes of the + # linked list are not None to avoid any potential type errors. + if ( + linked_list.head + and linked_list.head.next_node + and linked_list.head.next_node.next_node + ): + linked_list.head.next_node.next_node.next_node = linked_list.head.next_node + + has_cycle = linked_list.detect_cycle() + print(has_cycle) # Output: True diff --git a/data_structures/linked_list/from_sequence.py b/data_structures/linked_list/from_sequence.py new file mode 100644 index 000000000000..c41b25b211e9 --- /dev/null +++ b/data_structures/linked_list/from_sequence.py @@ -0,0 +1,57 @@ +""" +Recursive Program to create a Linked List from a sequence and +print a string representation of it. +""" + + +class Node: + def __init__(self, data=None) -> None: + self.data = data + self.next = None + + def __repr__(self) -> str: + """Returns a visual representation of the node and all its following nodes.""" + string_rep = "" + temp = self + while temp: + string_rep += f"<{temp.data}> ---> " + temp = temp.next + string_rep += "" + return string_rep + + +def make_linked_list(elements_list: list | tuple) -> Node: + """ + Creates a Linked List from the elements of the given sequence + (list/tuple) and returns the head of the Linked List. + + >>> make_linked_list([]) + Traceback (most recent call last): + ... + ValueError: The Elements List is empty + >>> make_linked_list(()) + Traceback (most recent call last): + ... + ValueError: The Elements List is empty + >>> make_linked_list([1]) + <1> ---> + >>> make_linked_list((1,)) + <1> ---> + >>> make_linked_list([1, 3, 5, 32, 44, 12, 43]) + <1> ---> <3> ---> <5> ---> <32> ---> <44> ---> <12> ---> <43> ---> + >>> make_linked_list((1, 3, 5, 32, 44, 12, 43)) + <1> ---> <3> ---> <5> ---> <32> ---> <44> ---> <12> ---> <43> ---> + """ + + # if elements_list is empty + if not elements_list: + raise ValueError("The Elements List is empty") + + # Set first element as Head + head = Node(elements_list[0]) + current = head + # Loop through elements from position 1 + for data in elements_list[1:]: + current.next = Node(data) + current = current.next + return head diff --git a/data_structures/linked_list/has_loop.py b/data_structures/linked_list/has_loop.py new file mode 100644 index 000000000000..a25204e3c4ce --- /dev/null +++ b/data_structures/linked_list/has_loop.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from collections.abc import Iterator +from typing import Any + + +class ContainsLoopError(Exception): + pass + + +class Node: + def __init__(self, data: Any) -> None: + self.data: Any = data + self.next_node: Node | None = None + + def __iter__(self) -> Iterator[Any]: + node = self + visited = set() + while node: + if node in visited: + raise ContainsLoopError + visited.add(node) + yield node.data + node = node.next_node + + @property + def has_loop(self) -> bool: + """ + A loop is when the exact same Node appears more than once in a linked list. + >>> root_node = Node(1) + >>> root_node.next_node = Node(2) + >>> root_node.next_node.next_node = Node(3) + >>> root_node.next_node.next_node.next_node = Node(4) + >>> root_node.has_loop + False + >>> root_node.next_node.next_node.next_node = root_node.next_node + >>> root_node.has_loop + True + """ + try: + list(self) + return False + except ContainsLoopError: + return True + + +if __name__ == "__main__": + root_node = Node(1) + root_node.next_node = Node(2) + root_node.next_node.next_node = Node(3) + root_node.next_node.next_node.next_node = Node(4) + print(root_node.has_loop) # False + root_node.next_node.next_node.next_node = root_node.next_node + print(root_node.has_loop) # True + + root_node = Node(5) + root_node.next_node = Node(6) + root_node.next_node.next_node = Node(5) + root_node.next_node.next_node.next_node = Node(6) + print(root_node.has_loop) # False + + root_node = Node(1) + print(root_node.has_loop) # False diff --git a/data_structures/linked_list/is_palindrome.py b/data_structures/linked_list/is_palindrome.py new file mode 100644 index 000000000000..da788e3e5045 --- /dev/null +++ b/data_structures/linked_list/is_palindrome.py @@ -0,0 +1,185 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class ListNode: + val: int = 0 + next_node: ListNode | None = None + + +def is_palindrome(head: ListNode | None) -> bool: + """ + Check if a linked list is a palindrome. + + Args: + head: The head of the linked list. + + Returns: + bool: True if the linked list is a palindrome, False otherwise. + + Examples: + >>> is_palindrome(None) + True + + >>> is_palindrome(ListNode(1)) + True + + >>> is_palindrome(ListNode(1, ListNode(2))) + False + + >>> is_palindrome(ListNode(1, ListNode(2, ListNode(1)))) + True + + >>> is_palindrome(ListNode(1, ListNode(2, ListNode(2, ListNode(1))))) + True + """ + if not head: + return True + # split the list to two parts + fast: ListNode | None = head.next_node + slow: ListNode | None = head + while fast and fast.next_node: + fast = fast.next_node.next_node + slow = slow.next_node if slow else None + if slow: + # slow will always be defined, + # adding this check to resolve mypy static check + second = slow.next_node + slow.next_node = None # Don't forget here! But forget still works! + # reverse the second part + node: ListNode | None = None + while second: + nxt = second.next_node + second.next_node = node + node = second + second = nxt + # compare two parts + # second part has the same or one less node + while node and head: + if node.val != head.val: + return False + node = node.next_node + head = head.next_node + return True + + +def is_palindrome_stack(head: ListNode | None) -> bool: + """ + Check if a linked list is a palindrome using a stack. + + Args: + head (ListNode): The head of the linked list. + + Returns: + bool: True if the linked list is a palindrome, False otherwise. + + Examples: + >>> is_palindrome_stack(None) + True + + >>> is_palindrome_stack(ListNode(1)) + True + + >>> is_palindrome_stack(ListNode(1, ListNode(2))) + False + + >>> is_palindrome_stack(ListNode(1, ListNode(2, ListNode(1)))) + True + + >>> is_palindrome_stack(ListNode(1, ListNode(2, ListNode(2, ListNode(1))))) + True + """ + if not head or not head.next_node: + return True + + # 1. Get the midpoint (slow) + slow: ListNode | None = head + fast: ListNode | None = head + while fast and fast.next_node: + fast = fast.next_node.next_node + slow = slow.next_node if slow else None + + # slow will always be defined, + # adding this check to resolve mypy static check + if slow: + stack = [slow.val] + + # 2. Push the second half into the stack + while slow.next_node: + slow = slow.next_node + stack.append(slow.val) + + # 3. Comparison + cur: ListNode | None = head + while stack and cur: + if stack.pop() != cur.val: + return False + cur = cur.next_node + + return True + + +def is_palindrome_dict(head: ListNode | None) -> bool: + """ + Check if a linked list is a palindrome using a dictionary. + + Args: + head (ListNode): The head of the linked list. + + Returns: + bool: True if the linked list is a palindrome, False otherwise. + + Examples: + >>> is_palindrome_dict(None) + True + + >>> is_palindrome_dict(ListNode(1)) + True + + >>> is_palindrome_dict(ListNode(1, ListNode(2))) + False + + >>> is_palindrome_dict(ListNode(1, ListNode(2, ListNode(1)))) + True + + >>> is_palindrome_dict(ListNode(1, ListNode(2, ListNode(2, ListNode(1))))) + True + + >>> is_palindrome_dict( + ... ListNode( + ... 1, ListNode(2, ListNode(1, ListNode(3, ListNode(2, ListNode(1))))) + ... ) + ... ) + False + """ + if not head or not head.next_node: + return True + d: dict[int, list[int]] = {} + pos = 0 + while head: + if head.val in d: + d[head.val].append(pos) + else: + d[head.val] = [pos] + head = head.next_node + pos += 1 + checksum = pos - 1 + middle = 0 + for v in d.values(): + if len(v) % 2 != 0: + middle += 1 + else: + for step, i in enumerate(range(len(v))): + if v[i] + v[len(v) - 1 - step] != checksum: + return False + if middle > 1: + return False + return True + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/linked_list/kth_element_from_end.py b/data_structures/linked_list/kth_element_from_end.py new file mode 100644 index 000000000000..b994d17c4608 --- /dev/null +++ b/data_structures/linked_list/kth_element_from_end.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass +class Node: + item: Any + next: Node | Any = None + + +class LinkedList: + def __init__(self) -> None: + self.head: Node | None = None + self.size = 0 + + def __str__(self) -> str: + """ + >>> linked_list = LinkedList() + >>> linked_list.add(23) + >>> linked_list.add(14) + >>> linked_list.add(9) + >>> print(linked_list) + 9 --> 14 --> 23 + """ + iterate = self.head + item_str = "" + item_list: list[str] = [] + while iterate: + item_list.append(str(iterate.item)) + iterate = iterate.next + + item_str = " --> ".join(item_list) + + return item_str + + def __len__(self) -> int: + """ + >>> linked_list = LinkedList() + >>> len(linked_list) + 0 + >>> linked_list.add("a") + >>> len(linked_list) + 1 + >>> linked_list.add("b") + >>> len(linked_list) + 2 + """ + return self.size + + def add(self, item: Any, position: int = 0) -> None: + """ + Add an item to the LinkedList at the specified position. + Default position is 0 (the head). + + Args: + item (Any): The item to add to the LinkedList. + position (int, optional): The position at which to add the item. + Defaults to 0. + + Raises: + ValueError: If the position is negative or out of bounds. + + >>> linked_list = LinkedList() + >>> linked_list.add(1) + >>> linked_list.add(2) + >>> linked_list.add(3) + >>> linked_list.add(4, 2) + >>> print(linked_list) + 3 --> 2 --> 4 --> 1 + + # Test adding to a negative position + >>> linked_list.add(5, -3) + Traceback (most recent call last): + ... + ValueError: Position must be non-negative + + # Test adding to an out-of-bounds position + >>> linked_list.add(5,7) + Traceback (most recent call last): + ... + ValueError: Out of bounds + >>> linked_list.add(5, 4) + >>> print(linked_list) + 3 --> 2 --> 4 --> 1 --> 5 + """ + if position < 0: + raise ValueError("Position must be non-negative") + + if position == 0 or self.head is None: + new_node = Node(item, self.head) + self.head = new_node + else: + current = self.head + for _ in range(position - 1): + current = current.next + if current is None: + raise ValueError("Out of bounds") + new_node = Node(item, current.next) + current.next = new_node + self.size += 1 + + def kth_element_from_end(self, position: int) -> Any: + """ + Find the kth node element from the end of the Linked List. + This is not a zero-based index search. + If position=1, it will return the last element in the list. + + >>> linked_list = LinkedList() + >>> linked_list.add(1) + >>> linked_list.add(2) + >>> linked_list.add(3) + >>> linked_list.add(4, 2) + >>> linked_list.add(5, 4) + >>> print(linked_list) + 3 --> 2 --> 4 --> 1 --> 5 + >>> linked_list.kth_element_from_end(2) + 1 + >>> linked_list.kth_element_from_end(5) + 3 + >>> linked_list.kth_element_from_end(3) + 4 + >>> linked_list.kth_element_from_end(10) + >>> linked_list.kth_element_from_end(0) + >>> linked_list.kth_element_from_end(-5) + """ + if (self.head is None) or (position <= 0) or (position > self.size): + return None + + slow_pointer = fast_pointer = self.head + + for _ in range(position): + fast_pointer = fast_pointer.next + + while fast_pointer is not None and slow_pointer is not None: + slow_pointer = slow_pointer.next + fast_pointer = fast_pointer.next + + return slow_pointer.item diff --git a/data_structures/linked_list/merge_sort_linked_list.py b/data_structures/linked_list/merge_sort_linked_list.py new file mode 100644 index 000000000000..e43c14b68ed2 --- /dev/null +++ b/data_structures/linked_list/merge_sort_linked_list.py @@ -0,0 +1,162 @@ +from collections.abc import Iterable +from dataclasses import dataclass + + +@dataclass(order=True) +class Node: + """ + A class representing a node in a linked list. + + Attributes: + data: The data stored in the node. + next: A reference to the next node in the linked list. + """ + + data: int + next: Node | None = None + + +def iter_linked_list(head: Node | None) -> Iterable[Node]: + """ + Iterate over the nodes of a linked list. + + Parameters: + head: The head node of the linked list. + + Yields: + Each node in the linked list, one by one. + + Example: + >>> head = Node(3, Node(1, Node(2))) + >>> head # dataclasses provide a nice .__repr__(). + Node(data=3, next=Node(data=1, next=Node(data=2, next=None))) + >>> tuple(iter_linked_list(head)) + (3, 1, 2) + """ + current = head + while current: + yield current.data + current = current.next + + +def get_middle(head: Node | None) -> Node | None: + """ + Find the node before the middle of the linked list + using the slow and fast pointer technique. + + Parameters: + head: The head node of the linked list. + + Returns: + The node before the middle of the linked list, + or None if the list has fewer than 2 nodes. + + Example: + >>> head = Node(1) + >>> head.next = Node(2) + >>> head.next.next = Node(3) + >>> middle = get_middle(head) + >>> middle.data + 2 + """ + if head is None or head.next is None: + return None + + slow: Node | None = head + fast: Node | None = head.next + + while fast is not None and fast.next is not None: + if slow is None: + return None + slow = slow.next + fast = fast.next.next + + return slow + + +def merge(left: Node | None, right: Node | None) -> Node | None: + """ + Merge two sorted linked lists into one sorted linked list. + + Parameters: + left: The head of the first sorted linked list. + right: The head of the second sorted linked list. + + Returns: + The head of the merged sorted linked list. + + Example: + >>> left = Node(1) + >>> left.next = Node(3) + >>> tuple(iter_linked_list(left)) + (1, 3) + >>> right = Node(2) + >>> right.next = Node(4) + >>> tuple(iter_linked_list(right)) + (2, 4) + >>> merged = merge(left, right) + >>> tuple(iter_linked_list(merged)) + (1, 2, 3, 4) + """ + + if left is None: + return right + if right is None: + return left + + if left <= right: + result = left + result.next = merge(left.next, right) + else: + result = right + result.next = merge(left, right.next) + + return result + + +def merge_sort_linked_list(head: Node | None) -> Node | None: + """ + Sort a linked list using the Merge Sort algorithm. + + Parameters: + head: The head node of the linked list to be sorted. + + Returns: + The head node of the sorted linked list. + + Example: + >>> head = Node(4) + >>> head.next = Node(2) + >>> head.next.next = Node(1) + >>> head.next.next.next = Node(3) + >>> tuple(iter_linked_list(head)) + (4, 2, 1, 3) + >>> sorted_head = merge_sort_linked_list(head) + >>> tuple(iter_linked_list(sorted_head)) + (1, 2, 3, 4) + """ + + # Base Case: 0 or 1 node + if head is None or head.next is None: + return head + + # Split the linked list into two halves + middle = get_middle(head) + if middle is None or middle.next is None: + return head + + next_to_middle = middle.next + middle.next = None # Split the list into two parts + + # Recursively sort both halves + left = merge_sort_linked_list(head) + right = merge_sort_linked_list(next_to_middle) + + # Merge sorted halves + return merge(left, right) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/linked_list/merge_two_lists.py b/data_structures/linked_list/merge_two_lists.py new file mode 100644 index 000000000000..e47dbdadcf39 --- /dev/null +++ b/data_structures/linked_list/merge_two_lists.py @@ -0,0 +1,83 @@ +""" +Algorithm that merges two sorted linked lists into one sorted linked list. +""" + +from __future__ import annotations + +from collections.abc import Iterable, Iterator +from dataclasses import dataclass + +test_data_odd = (3, 9, -11, 0, 7, 5, 1, -1) +test_data_even = (4, 6, 2, 0, 8, 10, 3, -2) + + +@dataclass +class Node: + data: int + next_node: Node | None + + +class SortedLinkedList: + def __init__(self, ints: Iterable[int]) -> None: + self.head: Node | None = None + for i in sorted(ints, reverse=True): + self.head = Node(i, self.head) + + def __iter__(self) -> Iterator[int]: + """ + >>> tuple(SortedLinkedList(test_data_odd)) == tuple(sorted(test_data_odd)) + True + >>> tuple(SortedLinkedList(test_data_even)) == tuple(sorted(test_data_even)) + True + """ + node = self.head + while node: + yield node.data + node = node.next_node + + def __len__(self) -> int: + """ + >>> for i in range(3): + ... len(SortedLinkedList(range(i))) == i + True + True + True + >>> len(SortedLinkedList(test_data_odd)) + 8 + """ + return sum(1 for _ in self) + + def __str__(self) -> str: + """ + >>> str(SortedLinkedList([])) + '' + >>> str(SortedLinkedList(test_data_odd)) + '-11 -> -1 -> 0 -> 1 -> 3 -> 5 -> 7 -> 9' + >>> str(SortedLinkedList(test_data_even)) + '-2 -> 0 -> 2 -> 3 -> 4 -> 6 -> 8 -> 10' + """ + return " -> ".join([str(node) for node in self]) + + +def merge_lists( + sll_one: SortedLinkedList, sll_two: SortedLinkedList +) -> SortedLinkedList: + """ + >>> SSL = SortedLinkedList + >>> merged = merge_lists(SSL(test_data_odd), SSL(test_data_even)) + >>> len(merged) + 16 + >>> str(merged) + '-11 -> -2 -> -1 -> 0 -> 0 -> 1 -> 2 -> 3 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10' + >>> list(merged) == list(sorted(test_data_odd + test_data_even)) + True + """ + return SortedLinkedList(list(sll_one) + list(sll_two)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + SSL = SortedLinkedList + print(merge_lists(SSL(test_data_odd), SSL(test_data_even))) diff --git a/data_structures/linked_list/middle_element_of_linked_list.py b/data_structures/linked_list/middle_element_of_linked_list.py new file mode 100644 index 000000000000..330193fef21e --- /dev/null +++ b/data_structures/linked_list/middle_element_of_linked_list.py @@ -0,0 +1,68 @@ +from __future__ import annotations + + +class Node: + def __init__(self, data: int) -> None: + self.data = data + self.next = None + + +class LinkedList: + def __init__(self) -> None: + self.head = None + + def push(self, new_data: int) -> int: + new_node = Node(new_data) + new_node.next = self.head + self.head = new_node + return self.head.data + + def middle_element(self) -> int | None: + """ + >>> link = LinkedList() + >>> link.middle_element() + No element found. + >>> link.push(5) + 5 + >>> link.push(6) + 6 + >>> link.push(8) + 8 + >>> link.push(8) + 8 + >>> link.push(10) + 10 + >>> link.push(12) + 12 + >>> link.push(17) + 17 + >>> link.push(7) + 7 + >>> link.push(3) + 3 + >>> link.push(20) + 20 + >>> link.push(-20) + -20 + >>> link.middle_element() + 12 + >>> + """ + slow_pointer = self.head + fast_pointer = self.head + if self.head: + while fast_pointer and fast_pointer.next: + fast_pointer = fast_pointer.next.next + slow_pointer = slow_pointer.next + return slow_pointer.data + else: + print("No element found.") + return None + + +if __name__ == "__main__": + link = LinkedList() + for _ in range(int(input().strip())): + data = int(input().strip()) + link.push(data) + print(link.middle_element()) diff --git a/data_structures/linked_list/partition_linked_list.py b/data_structures/linked_list/partition_linked_list.py new file mode 100644 index 000000000000..52bc5ed35989 --- /dev/null +++ b/data_structures/linked_list/partition_linked_list.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass +class Node: + item: Any + next: Node | Any = None + + +class LinkedList: + def __init__(self) -> None: + self.head: Node | None = None + self.size = 0 + + def __str__(self) -> str: + """ + >>> linked_list = LinkedList() + >>> linked_list.add(23) + >>> linked_list.add(14) + >>> linked_list.add(9) + >>> print(linked_list) + 9 --> 14 --> 23 + """ + iterate = self.head + item_str = "" + item_list: list[str] = [] + while iterate: + item_list.append(str(iterate.item)) + iterate = iterate.next + + item_str = " --> ".join(item_list) + + return item_str + + def __len__(self) -> int: + """ + >>> linked_list = LinkedList() + >>> len(linked_list) + 0 + >>> linked_list.add("a") + >>> len(linked_list) + 1 + >>> linked_list.add("b") + >>> len(linked_list) + 2 + """ + return self.size + + def add(self, item: Any, position: int = 0) -> None: + """ + Add an item to the LinkedList at the specified position. + Default position is 0 (the head). + + Args: + item (Any): The item to add to the LinkedList. + position (int, optional): The position at which to add the item. + Defaults to 0. + + Raises: + ValueError: If the position is negative or out of bounds. + + >>> linked_list = LinkedList() + >>> linked_list.add(1) + >>> linked_list.add(2) + >>> linked_list.add(3) + >>> linked_list.add(4, 2) + >>> print(linked_list) + 3 --> 2 --> 4 --> 1 + + # Test adding to a negative position + >>> linked_list.add(5, -3) + Traceback (most recent call last): + ... + ValueError: Position must be non-negative + + # Test adding to an out-of-bounds position + >>> linked_list.add(5,7) + Traceback (most recent call last): + ... + ValueError: Out of bounds + >>> linked_list.add(5, 4) + >>> print(linked_list) + 3 --> 2 --> 4 --> 1 --> 5 + """ + if position < 0: + raise ValueError("Position must be non-negative") + + if position == 0 or self.head is None: + new_node = Node(item, self.head) + self.head = new_node + else: + current = self.head + for _ in range(position - 1): + current = current.next + if current is None: + raise ValueError("Out of bounds") + new_node = Node(item, current.next) + current.next = new_node + self.size += 1 + + def partition_liked_list(self, value: int) -> None: + """ + Partition the linked list based on node elements in order. + All nodes with elements less than value should occur on the left, + while those greater than or equal to value should occur on the right. + + >>> linked_list = LinkedList() + >>> linked_list.add(1) + >>> linked_list.add(2) + >>> linked_list.add(3) + >>> linked_list.add(4, 2) + >>> linked_list.add(5, 4) + >>> print(linked_list) + 3 --> 2 --> 4 --> 1 --> 5 + >>> linked_list.partition_liked_list(3) + >>> print(linked_list) + 2 --> 1 --> 3 --> 4 --> 5 + >>> linked_list = LinkedList() + >>> linked_list.add(1) + >>> linked_list.add(2) + >>> linked_list.add(3) + >>> print(linked_list) + 3 --> 2 --> 1 + >>> linked_list.partition_liked_list(3) + >>> print(linked_list) + 2 --> 1 --> 3 + >>> linked_list = LinkedList() + >>> linked_list.add(5) + >>> linked_list.add(5) + >>> linked_list.add(5) + >>> print(linked_list) + 5 --> 5 --> 5 + >>> linked_list.partition_liked_list(5) + >>> print(linked_list) + 5 --> 5 --> 5 + >>> linked_list = LinkedList() + >>> linked_list.add(1, 0) + >>> linked_list.add(2, 1) + >>> linked_list.add(3, 2) + >>> linked_list.add(4, 3) + >>> linked_list.add(5, 4) + >>> print(linked_list) + 1 --> 2 --> 3 --> 4 --> 5 + >>> linked_list.partition_liked_list(6) + >>> print(linked_list) + 1 --> 2 --> 3 --> 4 --> 5 + >>> linked_list = LinkedList() + >>> linked_list.add(1) + >>> print(linked_list) + 1 + >>> linked_list.partition_liked_list(1) + >>> print(linked_list) + 1 + """ + if self.head is None: + return + + less_nodes, greater_nodes = Node(0), Node(0) + current, current_less, current_greater = self.head, less_nodes, greater_nodes + + while current is not None: + if current.item < value: + current_less.next = current + current_less = current_less.next + else: + current_greater.next = current + current_greater = current_greater.next + current = current.next + + current_less.next = greater_nodes.next + current_greater.next = None + self.head = less_nodes.next diff --git a/data_structures/linked_list/print_reverse.py b/data_structures/linked_list/print_reverse.py new file mode 100644 index 000000000000..a023745dee69 --- /dev/null +++ b/data_structures/linked_list/print_reverse.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +from collections.abc import Iterable, Iterator +from dataclasses import dataclass + + +@dataclass +class Node: + data: int + next_node: Node | None = None + + +class LinkedList: + """A class to represent a Linked List. + Use a tail pointer to speed up the append() operation. + """ + + def __init__(self) -> None: + """Initialize a LinkedList with the head node set to None. + >>> linked_list = LinkedList() + >>> (linked_list.head, linked_list.tail) + (None, None) + """ + self.head: Node | None = None + self.tail: Node | None = None # Speeds up the append() operation + + def __iter__(self) -> Iterator[int]: + """Iterate the LinkedList yielding each Node's data. + >>> linked_list = LinkedList() + >>> items = (1, 2, 3, 4, 5) + >>> linked_list.extend(items) + >>> tuple(linked_list) == items + True + """ + node = self.head + while node: + yield node.data + node = node.next_node + + def __repr__(self) -> str: + """Returns a string representation of the LinkedList. + >>> linked_list = LinkedList() + >>> str(linked_list) + '' + >>> linked_list.append(1) + >>> str(linked_list) + '1' + >>> linked_list.extend([2, 3, 4, 5]) + >>> str(linked_list) + '1 -> 2 -> 3 -> 4 -> 5' + """ + return " -> ".join([str(data) for data in self]) + + def append(self, data: int) -> None: + """Appends a new node with the given data to the end of the LinkedList. + >>> linked_list = LinkedList() + >>> str(linked_list) + '' + >>> linked_list.append(1) + >>> str(linked_list) + '1' + >>> linked_list.append(2) + >>> str(linked_list) + '1 -> 2' + """ + if self.tail: + self.tail.next_node = self.tail = Node(data) + else: + self.head = self.tail = Node(data) + + def extend(self, items: Iterable[int]) -> None: + """Appends each item to the end of the LinkedList. + >>> linked_list = LinkedList() + >>> linked_list.extend([]) + >>> str(linked_list) + '' + >>> linked_list.extend([1, 2]) + >>> str(linked_list) + '1 -> 2' + >>> linked_list.extend([3,4]) + >>> str(linked_list) + '1 -> 2 -> 3 -> 4' + """ + for item in items: + self.append(item) + + +def make_linked_list(elements_list: Iterable[int]) -> LinkedList: + """Creates a Linked List from the elements of the given sequence + (list/tuple) and returns the head of the Linked List. + >>> make_linked_list([]) + Traceback (most recent call last): + ... + Exception: The Elements List is empty + >>> make_linked_list([7]) + 7 + >>> make_linked_list(['abc']) + abc + >>> make_linked_list([7, 25]) + 7 -> 25 + """ + if not elements_list: + raise Exception("The Elements List is empty") + + linked_list = LinkedList() + linked_list.extend(elements_list) + return linked_list + + +def in_reverse(linked_list: LinkedList) -> str: + """Prints the elements of the given Linked List in reverse order + >>> in_reverse(LinkedList()) + '' + >>> in_reverse(make_linked_list([69, 88, 73])) + '73 <- 88 <- 69' + """ + return " <- ".join(str(line) for line in reversed(tuple(linked_list))) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + linked_list = make_linked_list((14, 52, 14, 12, 43)) + print(f"Linked List: {linked_list}") + print(f"Reverse List: {in_reverse(linked_list)}") diff --git a/data_structures/linked_list/remove_nth_node_from_end_of_list.py b/data_structures/linked_list/remove_nth_node_from_end_of_list.py new file mode 100644 index 000000000000..4bb048c7ca1c --- /dev/null +++ b/data_structures/linked_list/remove_nth_node_from_end_of_list.py @@ -0,0 +1,142 @@ +"""Learn more about this algorithm: https://www.geeksforgeeks.org/delete-nth-node-from-the-end-of-the-given-linked-list/""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class Node: + def __init__(self, data: int) -> None: + self.data = data + self.next: Node | None = None + + +class LinkedListExceptionError(Exception): + pass + + +class LinkedList: + def __init__(self) -> None: + self.head: Node | None = None + + # pulled from middle_element_of_linked_list + def append(self, new_data: int) -> int: + """ + >>> link = LinkedList() + >>> link.append(5) + 5 + >>> link.append(6) + 6 + """ + new_node = Node(new_data) + new_node.next = self.head + self.head = new_node + if not self.head: + raise LinkedListExceptionError( + "Unexpected error: Head of linked list is None after append operation." + ) + return self.head.data + + def view(self) -> str: + """ + >>> link = LinkedList() + >>> link.append(5) + 5 + >>> link.append(6) + 6 + >>> link.view() + '6->5' + """ + ret = "" + temp = self.head + while temp is not None: + ret += str(temp.data) + ret += "->" + temp = temp.next + return ret[:-2] + + def remove_nth_from_end(self, position_from_end: int) -> Node | None: + """ + >>> link = LinkedList() + >>> link.remove_nth_from_end(3) + Traceback (most recent call last): + ... + IndexError: No element found. + >>> link.append(5) + 5 + >>> link.append(6) + 6 + >>> link.append(8) + 8 + >>> link.append(8) + 8 + >>> link.append(10) + 10 + >>> link.append(12) + 12 + >>> link.append(17) + 17 + >>> link.remove_nth_from_end(100) + Traceback (most recent call last): + ... + IndexError: Index out of bounds error. + >>> link.append(-25) + -25 + >>> link.append(-20) + -20 + >>> link.remove_nth_from_end(3) + Node() + >>> + """ + # want to have two pointers, one at the start and the other k nodes forward + # We could complete this in one pass if we stored a self.size variable + if not self.head: + raise IndexError("No element found.") + + size = 0 + current: Node | None = self.head + while current: + size += 1 + current = current.next + + if position_from_end > size or position_from_end <= 0: + raise IndexError("Index out of bounds error.") + return None + + first: Node | None = self.head + for _i in range(position_from_end): + if first: + first = first.next + + # This condition checks if position_from_end is equal to the size of the list. + # If it is, then we simply delete the head node of the list. + if not first: + self.head = self.head.next if self.head else None + return self.head + + second: Node | None = self.head + prev: Node | None = None + + while first: + first = first.next + prev = second + if second: + second = second.next + + if prev and second: + prev.next = second.next + + return self.head + + +if __name__ == "__main__": + link = LinkedList() + for _ in range(int(input().strip())): + data = int(input().strip()) + link.append(data) + print("Before list: ") + print(link.view()) + print(link.remove_nth_from_end(int(input().strip()))) + print("After list: ") + print(link.view()) diff --git a/data_structures/linked_list/reverse_k_group.py b/data_structures/linked_list/reverse_k_group.py new file mode 100644 index 000000000000..5fc45491a540 --- /dev/null +++ b/data_structures/linked_list/reverse_k_group.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from collections.abc import Iterable, Iterator +from dataclasses import dataclass + + +@dataclass +class Node: + data: int + next_node: Node | None = None + + +class LinkedList: + def __init__(self, ints: Iterable[int]) -> None: + self.head: Node | None = None + for i in ints: + self.append(i) + + def __iter__(self) -> Iterator[int]: + """ + >>> ints = [] + >>> list(LinkedList(ints)) == ints + True + >>> ints = tuple(range(5)) + >>> tuple(LinkedList(ints)) == ints + True + """ + node = self.head + while node: + yield node.data + node = node.next_node + + def __len__(self) -> int: + """ + >>> for i in range(3): + ... len(LinkedList(range(i))) == i + True + True + True + >>> len(LinkedList("abcdefgh")) + 8 + """ + return sum(1 for _ in self) + + def __str__(self) -> str: + """ + >>> str(LinkedList([])) + '' + >>> str(LinkedList(range(5))) + '0 -> 1 -> 2 -> 3 -> 4' + """ + return " -> ".join([str(node) for node in self]) + + def append(self, data: int) -> None: + """ + >>> ll = LinkedList([1, 2]) + >>> tuple(ll) + (1, 2) + >>> ll.append(3) + >>> tuple(ll) + (1, 2, 3) + >>> ll.append(4) + >>> tuple(ll) + (1, 2, 3, 4) + >>> len(ll) + 4 + """ + if not self.head: + self.head = Node(data) + return + node = self.head + while node.next_node: + node = node.next_node + node.next_node = Node(data) + + def reverse_k_nodes(self, group_size: int) -> None: + """ + reverse nodes within groups of size k + >>> ll = LinkedList([1, 2, 3, 4, 5]) + >>> ll.reverse_k_nodes(2) + >>> tuple(ll) + (2, 1, 4, 3, 5) + >>> str(ll) + '2 -> 1 -> 4 -> 3 -> 5' + """ + if self.head is None or self.head.next_node is None: + return + + length = len(self) + dummy_head = Node(0) + dummy_head.next_node = self.head + previous_node = dummy_head + + while length >= group_size: + current_node = previous_node.next_node + assert current_node + next_node = current_node.next_node + for _ in range(1, group_size): + assert next_node, current_node + current_node.next_node = next_node.next_node + assert previous_node + next_node.next_node = previous_node.next_node + previous_node.next_node = next_node + next_node = current_node.next_node + previous_node = current_node + length -= group_size + self.head = dummy_head.next_node + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + ll = LinkedList([1, 2, 3, 4, 5]) + print(f"Original Linked List: {ll}") + k = 2 + ll.reverse_k_nodes(k) + print(f"After reversing groups of size {k}: {ll}") diff --git a/data_structures/linked_list/rotate_to_the_right.py b/data_structures/linked_list/rotate_to_the_right.py new file mode 100644 index 000000000000..6b1c54f4be4d --- /dev/null +++ b/data_structures/linked_list/rotate_to_the_right.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class Node: + data: int + next_node: Node | None = None + + +def print_linked_list(head: Node | None) -> None: + """ + Print the entire linked list iteratively. + + This function prints the elements of a linked list separated by '->'. + + Parameters: + head (Node | None): The head of the linked list to be printed, + or None if the linked list is empty. + + >>> head = insert_node(None, 0) + >>> head = insert_node(head, 2) + >>> head = insert_node(head, 1) + >>> print_linked_list(head) + 0->2->1 + >>> head = insert_node(head, 4) + >>> head = insert_node(head, 5) + >>> print_linked_list(head) + 0->2->1->4->5 + """ + if head is None: + return + while head.next_node is not None: + print(head.data, end="->") + head = head.next_node + print(head.data) + + +def insert_node(head: Node | None, data: int) -> Node: + """ + Insert a new node at the end of a linked list and return the new head. + + Parameters: + head (Node | None): The head of the linked list. + data (int): The data to be inserted into the new node. + + Returns: + Node: The new head of the linked list. + + >>> head = insert_node(None, 10) + >>> head = insert_node(head, 9) + >>> head = insert_node(head, 8) + >>> print_linked_list(head) + 10->9->8 + """ + new_node = Node(data) + # If the linked list is empty, the new_node becomes the head + if head is None: + return new_node + + temp_node = head + while temp_node.next_node: + temp_node = temp_node.next_node + + temp_node.next_node = new_node + return head + + +def rotate_to_the_right(head: Node, places: int) -> Node: + """ + Rotate a linked list to the right by places times. + + Parameters: + head: The head of the linked list. + places: The number of places to rotate. + + Returns: + Node: The head of the rotated linked list. + + >>> rotate_to_the_right(None, places=1) + Traceback (most recent call last): + ... + ValueError: The linked list is empty. + >>> head = insert_node(None, 1) + >>> rotate_to_the_right(head, places=1) == head + True + >>> head = insert_node(None, 1) + >>> head = insert_node(head, 2) + >>> head = insert_node(head, 3) + >>> head = insert_node(head, 4) + >>> head = insert_node(head, 5) + >>> new_head = rotate_to_the_right(head, places=2) + >>> print_linked_list(new_head) + 4->5->1->2->3 + """ + # Check if the list is empty or has only one element + if not head: + raise ValueError("The linked list is empty.") + + if head.next_node is None: + return head + + # Calculate the length of the linked list + length = 1 + temp_node = head + while temp_node.next_node is not None: + length += 1 + temp_node = temp_node.next_node + + # Adjust the value of places to avoid places longer than the list. + places %= length + + if places == 0: + return head # As no rotation is needed. + + # Find the new head position after rotation. + new_head_index = length - places + + # Traverse to the new head position + temp_node = head + for _ in range(new_head_index - 1): + assert temp_node.next_node + temp_node = temp_node.next_node + + # Update pointers to perform rotation + assert temp_node.next_node + new_head = temp_node.next_node + temp_node.next_node = None + temp_node = new_head + while temp_node.next_node: + temp_node = temp_node.next_node + temp_node.next_node = head + + assert new_head + return new_head + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + head = insert_node(None, 5) + head = insert_node(head, 1) + head = insert_node(head, 2) + head = insert_node(head, 4) + head = insert_node(head, 3) + + print("Original list: ", end="") + print_linked_list(head) + + places = 3 + new_head = rotate_to_the_right(head, places) + + print(f"After {places} iterations: ", end="") + print_linked_list(new_head) diff --git a/data_structures/linked_list/singly_linked_list.py b/data_structures/linked_list/singly_linked_list.py new file mode 100644 index 000000000000..e7d1bcd1c2c8 --- /dev/null +++ b/data_structures/linked_list/singly_linked_list.py @@ -0,0 +1,536 @@ +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass +from typing import Any + + +@dataclass +class Node: + """ + Create and initialize Node class instance. + >>> Node(20) + Node(20) + >>> Node("Hello, world!") + Node(Hello, world!) + >>> Node(None) + Node(None) + >>> Node(True) + Node(True) + """ + + data: Any + next_node: Node | None = None + + def __repr__(self) -> str: + """ + Get the string representation of this node. + >>> Node(10).__repr__() + 'Node(10)' + >>> repr(Node(10)) + 'Node(10)' + >>> str(Node(10)) + 'Node(10)' + >>> Node(10) + Node(10) + """ + return f"Node({self.data})" + + +class LinkedList: + def __init__(self) -> None: + """ + Create and initialize LinkedList class instance. + >>> linked_list = LinkedList() + >>> linked_list.head is None + True + """ + self.head: Node | None = None + + def __iter__(self) -> Iterator[Any]: + """ + This function is intended for iterators to access + and iterate through data inside linked list. + >>> linked_list = LinkedList() + >>> linked_list.insert_tail("tail") + >>> linked_list.insert_tail("tail_1") + >>> linked_list.insert_tail("tail_2") + >>> for node in linked_list: # __iter__ used here. + ... node + 'tail' + 'tail_1' + 'tail_2' + """ + node = self.head + while node: + yield node.data + node = node.next_node + + def __len__(self) -> int: + """ + Return length of linked list i.e. number of nodes + >>> linked_list = LinkedList() + >>> len(linked_list) + 0 + >>> linked_list.insert_tail("tail") + >>> len(linked_list) + 1 + >>> linked_list.insert_head("head") + >>> len(linked_list) + 2 + >>> _ = linked_list.delete_tail() + >>> len(linked_list) + 1 + >>> _ = linked_list.delete_head() + >>> len(linked_list) + 0 + """ + return sum(1 for _ in self) + + def __repr__(self) -> str: + """ + String representation/visualization of a Linked Lists + >>> linked_list = LinkedList() + >>> linked_list.insert_tail(1) + >>> linked_list.insert_tail(3) + >>> linked_list.__repr__() + '1 -> 3' + >>> repr(linked_list) + '1 -> 3' + >>> str(linked_list) + '1 -> 3' + >>> linked_list.insert_tail(5) + >>> f"{linked_list}" + '1 -> 3 -> 5' + """ + return " -> ".join([str(item) for item in self]) + + def __getitem__(self, index: int) -> Any: + """ + Indexing Support. Used to get a node at particular position + >>> linked_list = LinkedList() + >>> for i in range(0, 10): + ... linked_list.insert_nth(i, i) + >>> all(str(linked_list[i]) == str(i) for i in range(0, 10)) + True + >>> linked_list[-10] + Traceback (most recent call last): + ... + ValueError: list index out of range. + >>> linked_list[len(linked_list)] + Traceback (most recent call last): + ... + ValueError: list index out of range. + """ + if not 0 <= index < len(self): + raise ValueError("list index out of range.") + for i, node in enumerate(self): + if i == index: + return node + return None + + # Used to change the data of a particular node + def __setitem__(self, index: int, data: Any) -> None: + """ + >>> linked_list = LinkedList() + >>> for i in range(0, 10): + ... linked_list.insert_nth(i, i) + >>> linked_list[0] = 666 + >>> linked_list[0] + 666 + >>> linked_list[5] = -666 + >>> linked_list[5] + -666 + >>> linked_list[-10] = 666 + Traceback (most recent call last): + ... + ValueError: list index out of range. + >>> linked_list[len(linked_list)] = 666 + Traceback (most recent call last): + ... + ValueError: list index out of range. + """ + if not 0 <= index < len(self): + raise ValueError("list index out of range.") + current = self.head + assert current is not None + for _ in range(index): + current = current.next_node + assert current is not None + current.data = data + + def insert_tail(self, data: Any) -> None: + """ + Insert data to the end of linked list. + >>> linked_list = LinkedList() + >>> linked_list.insert_tail("tail") + >>> linked_list + tail + >>> linked_list.insert_tail("tail_2") + >>> linked_list + tail -> tail_2 + >>> linked_list.insert_tail("tail_3") + >>> linked_list + tail -> tail_2 -> tail_3 + """ + self.insert_nth(len(self), data) + + def insert_head(self, data: Any) -> None: + """ + Insert data to the beginning of linked list. + >>> linked_list = LinkedList() + >>> linked_list.insert_head("head") + >>> linked_list + head + >>> linked_list.insert_head("head_2") + >>> linked_list + head_2 -> head + >>> linked_list.insert_head("head_3") + >>> linked_list + head_3 -> head_2 -> head + """ + self.insert_nth(0, data) + + def insert_nth(self, index: int, data: Any) -> None: + """ + Insert data at given index. + >>> linked_list = LinkedList() + >>> linked_list.insert_tail("first") + >>> linked_list.insert_tail("second") + >>> linked_list.insert_tail("third") + >>> linked_list + first -> second -> third + >>> linked_list.insert_nth(1, "fourth") + >>> linked_list + first -> fourth -> second -> third + >>> linked_list.insert_nth(3, "fifth") + >>> linked_list + first -> fourth -> second -> fifth -> third + """ + if not 0 <= index <= len(self): + raise IndexError("list index out of range") + new_node = Node(data) + if self.head is None: + self.head = new_node + elif index == 0: + new_node.next_node = self.head # link new_node to head + self.head = new_node + else: + temp = self.head + assert temp is not None + for _ in range(index - 1): + temp = temp.next_node + assert temp is not None + new_node.next_node = temp.next_node + temp.next_node = new_node + + def print_list(self) -> None: # print every node data + """ + This method prints every node data. + >>> linked_list = LinkedList() + >>> linked_list.insert_tail("first") + >>> linked_list.insert_tail("second") + >>> linked_list.insert_tail("third") + >>> linked_list + first -> second -> third + """ + print(self) + + def delete_head(self) -> Any: + """ + Delete the first node and return the + node's data. + >>> linked_list = LinkedList() + >>> linked_list.insert_tail("first") + >>> linked_list.insert_tail("second") + >>> linked_list.insert_tail("third") + >>> linked_list + first -> second -> third + >>> linked_list.delete_head() + 'first' + >>> linked_list + second -> third + >>> linked_list.delete_head() + 'second' + >>> linked_list + third + >>> linked_list.delete_head() + 'third' + >>> linked_list.delete_head() + Traceback (most recent call last): + ... + IndexError: List index out of range. + """ + return self.delete_nth(0) + + def delete_tail(self) -> Any: # delete from tail + """ + Delete the tail end node and return the + node's data. + >>> linked_list = LinkedList() + >>> linked_list.insert_tail("first") + >>> linked_list.insert_tail("second") + >>> linked_list.insert_tail("third") + >>> linked_list + first -> second -> third + >>> linked_list.delete_tail() + 'third' + >>> linked_list + first -> second + >>> linked_list.delete_tail() + 'second' + >>> linked_list + first + >>> linked_list.delete_tail() + 'first' + >>> linked_list.delete_tail() + Traceback (most recent call last): + ... + IndexError: List index out of range. + """ + return self.delete_nth(len(self) - 1) + + def delete_nth(self, index: int = 0) -> Any: + """ + Delete node at given index and return the + node's data. + >>> linked_list = LinkedList() + >>> linked_list.insert_tail("first") + >>> linked_list.insert_tail("second") + >>> linked_list.insert_tail("third") + >>> linked_list + first -> second -> third + >>> linked_list.delete_nth(1) # delete middle + 'second' + >>> linked_list + first -> third + >>> linked_list.delete_nth(5) # this raises error + Traceback (most recent call last): + ... + IndexError: List index out of range. + >>> linked_list.delete_nth(-1) # this also raises error + Traceback (most recent call last): + ... + IndexError: List index out of range. + """ + if not 0 <= index <= len(self) - 1: # test if index is valid + raise IndexError("List index out of range.") + delete_node = self.head # default first node + if index == 0: + self.head = self.head.next_node + else: + temp = self.head + assert temp is not None + for _ in range(index - 1): + temp = temp.next_node + assert temp is not None + delete_node = temp.next_node + assert delete_node is not None + temp.next_node = delete_node.next_node + return delete_node.data + + def is_empty(self) -> bool: + """ + Check if linked list is empty. + >>> linked_list = LinkedList() + >>> linked_list.is_empty() + True + >>> linked_list.insert_head("first") + >>> linked_list.is_empty() + False + """ + return self.head is None + + def reverse(self) -> None: + """ + This reverses the linked list order. + >>> linked_list = LinkedList() + >>> linked_list.insert_tail("first") + >>> linked_list.insert_tail("second") + >>> linked_list.insert_tail("third") + >>> linked_list + first -> second -> third + >>> linked_list.reverse() + >>> linked_list + third -> second -> first + """ + prev = None + current = self.head + + while current: + # Store the current node's next node. + next_node = current.next_node + # Make the current node's next_node point backwards + current.next_node = prev + # Make the previous node be the current node + prev = current + # Make the current node the next_node node (to progress iteration) + current = next_node + # Return prev in order to put the head at the end + self.head = prev + + +def test_singly_linked_list() -> None: + """ + >>> test_singly_linked_list() + """ + linked_list = LinkedList() + assert linked_list.is_empty() is True + assert str(linked_list) == "" + + try: + linked_list.delete_head() + raise AssertionError # This should not happen. + except IndexError: + assert True # This should happen. + + try: + linked_list.delete_tail() + raise AssertionError # This should not happen. + except IndexError: + assert True # This should happen. + + for i in range(10): + assert len(linked_list) == i + linked_list.insert_nth(i, i + 1) + assert str(linked_list) == " -> ".join(str(i) for i in range(1, 11)) + + linked_list.insert_head(0) + linked_list.insert_tail(11) + assert str(linked_list) == " -> ".join(str(i) for i in range(12)) + + assert linked_list.delete_head() == 0 + assert linked_list.delete_nth(9) == 10 + assert linked_list.delete_tail() == 11 + assert len(linked_list) == 9 + assert str(linked_list) == " -> ".join(str(i) for i in range(1, 10)) + + assert all(linked_list[i] == i + 1 for i in range(9)) is True + + for i in range(9): + linked_list[i] = -i + assert all(linked_list[i] == -i for i in range(9)) is True + + linked_list.reverse() + assert str(linked_list) == " -> ".join(str(i) for i in range(-8, 1)) + + +def test_singly_linked_list_2() -> None: + """ + This section of the test used varying data types for input. + >>> test_singly_linked_list_2() + """ + test_input = [ + -9, + 100, + Node(77345112), + "dlrow olleH", + 7, + 5555, + 0, + -192.55555, + "Hello, world!", + 77.9, + Node(10), + None, + None, + 12.20, + ] + linked_list = LinkedList() + + for i in test_input: + linked_list.insert_tail(i) + + # Check if it's empty or not + assert linked_list.is_empty() is False + assert ( + str(linked_list) + == "-9 -> 100 -> Node(77345112) -> dlrow olleH -> 7 -> 5555 -> " + "0 -> -192.55555 -> Hello, world! -> 77.9 -> Node(10) -> None -> None -> 12.2" + ) + + # Delete the head + result = linked_list.delete_head() + assert result == -9 + assert ( + str(linked_list) == "100 -> Node(77345112) -> dlrow olleH -> 7 -> 5555 -> 0 -> " + "-192.55555 -> Hello, world! -> 77.9 -> Node(10) -> None -> None -> 12.2" + ) + + # Delete the tail + result = linked_list.delete_tail() + assert result == 12.2 + assert ( + str(linked_list) == "100 -> Node(77345112) -> dlrow olleH -> 7 -> 5555 -> 0 -> " + "-192.55555 -> Hello, world! -> 77.9 -> Node(10) -> None -> None" + ) + + # Delete a node in specific location in linked list + result = linked_list.delete_nth(10) + assert result is None + assert ( + str(linked_list) == "100 -> Node(77345112) -> dlrow olleH -> 7 -> 5555 -> 0 -> " + "-192.55555 -> Hello, world! -> 77.9 -> Node(10) -> None" + ) + + # Add a Node instance to its head + linked_list.insert_head(Node("Hello again, world!")) + assert ( + str(linked_list) + == "Node(Hello again, world!) -> 100 -> Node(77345112) -> dlrow olleH -> " + "7 -> 5555 -> 0 -> -192.55555 -> Hello, world! -> 77.9 -> Node(10) -> None" + ) + + # Add None to its tail + linked_list.insert_tail(None) + assert ( + str(linked_list) + == "Node(Hello again, world!) -> 100 -> Node(77345112) -> dlrow olleH -> 7 -> " + "5555 -> 0 -> -192.55555 -> Hello, world! -> 77.9 -> Node(10) -> None -> None" + ) + + # Reverse the linked list + linked_list.reverse() + assert ( + str(linked_list) + == "None -> None -> Node(10) -> 77.9 -> Hello, world! -> -192.55555 -> 0 -> " + "5555 -> 7 -> dlrow olleH -> Node(77345112) -> 100 -> Node(Hello again, world!)" + ) + + +def main() -> None: + from doctest import testmod + + testmod() + + linked_list = LinkedList() + linked_list.insert_head(input("Inserting 1st at head ").strip()) + linked_list.insert_head(input("Inserting 2nd at head ").strip()) + print("\nPrint list:") + linked_list.print_list() + linked_list.insert_tail(input("\nInserting 1st at tail ").strip()) + linked_list.insert_tail(input("Inserting 2nd at tail ").strip()) + print("\nPrint list:") + linked_list.print_list() + print("\nDelete head") + linked_list.delete_head() + print("Delete tail") + linked_list.delete_tail() + print("\nPrint list:") + linked_list.print_list() + print("\nReverse linked list") + linked_list.reverse() + print("\nPrint list:") + linked_list.print_list() + print("\nString representation of linked list:") + print(linked_list) + print("\nReading/changing Node data using indexing:") + print(f"Element at Position 1: {linked_list[1]}") + linked_list[1] = input("Enter New Value: ").strip() + print("New list:") + print(linked_list) + print(f"length of linked_list is : {len(linked_list)}") + + +if __name__ == "__main__": + main() diff --git a/data_structures/linked_list/skip_list.py b/data_structures/linked_list/skip_list.py new file mode 100644 index 000000000000..214f32b55eda --- /dev/null +++ b/data_structures/linked_list/skip_list.py @@ -0,0 +1,448 @@ +""" +Based on "Skip Lists: A Probabilistic Alternative to Balanced Trees" by William Pugh +https://epaperpress.com/sortsearch/download/skiplist.pdf +""" + +from __future__ import annotations + +from itertools import pairwise +from random import random +from typing import TypeVar + +KT = TypeVar("KT") +VT = TypeVar("VT") + + +class Node[KT, VT]: + def __init__(self, key: KT | str = "root", value: VT | None = None) -> None: + self.key = key + self.value = value + self.forward: list[Node[KT, VT]] = [] + + def __repr__(self) -> str: + """ + :return: Visual representation of Node + + >>> node = Node("Key", 2) + >>> repr(node) + 'Node(Key: 2)' + """ + + return f"Node({self.key}: {self.value})" + + @property + def level(self) -> int: + """ + :return: Number of forward references + + >>> node = Node("Key", 2) + >>> node.level + 0 + >>> node.forward.append(Node("Key2", 4)) + >>> node.level + 1 + >>> node.forward.append(Node("Key3", 6)) + >>> node.level + 2 + """ + + return len(self.forward) + + +class SkipList[KT, VT]: + def __init__(self, p: float = 0.5, max_level: int = 16) -> None: + self.head: Node[KT, VT] = Node[KT, VT]() + self.level = 0 + self.p = p + self.max_level = max_level + + def __str__(self) -> str: + """ + :return: Visual representation of SkipList + + >>> skip_list = SkipList() + >>> print(skip_list) + SkipList(level=0) + >>> skip_list.insert("Key1", "Value") + >>> print(skip_list) # doctest: +ELLIPSIS + SkipList(level=... + [root]--... + [Key1]--Key1... + None *... + >>> skip_list.insert("Key2", "OtherValue") + >>> print(skip_list) # doctest: +ELLIPSIS + SkipList(level=... + [root]--... + [Key1]--Key1... + [Key2]--Key2... + None *... + """ + + items = list(self) + + if len(items) == 0: + return f"SkipList(level={self.level})" + + label_size = max((len(str(item)) for item in items), default=4) + label_size = max(label_size, 4) + 4 + + node = self.head + lines = [] + + forwards = node.forward.copy() + lines.append(f"[{node.key}]".ljust(label_size, "-") + "* " * len(forwards)) + lines.append(" " * label_size + "| " * len(forwards)) + + while len(node.forward) != 0: + node = node.forward[0] + + lines.append( + f"[{node.key}]".ljust(label_size, "-") + + " ".join(str(n.key) if n.key == node.key else "|" for n in forwards) + ) + lines.append(" " * label_size + "| " * len(forwards)) + forwards[: node.level] = node.forward + + lines.append("None".ljust(label_size) + "* " * len(forwards)) + return f"SkipList(level={self.level})\n" + "\n".join(lines) + + def __iter__(self): + node = self.head + + while len(node.forward) != 0: + yield node.forward[0].key + node = node.forward[0] + + def random_level(self) -> int: + """ + :return: Random level from [1, self.max_level] interval. + Higher values are less likely. + """ + + level = 1 + while random() < self.p and level < self.max_level: + level += 1 + + return level + + def _locate_node(self, key) -> tuple[Node[KT, VT] | None, list[Node[KT, VT]]]: + """ + :param key: Searched key, + :return: Tuple with searched node (or None if given key is not present) + and list of nodes that refer (if key is present) of should refer to + given node. + """ + + # Nodes with refer or should refer to output node + update_vector = [] + + node = self.head + + for i in reversed(range(self.level)): + # i < node.level - When node level is lesser than `i` decrement `i`. + # node.forward[i].key < key - Jumping to node with key value higher + # or equal to searched key would result + # in skipping searched key. + while i < node.level and node.forward[i].key < key: + node = node.forward[i] + # Each leftmost node (relative to searched node) will potentially have to + # be updated. + update_vector.append(node) + + update_vector.reverse() # Note that we were inserting values in reverse order. + + # len(node.forward) != 0 - If current node doesn't contain any further + # references then searched key is not present. + # node.forward[0].key == key - Next node key should be equal to search key + # if key is present. + if len(node.forward) != 0 and node.forward[0].key == key: + return node.forward[0], update_vector + else: + return None, update_vector + + def delete(self, key: KT) -> None: + """ + :param key: Key to remove from list. + + >>> skip_list = SkipList() + >>> skip_list.insert(2, "Two") + >>> skip_list.insert(1, "One") + >>> skip_list.insert(3, "Three") + >>> list(skip_list) + [1, 2, 3] + >>> skip_list.delete(2) + >>> list(skip_list) + [1, 3] + """ + + node, update_vector = self._locate_node(key) + + if node is not None: + for i, update_node in enumerate(update_vector): + # Remove or replace all references to removed node. + if update_node.level > i and update_node.forward[i].key == key: + if node.level > i: + update_node.forward[i] = node.forward[i] + else: + update_node.forward = update_node.forward[:i] + + def insert(self, key: KT, value: VT) -> None: + """ + :param key: Key to insert. + :param value: Value associated with given key. + + >>> skip_list = SkipList() + >>> skip_list.insert(2, "Two") + >>> skip_list.find(2) + 'Two' + >>> list(skip_list) + [2] + """ + + node, update_vector = self._locate_node(key) + if node is not None: + node.value = value + else: + level = self.random_level() + + if level > self.level: + # After level increase we have to add additional nodes to head. + for _ in range(self.level - 1, level): + update_vector.append(self.head) + self.level = level + + new_node = Node(key, value) + + for i, update_node in enumerate(update_vector[:level]): + # Change references to pass through new node. + if update_node.level > i: + new_node.forward.append(update_node.forward[i]) + + if update_node.level < i + 1: + update_node.forward.append(new_node) + else: + update_node.forward[i] = new_node + + def find(self, key: VT) -> VT | None: + """ + :param key: Search key. + :return: Value associated with given key or None if given key is not present. + + >>> skip_list = SkipList() + >>> skip_list.find(2) + >>> skip_list.insert(2, "Two") + >>> skip_list.find(2) + 'Two' + >>> skip_list.insert(2, "Three") + >>> skip_list.find(2) + 'Three' + """ + + node, _ = self._locate_node(key) + + if node is not None: + return node.value + + return None + + +def test_insert() -> None: + skip_list = SkipList() + skip_list.insert("Key1", 3) + skip_list.insert("Key2", 12) + skip_list.insert("Key3", 41) + skip_list.insert("Key4", -19) + + node = skip_list.head + all_values = {} + while node.level != 0: + node = node.forward[0] + all_values[node.key] = node.value + + assert len(all_values) == 4 + assert all_values["Key1"] == 3 + assert all_values["Key2"] == 12 + assert all_values["Key3"] == 41 + assert all_values["Key4"] == -19 + + +def test_insert_overrides_existing_value() -> None: + skip_list = SkipList() + skip_list.insert("Key1", 10) + skip_list.insert("Key1", 12) + + skip_list.insert("Key5", 7) + skip_list.insert("Key7", 10) + skip_list.insert("Key10", 5) + + skip_list.insert("Key7", 7) + skip_list.insert("Key5", 5) + skip_list.insert("Key10", 10) + + node = skip_list.head + all_values = {} + while node.level != 0: + node = node.forward[0] + all_values[node.key] = node.value + + if len(all_values) != 4: + print() + assert len(all_values) == 4 + assert all_values["Key1"] == 12 + assert all_values["Key7"] == 7 + assert all_values["Key5"] == 5 + assert all_values["Key10"] == 10 + + +def test_searching_empty_list_returns_none() -> None: + skip_list = SkipList() + assert skip_list.find("Some key") is None + + +def test_search() -> None: + skip_list = SkipList() + + skip_list.insert("Key2", 20) + assert skip_list.find("Key2") == 20 + + skip_list.insert("Some Key", 10) + skip_list.insert("Key2", 8) + skip_list.insert("V", 13) + + assert skip_list.find("Y") is None + assert skip_list.find("Key2") == 8 + assert skip_list.find("Some Key") == 10 + assert skip_list.find("V") == 13 + + +def test_deleting_item_from_empty_list_do_nothing() -> None: + skip_list = SkipList() + skip_list.delete("Some key") + + assert len(skip_list.head.forward) == 0 + + +def test_deleted_items_are_not_founded_by_find_method() -> None: + skip_list = SkipList() + + skip_list.insert("Key1", 12) + skip_list.insert("V", 13) + skip_list.insert("X", 14) + skip_list.insert("Key2", 15) + + skip_list.delete("V") + skip_list.delete("Key2") + + assert skip_list.find("V") is None + assert skip_list.find("Key2") is None + + +def test_delete_removes_only_given_key() -> None: + skip_list = SkipList() + + skip_list.insert("Key1", 12) + skip_list.insert("V", 13) + skip_list.insert("X", 14) + skip_list.insert("Key2", 15) + + skip_list.delete("V") + assert skip_list.find("V") is None + assert skip_list.find("X") == 14 + assert skip_list.find("Key1") == 12 + assert skip_list.find("Key2") == 15 + + skip_list.delete("X") + assert skip_list.find("V") is None + assert skip_list.find("X") is None + assert skip_list.find("Key1") == 12 + assert skip_list.find("Key2") == 15 + + skip_list.delete("Key1") + assert skip_list.find("V") is None + assert skip_list.find("X") is None + assert skip_list.find("Key1") is None + assert skip_list.find("Key2") == 15 + + skip_list.delete("Key2") + assert skip_list.find("V") is None + assert skip_list.find("X") is None + assert skip_list.find("Key1") is None + assert skip_list.find("Key2") is None + + +def test_delete_doesnt_leave_dead_nodes() -> None: + skip_list = SkipList() + + skip_list.insert("Key1", 12) + skip_list.insert("V", 13) + skip_list.insert("X", 142) + skip_list.insert("Key2", 15) + + skip_list.delete("X") + + def traverse_keys(node): + yield node.key + for forward_node in node.forward: + yield from traverse_keys(forward_node) + + assert len(set(traverse_keys(skip_list.head))) == 4 + + +def test_iter_always_yields_sorted_values() -> None: + def is_sorted(lst): + return all(next_item >= item for item, next_item in pairwise(lst)) + + skip_list = SkipList() + for i in range(10): + skip_list.insert(i, i) + assert is_sorted(list(skip_list)) + skip_list.delete(5) + skip_list.delete(8) + skip_list.delete(2) + assert is_sorted(list(skip_list)) + skip_list.insert(-12, -12) + skip_list.insert(77, 77) + assert is_sorted(list(skip_list)) + + +def pytests() -> None: + for _ in range(100): + # Repeat test 100 times due to the probabilistic nature of skip list + # random values == random bugs + test_insert() + test_insert_overrides_existing_value() + + test_searching_empty_list_returns_none() + test_search() + + test_deleting_item_from_empty_list_do_nothing() + test_deleted_items_are_not_founded_by_find_method() + test_delete_removes_only_given_key() + test_delete_doesnt_leave_dead_nodes() + + test_iter_always_yields_sorted_values() + + +def main() -> None: + """ + >>> pytests() + """ + + skip_list = SkipList() + skip_list.insert(2, "2") + skip_list.insert(4, "4") + skip_list.insert(6, "4") + skip_list.insert(4, "5") + skip_list.insert(8, "4") + skip_list.insert(9, "4") + + skip_list.delete(4) + + print(skip_list) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/data_structures/linked_list/sorted_linked_list.py b/data_structures/linked_list/sorted_linked_list.py new file mode 100644 index 000000000000..82807ff0b135 --- /dev/null +++ b/data_structures/linked_list/sorted_linked_list.py @@ -0,0 +1,330 @@ +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass + + +@dataclass(order=True) +class Node: + """ + A class representing a node in a linked list. + + Attributes: + data: The data stored in the node. + next: A reference to the next node in the linked list. + + >>> Node(1, Node(2, Node(3))) + Node(data=1, next=Node(data=2, next=Node(data=3, next=None))) + """ + + data: int + next: Node | None = None + + +class SortedLinkedList: + """This class represents a sorted linked list.""" + + def __init__(self) -> None: + """ + Create and initialize LinkedList class instance. + >>> linked_list = SortedLinkedList() + >>> linked_list.head is None + True + """ + self.head: Node | None = None + self.tail: Node | None = None + + def __iter__(self) -> Iterator[int]: + """Iterate over the data of the nodes in the linked list. + + >>> linked_list = SortedLinkedList() + >>> linked_list.insert(3) + >>> linked_list.insert(1) + >>> linked_list.insert(2) + >>> tuple(linked_list) + (1, 2, 3) + """ + current = self.head + while current: + yield current.data + current = current.next + + def __len__(self) -> int: + """Return the number of nodes in the linked list. + + >>> linked_list = SortedLinkedList() + >>> len(linked_list) + 0 + >>> linked_list.insert(3) + >>> len(linked_list) + 1 + >>> linked_list.insert(1) + >>> linked_list.insert(2) + >>> len(linked_list) + 3 + """ + return len(tuple(self)) + + def __contains__(self, data: int) -> bool: + """Check if a node with the given data exists in the linked list. + + >>> linked_list = SortedLinkedList() + >>> linked_list.insert(3) + >>> 3 in linked_list + True + >>> 1 in linked_list + False + """ + return data in tuple(self) + + def insert(self, data: int) -> None: + """Inserts a node in its sorted position + This function can be rewritten for any data type, but + the comparator here must be changed + + Args: + data (int): the data of the linked list + + Doctests + >>> linked_list = SortedLinkedList() + >>> linked_list.insert(32) + >>> linked_list.insert(57) + >>> linked_list.insert(45) + >>> tuple(linked_list) + (32, 45, 57) + """ + new_node = Node(data) + if self.head is None: + self.head = new_node + self.tail = new_node + elif new_node < self.head: + new_node.next = self.head + self.head = new_node + else: + temp_node: Node | None = self.head + if temp_node: + while temp_node.next and temp_node.next.data < data: + temp_node = temp_node.next + new_node.next = temp_node.next + temp_node.next = new_node + if new_node.next is None: + self.tail = new_node + + def delete(self, data: int) -> bool: + """This Function deletes first appearance of node with + data from it's sorted position + + This function can be re written for any data type but + the comparator her must have to be changed + + Args: + data (int): the data of the node that is needed to be deleted + + Returns: + bool: status whether the node got deleted or not + + Doctests + + >>> linkedList=SortedLinkedList() + >>> linkedList.insert(32) + >>> linkedList.insert(57) + >>> linkedList.insert(45) + >>> tuple(linkedList) + (32, 45, 57) + >>> linkedList.delete(45) + True + >>> tuple(linkedList) + (32, 57) + """ + if self.head is None: + return False + + if self.head.data == data: + self.head = self.head.next + if self.head is None: + self.tail = None + return True + + temp_node: Node | None = self.head + if temp_node: + while temp_node.next: + if temp_node.next.data == data: + temp_node.next = temp_node.next.next + if temp_node.next is None: + self.tail = temp_node + return True + temp_node = temp_node.next + + return False + + def search(self, data: int) -> bool: + """This function searches the data given input from user + and return whether the data exists or not + + Args: + data (int): Data to be searched + + Returns: + bool: flag indicating whether data exists or not + + Doctests + >>> linkedList=SortedLinkedList() + >>> linkedList.insert(32) + >>> linkedList.insert(57) + >>> linkedList.insert(45) + >>> tuple(linkedList) + (32, 45, 57) + >>> linkedList.search(45) + True + >>> linkedList.search(90) + False + """ + return data in self + + def is_empty(self) -> bool: + """This function will check whether the list is empty or not + + Returns: + bool: flag indicating whether list is empty or not + + Doctests + + >>> linkedList=SortedLinkedList() + >>> linkedList.is_empty() + True + >>> linkedList.insert(32) + >>> linkedList.insert(57) + >>> linkedList.insert(45) + >>> linkedList.is_empty() + False + """ + return not self + + def min_value(self) -> int | None: + """This function will return minimum value + + Returns: + int | None: min value or None if list is empty + + Doctests + + >>> linkedList=SortedLinkedList() + >>> linkedList.min_value() is None + True + >>> linkedList.insert(32) + >>> linkedList.insert(57) + >>> linkedList.insert(45) + >>> linkedList.min_value() + 32 + """ + return min(self) if self.head else None + + def max_value(self) -> int | None: + """This function will return maximum value + + + Returns: + int | None: max value or None if list is empty + + Doctests + + >>> linkedList=SortedLinkedList() + >>> linkedList.max_value() is None + True + >>> linkedList.insert(32) + >>> linkedList.insert(57) + >>> linkedList.insert(45) + >>> linkedList.max_value() + 57 + """ + return max(self) if self.head else None + + def remove_duplicates(self) -> None: + """ + This Function will remove the duplicates from the list + + Doctests + + >>> linkedList=SortedLinkedList() + >>> linkedList.insert(32) + >>> linkedList.insert(57) + >>> linkedList.insert(45) + >>> linkedList.insert(45) + >>> tuple(linkedList) + (32, 45, 45, 57) + >>> linkedList.remove_duplicates() + >>> tuple(linkedList) + (32, 45, 57) + """ + + temp: Node | None = self.head + while temp and temp.next: + if temp.data == temp.next.data: + temp.next = temp.next.next + else: + temp = temp.next + + def merge(self, other_list: SortedLinkedList) -> None: + """This Function will merge the input list with current list + + Args: + other_list (SortedLinkedList): The list to be merged + + Doctests + + >>> linkedList=SortedLinkedList() + >>> linkedList.insert(32) + >>> linkedList.insert(57) + >>> linkedList.insert(45) + >>> tuple(linkedList) + (32, 45, 57) + >>> linkedList2=SortedLinkedList() + >>> linkedList2.insert(23) + >>> linkedList2.insert(47) + >>> linkedList2.insert(95) + >>> tuple(linkedList2) + (23, 47, 95) + >>> linkedList.merge(linkedList2) + >>> tuple(linkedList) + (23, 32, 45, 47, 57, 95) + """ + if other_list.head is None: + return + elif self.head is None: + self.head = other_list.head + self.tail = other_list.tail + return + else: + temp: Node | None = other_list.head + + while temp: + self.insert(temp.data) + temp = temp.next + + +if __name__ == "__main__": + linked_list = SortedLinkedList() + while True: + print("Enter") + print("1. Insert") + print("2. Display") + print("3. Delete") + print("4. Exit") + choice = input("Enter your choice: ") + + match choice: + case "1": + node_data = int(input("Enter a number: ")) + linked_list.insert(node_data) + case "2": + linked_list.display() + case "3": + node_data = int(input("Enter the data to delete: ")) + if linked_list.delete(node_data): + print(f"Node with data {node_data} deleted successfully") + else: + print(f"Node with data {node_data} not found in the list") + case "4": + break + case _: + print("Wrong input") diff --git a/data_structures/linked_list/swap_nodes.py b/data_structures/linked_list/swap_nodes.py new file mode 100644 index 000000000000..d66512087d2d --- /dev/null +++ b/data_structures/linked_list/swap_nodes.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass +from typing import Any + + +@dataclass +class Node: + data: Any + next_node: Node | None = None + + +@dataclass +class LinkedList: + head: Node | None = None + + def __iter__(self) -> Iterator: + """ + >>> linked_list = LinkedList() + >>> list(linked_list) + [] + >>> linked_list.push(0) + >>> tuple(linked_list) + (0,) + """ + node = self.head + while node: + yield node.data + node = node.next_node + + def __len__(self) -> int: + """ + >>> linked_list = LinkedList() + >>> len(linked_list) + 0 + >>> linked_list.push(0) + >>> len(linked_list) + 1 + """ + return sum(1 for _ in self) + + def push(self, new_data: Any) -> None: + """ + Add a new node with the given data to the beginning of the Linked List. + + Args: + new_data (Any): The data to be added to the new node. + + Returns: + None + + Examples: + >>> linked_list = LinkedList() + >>> linked_list.push(5) + >>> linked_list.push(4) + >>> linked_list.push(3) + >>> linked_list.push(2) + >>> linked_list.push(1) + >>> list(linked_list) + [1, 2, 3, 4, 5] + """ + new_node = Node(new_data) + new_node.next_node = self.head + self.head = new_node + + def swap_nodes(self, node_data_1: Any, node_data_2: Any) -> None: + """ + Swap the positions of two nodes in the Linked List based on their data values. + + Args: + node_data_1: Data value of the first node to be swapped. + node_data_2: Data value of the second node to be swapped. + + + Note: + If either of the specified data values isn't found then, no swapping occurs. + + Examples: + When both values are present in a linked list. + >>> linked_list = LinkedList() + >>> linked_list.push(5) + >>> linked_list.push(4) + >>> linked_list.push(3) + >>> linked_list.push(2) + >>> linked_list.push(1) + >>> list(linked_list) + [1, 2, 3, 4, 5] + >>> linked_list.swap_nodes(1, 5) + >>> tuple(linked_list) + (5, 2, 3, 4, 1) + + When one value is present and the other isn't in the linked list. + >>> second_list = LinkedList() + >>> second_list.push(6) + >>> second_list.push(7) + >>> second_list.push(8) + >>> second_list.push(9) + >>> second_list.swap_nodes(1, 6) is None + True + + When both values are absent in the linked list. + >>> second_list = LinkedList() + >>> second_list.push(10) + >>> second_list.push(9) + >>> second_list.push(8) + >>> second_list.push(7) + >>> second_list.swap_nodes(1, 3) is None + True + + When linkedlist is empty. + >>> second_list = LinkedList() + >>> second_list.swap_nodes(1, 3) is None + True + + Returns: + None + """ + if node_data_1 == node_data_2: + return + + node_1 = self.head + while node_1 and node_1.data != node_data_1: + node_1 = node_1.next_node + node_2 = self.head + while node_2 and node_2.data != node_data_2: + node_2 = node_2.next_node + if node_1 is None or node_2 is None: + return + # Swap the data values of the two nodes + node_1.data, node_2.data = node_2.data, node_1.data + + +if __name__ == "__main__": + """ + Python script that outputs the swap of nodes in a linked list. + """ + from doctest import testmod + + testmod() + linked_list = LinkedList() + for i in range(5, 0, -1): + linked_list.push(i) + + print(f"Original Linked List: {list(linked_list)}") + linked_list.swap_nodes(1, 4) + print(f"Modified Linked List: {list(linked_list)}") + print("After swapping the nodes whose data is 1 and 4.") diff --git a/data_structures/linked_list/xor_linked_list.py b/data_structures/linked_list/xor_linked_list.py new file mode 100644 index 000000000000..3de6e12c910e --- /dev/null +++ b/data_structures/linked_list/xor_linked_list.py @@ -0,0 +1,85 @@ +""" +XOR Linked List implementation +A memory-efficient doubly linked list that uses the XOR of node addresses. +Each node stores one pointer that is the XOR of the previous and next node addresses. +https://en.wikipedia.org/wiki/XOR_linked_list +Example: +>>> xor_list = XORLinkedList() +>>> xor_list.insert(10) +>>> xor_list.insert(20) +>>> xor_list.insert(30) +>>> xor_list.to_list() +[10, 20, 30] +""" + +from dataclasses import dataclass + + +@dataclass +class Node: + value: int + both: int = 0 # XOR of prev and next node IDs + + +class XORLinkedList: + def __init__(self) -> None: + """Initializes an empty XOR Linked List.""" + # Use 'Node | None' instead of 'Optional[Node]' (per ruff UP045) + self.head: Node | None = None + self.tail: Node | None = None + # id -> node map to simulate pointer references + self._nodes: dict[int, Node] = {} + + def _xor(self, node_a: Node | None, node_b: Node | None) -> int: + """ + Helper function to get the XOR of two node IDs (simulated addresses). + Names 'node_a' and 'node_b' are used for descriptive parameters. + """ + id_a = id(node_a) if node_a else 0 + id_b = id(node_b) if node_b else 0 + return id_a ^ id_b + + def insert(self, value: int) -> None: + """Inserts a value at the end of the list.""" + node = Node(value) + self._nodes[id(node)] = node + node_id = id(node) + + if self.head is None: + # If the list is empty, head and tail are the new node + self.head = self.tail = node + else: + # If the list is not empty, append to the tail + # The new node's pointer is just the ID of the old tail + node.both = id(self.tail) + if self.tail: # Type checker guard + # The old tail's pointer must be updated to XOR + # its previous node ID with the new node's ID. + # self.tail.both was (prev_id ^ 0) + # self.tail.both becomes (prev_id ^ new_node_id) + self.tail.both ^= node_id + self.tail = node + + def to_list(self) -> list[int]: + """Converts the XOR list to a standard Python list (forward traversal).""" + result = [] + prev_id = 0 + current = self.head + while current: + result.append(current.value) + # Find next node's ID: + # current.both = prev_id ^ next_id + # so, next_id = prev_id ^ current.both + current_id = id(current) + next_id = prev_id ^ current.both + + # Move forward + prev_id = current_id + current = self._nodes.get(next_id) + return result + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/queues/__init__.py b/data_structures/queues/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/queues/circular_queue.py b/data_structures/queues/circular_queue.py new file mode 100644 index 000000000000..e7920a31d0c9 --- /dev/null +++ b/data_structures/queues/circular_queue.py @@ -0,0 +1,109 @@ +# Implementation of Circular Queue (using Python lists) + + +class CircularQueue: + """Circular FIFO queue with a fixed capacity""" + + def __init__(self, n: int) -> None: + self.n = n + self.array = [None] * self.n + self.front = 0 # index of the first element + self.rear = 0 + self.size = 0 + + def __len__(self) -> int: + """ + >>> cq = CircularQueue(5) + >>> len(cq) + 0 + >>> cq.enqueue("A") # doctest: +ELLIPSIS + + >>> cq.array + ['A', None, None, None, None] + >>> len(cq) + 1 + """ + return self.size + + def is_empty(self) -> bool: + """ + Checks whether the queue is empty or not + >>> cq = CircularQueue(5) + >>> cq.is_empty() + True + >>> cq.enqueue("A").is_empty() + False + """ + return self.size == 0 + + def first(self): + """ + Returns the first element of the queue + >>> cq = CircularQueue(5) + >>> cq.first() + False + >>> cq.enqueue("A").first() + 'A' + """ + return False if self.is_empty() else self.array[self.front] + + def enqueue(self, data) -> "CircularQueue": + """ + This function inserts an element at the end of the queue using self.rear value + as an index. + + >>> cq = CircularQueue(5) + >>> cq.enqueue("A") # doctest: +ELLIPSIS + + >>> (cq.size, cq.first()) + (1, 'A') + >>> cq.enqueue("B") # doctest: +ELLIPSIS + + >>> cq.array + ['A', 'B', None, None, None] + >>> (cq.size, cq.first()) + (2, 'A') + >>> cq.enqueue("C").enqueue("D").enqueue("E") # doctest: +ELLIPSIS + + >>> cq.enqueue("F") + Traceback (most recent call last): + ... + Exception: QUEUE IS FULL + """ + if self.size >= self.n: + raise Exception("QUEUE IS FULL") + + self.array[self.rear] = data + self.rear = (self.rear + 1) % self.n + self.size += 1 + return self + + def dequeue(self): + """ + This function removes an element from the queue using on self.front value as an + index and returns it + + >>> cq = CircularQueue(5) + >>> cq.dequeue() + Traceback (most recent call last): + ... + Exception: UNDERFLOW + >>> cq.enqueue("A").enqueue("B").dequeue() + 'A' + >>> (cq.size, cq.first()) + (1, 'B') + >>> cq.dequeue() + 'B' + >>> cq.dequeue() + Traceback (most recent call last): + ... + Exception: UNDERFLOW + """ + if self.size == 0: + raise Exception("UNDERFLOW") + + temp = self.array[self.front] + self.array[self.front] = None + self.front = (self.front + 1) % self.n + self.size -= 1 + return temp diff --git a/data_structures/queues/circular_queue_linked_list.py b/data_structures/queues/circular_queue_linked_list.py new file mode 100644 index 000000000000..da8629678e52 --- /dev/null +++ b/data_structures/queues/circular_queue_linked_list.py @@ -0,0 +1,161 @@ +# Implementation of Circular Queue using linked lists +# https://en.wikipedia.org/wiki/Circular_buffer + +from __future__ import annotations + +from typing import Any + + +class CircularQueueLinkedList: + """ + Circular FIFO list with the given capacity (default queue length : 6) + + >>> cq = CircularQueueLinkedList(2) + >>> cq.enqueue('a') + >>> cq.enqueue('b') + >>> cq.enqueue('c') + Traceback (most recent call last): + ... + Exception: Full Queue + """ + + def __init__(self, initial_capacity: int = 6) -> None: + self.front: Node | None = None + self.rear: Node | None = None + self.create_linked_list(initial_capacity) + + def create_linked_list(self, initial_capacity: int) -> None: + current_node = Node() + self.front = current_node + self.rear = current_node + previous_node = current_node + for _ in range(1, initial_capacity): + current_node = Node() + previous_node.next = current_node + current_node.prev = previous_node + previous_node = current_node + previous_node.next = self.front + self.front.prev = previous_node + + def is_empty(self) -> bool: + """ + Checks whether the queue is empty or not + >>> cq = CircularQueueLinkedList() + >>> cq.is_empty() + True + >>> cq.enqueue('a') + >>> cq.is_empty() + False + >>> cq.dequeue() + 'a' + >>> cq.is_empty() + True + """ + + return ( + self.front == self.rear + and self.front is not None + and self.front.data is None + ) + + def first(self) -> Any | None: + """ + Returns the first element of the queue + >>> cq = CircularQueueLinkedList() + >>> cq.first() + Traceback (most recent call last): + ... + Exception: Empty Queue + >>> cq.enqueue('a') + >>> cq.first() + 'a' + >>> cq.dequeue() + 'a' + >>> cq.first() + Traceback (most recent call last): + ... + Exception: Empty Queue + >>> cq.enqueue('b') + >>> cq.enqueue('c') + >>> cq.first() + 'b' + """ + self.check_can_perform_operation() + return self.front.data if self.front else None + + def enqueue(self, data: Any) -> None: + """ + Saves data at the end of the queue + + >>> cq = CircularQueueLinkedList() + >>> cq.enqueue('a') + >>> cq.enqueue('b') + >>> cq.dequeue() + 'a' + >>> cq.dequeue() + 'b' + >>> cq.dequeue() + Traceback (most recent call last): + ... + Exception: Empty Queue + """ + if self.rear is None: + return + + self.check_is_full() + if not self.is_empty(): + self.rear = self.rear.next + if self.rear: + self.rear.data = data + + def dequeue(self) -> Any: + """ + Removes and retrieves the first element of the queue + + >>> cq = CircularQueueLinkedList() + >>> cq.dequeue() + Traceback (most recent call last): + ... + Exception: Empty Queue + >>> cq.enqueue('a') + >>> cq.dequeue() + 'a' + >>> cq.dequeue() + Traceback (most recent call last): + ... + Exception: Empty Queue + """ + self.check_can_perform_operation() + if self.rear is None or self.front is None: + return None + if self.front == self.rear: + data = self.front.data + self.front.data = None + return data + + old_front = self.front + self.front = old_front.next + data = old_front.data + old_front.data = None + return data + + def check_can_perform_operation(self) -> None: + if self.is_empty(): + raise Exception("Empty Queue") + + def check_is_full(self) -> None: + if self.rear and self.rear.next == self.front: + raise Exception("Full Queue") + + +class Node: + def __init__(self) -> None: + self.data: Any | None = None + self.next: Node | None = None + self.prev: Node | None = None + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/queues/double_ended_queue.py b/data_structures/queues/double_ended_queue.py new file mode 100644 index 000000000000..c28d46c65168 --- /dev/null +++ b/data_structures/queues/double_ended_queue.py @@ -0,0 +1,463 @@ +""" +Implementation of double ended queue. +""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass +from typing import Any + + +class Deque: + """ + Deque data structure. + Operations + ---------- + append(val: Any) -> None + appendleft(val: Any) -> None + extend(iterable: Iterable) -> None + extendleft(iterable: Iterable) -> None + pop() -> Any + popleft() -> Any + Observers + --------- + is_empty() -> bool + Attributes + ---------- + _front: _Node + front of the deque a.k.a. the first element + _back: _Node + back of the element a.k.a. the last element + _len: int + the number of nodes + """ + + __slots__ = ("_back", "_front", "_len") + + @dataclass + class _Node: + """ + Representation of a node. + Contains a value and a pointer to the next node as well as to the previous one. + """ + + val: Any = None + next_node: Deque._Node | None = None + prev_node: Deque._Node | None = None + + class _Iterator: + """ + Helper class for iteration. Will be used to implement iteration. + Attributes + ---------- + _cur: _Node + the current node of the iteration. + """ + + __slots__ = ("_cur",) + + def __init__(self, cur: Deque._Node | None) -> None: + self._cur = cur + + def __iter__(self) -> Deque._Iterator: + """ + >>> our_deque = Deque([1, 2, 3]) + >>> iterator = iter(our_deque) + """ + return self + + def __next__(self) -> Any: + """ + >>> our_deque = Deque([1, 2, 3]) + >>> iterator = iter(our_deque) + >>> next(iterator) + 1 + >>> next(iterator) + 2 + >>> next(iterator) + 3 + """ + if self._cur is None: + # finished iterating + raise StopIteration + val = self._cur.val + self._cur = self._cur.next_node + + return val + + def __init__(self, iterable: Iterable[Any] | None = None) -> None: + self._front: Any = None + self._back: Any = None + self._len: int = 0 + + if iterable is not None: + # append every value to the deque + for val in iterable: + self.append(val) + + def append(self, val: Any) -> None: + """ + Adds val to the end of the deque. + Time complexity: O(1) + >>> our_deque_1 = Deque([1, 2, 3]) + >>> our_deque_1.append(4) + >>> our_deque_1 + [1, 2, 3, 4] + >>> our_deque_2 = Deque('ab') + >>> our_deque_2.append('c') + >>> our_deque_2 + ['a', 'b', 'c'] + >>> from collections import deque + >>> deque_collections_1 = deque([1, 2, 3]) + >>> deque_collections_1.append(4) + >>> deque_collections_1 + deque([1, 2, 3, 4]) + >>> deque_collections_2 = deque('ab') + >>> deque_collections_2.append('c') + >>> deque_collections_2 + deque(['a', 'b', 'c']) + >>> list(our_deque_1) == list(deque_collections_1) + True + >>> list(our_deque_2) == list(deque_collections_2) + True + """ + node = self._Node(val, None, None) + if self.is_empty(): + # front = back + self._front = self._back = node + self._len = 1 + else: + # connect nodes + self._back.next_node = node + node.prev_node = self._back + self._back = node # assign new back to the new node + + self._len += 1 + + # make sure there were no errors + assert not self.is_empty(), "Error on appending value." + + def appendleft(self, val: Any) -> None: + """ + Adds val to the beginning of the deque. + Time complexity: O(1) + >>> our_deque_1 = Deque([2, 3]) + >>> our_deque_1.appendleft(1) + >>> our_deque_1 + [1, 2, 3] + >>> our_deque_2 = Deque('bc') + >>> our_deque_2.appendleft('a') + >>> our_deque_2 + ['a', 'b', 'c'] + >>> from collections import deque + >>> deque_collections_1 = deque([2, 3]) + >>> deque_collections_1.appendleft(1) + >>> deque_collections_1 + deque([1, 2, 3]) + >>> deque_collections_2 = deque('bc') + >>> deque_collections_2.appendleft('a') + >>> deque_collections_2 + deque(['a', 'b', 'c']) + >>> list(our_deque_1) == list(deque_collections_1) + True + >>> list(our_deque_2) == list(deque_collections_2) + True + """ + node = self._Node(val, None, None) + if self.is_empty(): + # front = back + self._front = self._back = node + self._len = 1 + else: + # connect nodes + node.next_node = self._front + self._front.prev_node = node + self._front = node # assign new front to the new node + + self._len += 1 + + # make sure there were no errors + assert not self.is_empty(), "Error on appending value." + + def extend(self, iterable: Iterable[Any]) -> None: + """ + Appends every value of iterable to the end of the deque. + Time complexity: O(n) + >>> our_deque_1 = Deque([1, 2, 3]) + >>> our_deque_1.extend([4, 5]) + >>> our_deque_1 + [1, 2, 3, 4, 5] + >>> our_deque_2 = Deque('ab') + >>> our_deque_2.extend('cd') + >>> our_deque_2 + ['a', 'b', 'c', 'd'] + >>> from collections import deque + >>> deque_collections_1 = deque([1, 2, 3]) + >>> deque_collections_1.extend([4, 5]) + >>> deque_collections_1 + deque([1, 2, 3, 4, 5]) + >>> deque_collections_2 = deque('ab') + >>> deque_collections_2.extend('cd') + >>> deque_collections_2 + deque(['a', 'b', 'c', 'd']) + >>> list(our_deque_1) == list(deque_collections_1) + True + >>> list(our_deque_2) == list(deque_collections_2) + True + """ + for val in iterable: + self.append(val) + + def extendleft(self, iterable: Iterable[Any]) -> None: + """ + Appends every value of iterable to the beginning of the deque. + Time complexity: O(n) + >>> our_deque_1 = Deque([1, 2, 3]) + >>> our_deque_1.extendleft([0, -1]) + >>> our_deque_1 + [-1, 0, 1, 2, 3] + >>> our_deque_2 = Deque('cd') + >>> our_deque_2.extendleft('ba') + >>> our_deque_2 + ['a', 'b', 'c', 'd'] + >>> from collections import deque + >>> deque_collections_1 = deque([1, 2, 3]) + >>> deque_collections_1.extendleft([0, -1]) + >>> deque_collections_1 + deque([-1, 0, 1, 2, 3]) + >>> deque_collections_2 = deque('cd') + >>> deque_collections_2.extendleft('ba') + >>> deque_collections_2 + deque(['a', 'b', 'c', 'd']) + >>> list(our_deque_1) == list(deque_collections_1) + True + >>> list(our_deque_2) == list(deque_collections_2) + True + """ + for val in iterable: + self.appendleft(val) + + def pop(self) -> Any: + """ + Removes the last element of the deque and returns it. + Time complexity: O(1) + @returns topop.val: the value of the node to pop. + >>> our_deque1 = Deque([1]) + >>> our_popped1 = our_deque1.pop() + >>> our_popped1 + 1 + >>> our_deque1 + [] + + >>> our_deque2 = Deque([1, 2, 3, 15182]) + >>> our_popped2 = our_deque2.pop() + >>> our_popped2 + 15182 + >>> our_deque2 + [1, 2, 3] + + >>> from collections import deque + >>> deque_collections = deque([1, 2, 3, 15182]) + >>> collections_popped = deque_collections.pop() + >>> collections_popped + 15182 + >>> deque_collections + deque([1, 2, 3]) + >>> list(our_deque2) == list(deque_collections) + True + >>> our_popped2 == collections_popped + True + """ + # make sure the deque has elements to pop + assert not self.is_empty(), "Deque is empty." + + topop = self._back + # if only one element in the queue: point the front and back to None + # else remove one element from back + if self._front == self._back: + self._front = None + self._back = None + else: + self._back = self._back.prev_node # set new back + # drop the last node, python will deallocate memory automatically + self._back.next_node = None + + self._len -= 1 + + return topop.val + + def popleft(self) -> Any: + """ + Removes the first element of the deque and returns it. + Time complexity: O(1) + @returns topop.val: the value of the node to pop. + >>> our_deque1 = Deque([1]) + >>> our_popped1 = our_deque1.pop() + >>> our_popped1 + 1 + >>> our_deque1 + [] + >>> our_deque2 = Deque([15182, 1, 2, 3]) + >>> our_popped2 = our_deque2.popleft() + >>> our_popped2 + 15182 + >>> our_deque2 + [1, 2, 3] + >>> from collections import deque + >>> deque_collections = deque([15182, 1, 2, 3]) + >>> collections_popped = deque_collections.popleft() + >>> collections_popped + 15182 + >>> deque_collections + deque([1, 2, 3]) + >>> list(our_deque2) == list(deque_collections) + True + >>> our_popped2 == collections_popped + True + """ + # make sure the deque has elements to pop + assert not self.is_empty(), "Deque is empty." + + topop = self._front + # if only one element in the queue: point the front and back to None + # else remove one element from front + if self._front == self._back: + self._front = None + self._back = None + else: + self._front = self._front.next_node # set new front and drop the first node + self._front.prev_node = None + + self._len -= 1 + + return topop.val + + def is_empty(self) -> bool: + """ + Checks if the deque is empty. + Time complexity: O(1) + >>> our_deque = Deque([1, 2, 3]) + >>> our_deque.is_empty() + False + >>> our_empty_deque = Deque() + >>> our_empty_deque.is_empty() + True + >>> from collections import deque + >>> empty_deque_collections = deque() + >>> list(our_empty_deque) == list(empty_deque_collections) + True + """ + return self._front is None + + def __len__(self) -> int: + """ + Implements len() function. Returns the length of the deque. + Time complexity: O(1) + >>> our_deque = Deque([1, 2, 3]) + >>> len(our_deque) + 3 + >>> our_empty_deque = Deque() + >>> len(our_empty_deque) + 0 + >>> from collections import deque + >>> deque_collections = deque([1, 2, 3]) + >>> len(deque_collections) + 3 + >>> empty_deque_collections = deque() + >>> len(empty_deque_collections) + 0 + >>> len(our_empty_deque) == len(empty_deque_collections) + True + """ + return self._len + + def __eq__(self, other: object) -> bool: + """ + Implements "==" operator. Returns if *self* is equal to *other*. + Time complexity: O(n) + >>> our_deque_1 = Deque([1, 2, 3]) + >>> our_deque_2 = Deque([1, 2, 3]) + >>> our_deque_1 == our_deque_2 + True + >>> our_deque_3 = Deque([1, 2]) + >>> our_deque_1 == our_deque_3 + False + >>> from collections import deque + >>> deque_collections_1 = deque([1, 2, 3]) + >>> deque_collections_2 = deque([1, 2, 3]) + >>> deque_collections_1 == deque_collections_2 + True + >>> deque_collections_3 = deque([1, 2]) + >>> deque_collections_1 == deque_collections_3 + False + >>> (our_deque_1 == our_deque_2) == (deque_collections_1 == deque_collections_2) + True + >>> (our_deque_1 == our_deque_3) == (deque_collections_1 == deque_collections_3) + True + """ + + if not isinstance(other, Deque): + return NotImplemented + + me = self._front + oth = other._front + + # if the length of the dequeues are not the same, they are not equal + if len(self) != len(other): + return False + + while me is not None and oth is not None: + # compare every value + if me.val != oth.val: + return False + me = me.next_node + oth = oth.next_node + + return True + + def __iter__(self) -> Deque._Iterator: + """ + Implements iteration. + Time complexity: O(1) + >>> our_deque = Deque([1, 2, 3]) + >>> for v in our_deque: + ... print(v) + 1 + 2 + 3 + >>> from collections import deque + >>> deque_collections = deque([1, 2, 3]) + >>> for v in deque_collections: + ... print(v) + 1 + 2 + 3 + """ + return Deque._Iterator(self._front) + + def __repr__(self) -> str: + """ + Implements representation of the deque. + Represents it as a list, with its values between '[' and ']'. + Time complexity: O(n) + >>> our_deque = Deque([1, 2, 3]) + >>> our_deque + [1, 2, 3] + """ + values_list = [] + aux = self._front + while aux is not None: + # append the values in a list to display + values_list.append(aux.val) + aux = aux.next_node + + return f"[{', '.join(repr(val) for val in values_list)}]" + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + dq = Deque([3]) + dq.pop() diff --git a/data_structures/queues/linked_queue.py b/data_structures/queues/linked_queue.py new file mode 100644 index 000000000000..80f6d309af9a --- /dev/null +++ b/data_structures/queues/linked_queue.py @@ -0,0 +1,156 @@ +"""A Queue using a linked list like structure""" + +from __future__ import annotations + +from collections.abc import Iterator +from typing import Any + + +class Node: + def __init__(self, data: Any) -> None: + self.data: Any = data + self.next: Node | None = None + + def __str__(self) -> str: + return f"{self.data}" + + +class LinkedQueue: + """ + >>> queue = LinkedQueue() + >>> queue.is_empty() + True + >>> queue.put(5) + >>> queue.put(9) + >>> queue.put('python') + >>> queue.is_empty() + False + >>> queue.get() + 5 + >>> queue.put('algorithms') + >>> queue.get() + 9 + >>> queue.get() + 'python' + >>> queue.get() + 'algorithms' + >>> queue.is_empty() + True + >>> queue.get() + Traceback (most recent call last): + ... + IndexError: dequeue from empty queue + """ + + def __init__(self) -> None: + self.front: Node | None = None + self.rear: Node | None = None + + def __iter__(self) -> Iterator[Any]: + node = self.front + while node: + yield node.data + node = node.next + + def __len__(self) -> int: + """ + >>> queue = LinkedQueue() + >>> for i in range(1, 6): + ... queue.put(i) + >>> len(queue) + 5 + >>> for i in range(1, 6): + ... assert len(queue) == 6 - i + ... _ = queue.get() + >>> len(queue) + 0 + """ + return len(tuple(iter(self))) + + def __str__(self) -> str: + """ + >>> queue = LinkedQueue() + >>> for i in range(1, 4): + ... queue.put(i) + >>> queue.put("Python") + >>> queue.put(3.14) + >>> queue.put(True) + >>> str(queue) + '1 <- 2 <- 3 <- Python <- 3.14 <- True' + """ + return " <- ".join(str(item) for item in self) + + def is_empty(self) -> bool: + """ + >>> queue = LinkedQueue() + >>> queue.is_empty() + True + >>> for i in range(1, 6): + ... queue.put(i) + >>> queue.is_empty() + False + """ + return len(self) == 0 + + def put(self, item: Any) -> None: + """ + >>> queue = LinkedQueue() + >>> queue.get() + Traceback (most recent call last): + ... + IndexError: dequeue from empty queue + >>> for i in range(1, 6): + ... queue.put(i) + >>> str(queue) + '1 <- 2 <- 3 <- 4 <- 5' + """ + node = Node(item) + if self.is_empty(): + self.front = self.rear = node + else: + assert isinstance(self.rear, Node) + self.rear.next = node + self.rear = node + + def get(self) -> Any: + """ + >>> queue = LinkedQueue() + >>> queue.get() + Traceback (most recent call last): + ... + IndexError: dequeue from empty queue + >>> queue = LinkedQueue() + >>> for i in range(1, 6): + ... queue.put(i) + >>> for i in range(1, 6): + ... assert queue.get() == i + >>> len(queue) + 0 + """ + if self.is_empty(): + raise IndexError("dequeue from empty queue") + assert isinstance(self.front, Node) + node = self.front + self.front = self.front.next + if self.front is None: + self.rear = None + return node.data + + def clear(self) -> None: + """ + >>> queue = LinkedQueue() + >>> for i in range(1, 6): + ... queue.put(i) + >>> queue.clear() + >>> len(queue) + 0 + >>> str(queue) + '' + """ + self.front = self.rear = None + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/data_structures/queues/monotonic_queue.py b/data_structures/queues/monotonic_queue.py new file mode 100644 index 000000000000..b19672fd7e66 --- /dev/null +++ b/data_structures/queues/monotonic_queue.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from collections import deque + +arr = [1, 3, -1, -3, 5, 3, 6, 7] +window_size = 3 +expect = [3, 3, 5, 5, 6, 7] + + +def max_sliding_window(arr: list[float], window_size: int) -> list[float]: + """ + Given an array of integers nums, there is a sliding window of size k which is moving + from the very left of the array to the very right. + Each time the sliding window of length window_size moves right by one position. + Return the max sliding window. + >>> max_sliding_window(arr, window_size) == expect + True + """ + max_val = [] + mono_queue: deque = deque() + for i in range(len(arr)): + # pop the element if the index is outside the window size k + if mono_queue and i - mono_queue[0] >= window_size: + mono_queue.popleft() + # keep the queue monotonically decreasing + # so that the max value is always on the top + while mono_queue and arr[i] >= arr[mono_queue[-1]]: + mono_queue.pop() + mono_queue.append(i) + # the maximum value is the first element in queue + if i >= window_size - 1: + max_val.append(arr[mono_queue[0]]) + return max_val + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/data_structures/queues/priority_queue_using_list.py b/data_structures/queues/priority_queue_using_list.py new file mode 100644 index 000000000000..a5bf93b8e2c6 --- /dev/null +++ b/data_structures/queues/priority_queue_using_list.py @@ -0,0 +1,231 @@ +""" +Pure Python implementations of a Fixed Priority Queue and an Element Priority Queue +using Python lists. +""" + + +class OverFlowError(Exception): + pass + + +class UnderFlowError(Exception): + pass + + +class FixedPriorityQueue: + """ + Tasks can be added to a Priority Queue at any time and in any order but when Tasks + are removed then the Task with the highest priority is removed in FIFO order. In + code we will use three levels of priority with priority zero Tasks being the most + urgent (high priority) and priority 2 tasks being the least urgent. + + Examples + >>> fpq = FixedPriorityQueue() + >>> fpq.enqueue(0, 10) + >>> fpq.enqueue(1, 70) + >>> fpq.enqueue(0, 100) + >>> fpq.enqueue(2, 1) + >>> fpq.enqueue(2, 5) + >>> fpq.enqueue(1, 7) + >>> fpq.enqueue(2, 4) + >>> fpq.enqueue(1, 64) + >>> fpq.enqueue(0, 128) + >>> print(fpq) + Priority 0: [10, 100, 128] + Priority 1: [70, 7, 64] + Priority 2: [1, 5, 4] + >>> fpq.dequeue() + 10 + >>> fpq.dequeue() + 100 + >>> fpq.dequeue() + 128 + >>> fpq.dequeue() + 70 + >>> fpq.dequeue() + 7 + >>> print(fpq) + Priority 0: [] + Priority 1: [64] + Priority 2: [1, 5, 4] + >>> fpq.dequeue() + 64 + >>> fpq.dequeue() + 1 + >>> fpq.dequeue() + 5 + >>> fpq.dequeue() + 4 + >>> fpq.dequeue() + Traceback (most recent call last): + ... + data_structures.queues.priority_queue_using_list.UnderFlowError: All queues are empty + >>> print(fpq) + Priority 0: [] + Priority 1: [] + Priority 2: [] + """ # noqa: E501 + + def __init__(self) -> None: + self.queues = [ + [], + [], + [], + ] + + def enqueue(self, priority: int, data: int) -> None: + """ + Add an element to a queue based on its priority. + If the priority is invalid ValueError is raised. + If the queue is full an OverFlowError is raised. + """ + try: + if len(self.queues[priority]) >= 100: + raise OverflowError("Maximum queue size is 100") + self.queues[priority].append(data) + except IndexError: + raise ValueError("Valid priorities are 0, 1, and 2") + + def dequeue(self) -> int: + """ + Return the highest priority element in FIFO order. + If the queue is empty then an under flow exception is raised. + """ + for queue in self.queues: + if queue: + return queue.pop(0) + raise UnderFlowError("All queues are empty") + + def __str__(self) -> str: + return "\n".join(f"Priority {i}: {q}" for i, q in enumerate(self.queues)) + + +class ElementPriorityQueue: + """ + Element Priority Queue is the same as Fixed Priority Queue except that the value of + the element itself is the priority. The rules for priorities are the same the as + Fixed Priority Queue. + + >>> epq = ElementPriorityQueue() + >>> epq.enqueue(10) + >>> epq.enqueue(70) + >>> epq.enqueue(4) + >>> epq.enqueue(1) + >>> epq.enqueue(5) + >>> epq.enqueue(7) + >>> epq.enqueue(4) + >>> epq.enqueue(64) + >>> epq.enqueue(128) + >>> print(epq) + [10, 70, 4, 1, 5, 7, 4, 64, 128] + >>> epq.dequeue() + 1 + >>> epq.dequeue() + 4 + >>> epq.dequeue() + 4 + >>> epq.dequeue() + 5 + >>> epq.dequeue() + 7 + >>> epq.dequeue() + 10 + >>> print(epq) + [70, 64, 128] + >>> epq.dequeue() + 64 + >>> epq.dequeue() + 70 + >>> epq.dequeue() + 128 + >>> epq.dequeue() + Traceback (most recent call last): + ... + data_structures.queues.priority_queue_using_list.UnderFlowError: The queue is empty + >>> print(epq) + [] + """ + + def __init__(self) -> None: + self.queue = [] + + def enqueue(self, data: int) -> None: + """ + This function enters the element into the queue + If the queue is full an Exception is raised saying Over Flow! + """ + if len(self.queue) == 100: + raise OverFlowError("Maximum queue size is 100") + self.queue.append(data) + + def dequeue(self) -> int: + """ + Return the highest priority element in FIFO order. + If the queue is empty then an under flow exception is raised. + """ + if not self.queue: + raise UnderFlowError("The queue is empty") + data = min(self.queue) + self.queue.remove(data) + return data + + def __str__(self) -> str: + """ + Prints all the elements within the Element Priority Queue + """ + return str(self.queue) + + +def fixed_priority_queue() -> None: + fpq = FixedPriorityQueue() + fpq.enqueue(0, 10) + fpq.enqueue(1, 70) + fpq.enqueue(0, 100) + fpq.enqueue(2, 1) + fpq.enqueue(2, 5) + fpq.enqueue(1, 7) + fpq.enqueue(2, 4) + fpq.enqueue(1, 64) + fpq.enqueue(0, 128) + print(fpq) + print(fpq.dequeue()) + print(fpq.dequeue()) + print(fpq.dequeue()) + print(fpq.dequeue()) + print(fpq.dequeue()) + print(fpq) + print(fpq.dequeue()) + print(fpq.dequeue()) + print(fpq.dequeue()) + print(fpq.dequeue()) + print(fpq.dequeue()) + + +def element_priority_queue() -> None: + epq = ElementPriorityQueue() + epq.enqueue(10) + epq.enqueue(70) + epq.enqueue(100) + epq.enqueue(1) + epq.enqueue(5) + epq.enqueue(7) + epq.enqueue(4) + epq.enqueue(64) + epq.enqueue(128) + print(epq) + print(epq.dequeue()) + print(epq.dequeue()) + print(epq.dequeue()) + print(epq.dequeue()) + print(epq.dequeue()) + print(epq) + print(epq.dequeue()) + print(epq.dequeue()) + print(epq.dequeue()) + print(epq.dequeue()) + print(epq.dequeue()) + + +if __name__ == "__main__": + fixed_priority_queue() + element_priority_queue() diff --git a/data_structures/queues/queue_by_list.py b/data_structures/queues/queue_by_list.py new file mode 100644 index 000000000000..182cc4147c47 --- /dev/null +++ b/data_structures/queues/queue_by_list.py @@ -0,0 +1,138 @@ +"""Queue represented by a Python list""" + +from collections.abc import Iterable + + +class QueueByList[T]: + def __init__(self, iterable: Iterable[T] | None = None) -> None: + """ + >>> QueueByList() + Queue(()) + >>> QueueByList([10, 20, 30]) + Queue((10, 20, 30)) + >>> QueueByList((i**2 for i in range(1, 4))) + Queue((1, 4, 9)) + """ + self.entries: list[T] = list(iterable or []) + + def __len__(self) -> int: + """ + >>> len(QueueByList()) + 0 + >>> from string import ascii_lowercase + >>> len(QueueByList(ascii_lowercase)) + 26 + >>> queue = QueueByList() + >>> for i in range(1, 11): + ... queue.put(i) + >>> len(queue) + 10 + >>> for i in range(2): + ... queue.get() + 1 + 2 + >>> len(queue) + 8 + """ + + return len(self.entries) + + def __repr__(self) -> str: + """ + >>> queue = QueueByList() + >>> queue + Queue(()) + >>> str(queue) + 'Queue(())' + >>> queue.put(10) + >>> queue + Queue((10,)) + >>> queue.put(20) + >>> queue.put(30) + >>> queue + Queue((10, 20, 30)) + """ + + return f"Queue({tuple(self.entries)})" + + def put(self, item: T) -> None: + """Put `item` to the Queue + + >>> queue = QueueByList() + >>> queue.put(10) + >>> queue.put(20) + >>> len(queue) + 2 + >>> queue + Queue((10, 20)) + """ + + self.entries.append(item) + + def get(self) -> T: + """ + Get `item` from the Queue + + >>> queue = QueueByList((10, 20, 30)) + >>> queue.get() + 10 + >>> queue.put(40) + >>> queue.get() + 20 + >>> queue.get() + 30 + >>> len(queue) + 1 + >>> queue.get() + 40 + >>> queue.get() + Traceback (most recent call last): + ... + IndexError: Queue is empty + """ + + if not self.entries: + raise IndexError("Queue is empty") + return self.entries.pop(0) + + def rotate(self, rotation: int) -> None: + """Rotate the items of the Queue `rotation` times + + >>> queue = QueueByList([10, 20, 30, 40]) + >>> queue + Queue((10, 20, 30, 40)) + >>> queue.rotate(1) + >>> queue + Queue((20, 30, 40, 10)) + >>> queue.rotate(2) + >>> queue + Queue((40, 10, 20, 30)) + """ + + put = self.entries.append + get = self.entries.pop + + for _ in range(rotation): + put(get(0)) + + def get_front(self) -> T: + """Get the front item from the Queue + + >>> queue = QueueByList((10, 20, 30)) + >>> queue.get_front() + 10 + >>> queue + Queue((10, 20, 30)) + >>> queue.get() + 10 + >>> queue.get_front() + 20 + """ + + return self.entries[0] + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/data_structures/queues/queue_by_two_stacks.py b/data_structures/queues/queue_by_two_stacks.py new file mode 100644 index 000000000000..f9e302ffcedd --- /dev/null +++ b/data_structures/queues/queue_by_two_stacks.py @@ -0,0 +1,112 @@ +"""Queue implementation using two stacks""" + +from collections.abc import Iterable + + +class QueueByTwoStacks[T]: + def __init__(self, iterable: Iterable[T] | None = None) -> None: + """ + >>> QueueByTwoStacks() + Queue(()) + >>> QueueByTwoStacks([10, 20, 30]) + Queue((10, 20, 30)) + >>> QueueByTwoStacks((i**2 for i in range(1, 4))) + Queue((1, 4, 9)) + """ + self._stack1: list[T] = list(iterable or []) + self._stack2: list[T] = [] + + def __len__(self) -> int: + """ + >>> len(QueueByTwoStacks()) + 0 + >>> from string import ascii_lowercase + >>> len(QueueByTwoStacks(ascii_lowercase)) + 26 + >>> queue = QueueByTwoStacks() + >>> for i in range(1, 11): + ... queue.put(i) + ... + >>> len(queue) + 10 + >>> for i in range(2): + ... queue.get() + 1 + 2 + >>> len(queue) + 8 + """ + + return len(self._stack1) + len(self._stack2) + + def __repr__(self) -> str: + """ + >>> queue = QueueByTwoStacks() + >>> queue + Queue(()) + >>> str(queue) + 'Queue(())' + >>> queue.put(10) + >>> queue + Queue((10,)) + >>> queue.put(20) + >>> queue.put(30) + >>> queue + Queue((10, 20, 30)) + """ + return f"Queue({tuple(self._stack2[::-1] + self._stack1)})" + + def put(self, item: T) -> None: + """ + Put `item` into the Queue + + >>> queue = QueueByTwoStacks() + >>> queue.put(10) + >>> queue.put(20) + >>> len(queue) + 2 + >>> queue + Queue((10, 20)) + """ + + self._stack1.append(item) + + def get(self) -> T: + """ + Get `item` from the Queue + + >>> queue = QueueByTwoStacks((10, 20, 30)) + >>> queue.get() + 10 + >>> queue.put(40) + >>> queue.get() + 20 + >>> queue.get() + 30 + >>> len(queue) + 1 + >>> queue.get() + 40 + >>> queue.get() + Traceback (most recent call last): + ... + IndexError: Queue is empty + """ + + # To reduce number of attribute look-ups in `while` loop. + stack1_pop = self._stack1.pop + stack2_append = self._stack2.append + + if not self._stack2: + while self._stack1: + stack2_append(stack1_pop()) + + if not self._stack2: + raise IndexError("Queue is empty") + return self._stack2.pop() + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/data_structures/Queue/QueueOnPseudoStack.py b/data_structures/queues/queue_on_pseudo_stack.py similarity index 63% rename from data_structures/Queue/QueueOnPseudoStack.py rename to data_structures/queues/queue_on_pseudo_stack.py index b69fbcc988f7..95b84a309783 100644 --- a/data_structures/Queue/QueueOnPseudoStack.py +++ b/data_structures/queues/queue_on_pseudo_stack.py @@ -1,17 +1,22 @@ """Queue represented by a pseudo stack (represented by a list with pop and append)""" -class Queue(): - def __init__(self): + +from typing import Any + + +class Queue: + def __init__(self) -> None: self.stack = [] self.length = 0 - def __str__(self): - printed = '<' + str(self.stack)[1:-1] + '>' + def __str__(self) -> str: + printed = "<" + str(self.stack)[1:-1] + ">" return printed """Enqueues {@code item} @param item item to enqueue""" - def put(self, item): + + def put(self, item: Any) -> None: self.stack.append(item) self.length = self.length + 1 @@ -19,19 +24,21 @@ def put(self, item): @requirement: |self.length| > 0 @return dequeued item that was dequeued""" - def get(self): + + def get(self) -> Any: self.rotate(1) - dequeued = self.stack[self.length-1] + dequeued = self.stack[self.length - 1] self.stack = self.stack[:-1] - self.rotate(self.length-1) - self.length = self.length -1 + self.rotate(self.length - 1) + self.length = self.length - 1 return dequeued """Rotates the queue {@code rotation} times @param rotation number of times to rotate queue""" - def rotate(self, rotation): - for i in range(rotation): + + def rotate(self, rotation: int) -> None: + for _ in range(rotation): temp = self.stack[0] self.stack = self.stack[1:] self.put(temp) @@ -39,12 +46,14 @@ def rotate(self, rotation): """Reports item at the front of self @return item at front of self.stack""" - def front(self): + + def front(self) -> Any: front = self.get() self.put(front) - self.rotate(self.length-1) + self.rotate(self.length - 1) return front """Returns the length of this.stack""" - def size(self): + + def size(self) -> int: return self.length diff --git a/data_structures/stacks/__init__.py b/data_structures/stacks/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/stacks/balanced_parentheses.py b/data_structures/stacks/balanced_parentheses.py new file mode 100644 index 000000000000..a05627bd12dd --- /dev/null +++ b/data_structures/stacks/balanced_parentheses.py @@ -0,0 +1,44 @@ +from .stack import Stack + + +def balanced_parentheses(parentheses: str) -> bool: + """Use a stack to check if a string of parentheses is balanced. + >>> balanced_parentheses("([]{})") + True + >>> balanced_parentheses("[()]{}{[()()]()}") + True + >>> balanced_parentheses("[(])") + False + >>> balanced_parentheses("1+2*3-4") + True + >>> balanced_parentheses("{}") + True + >>> balanced_parentheses("))((") + False + >>> balanced_parentheses("((") + False + >>> balanced_parentheses("") + True + """ + stack: Stack[str] = Stack() + bracket_pairs = {"(": ")", "[": "]", "{": "}"} + for bracket in parentheses: + if bracket in bracket_pairs: + stack.push(bracket) + elif bracket in (")", "]", "}") and ( + stack.is_empty() or bracket_pairs[stack.pop()] != bracket + ): + return False + return stack.is_empty() + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + + examples = ["((()))", "((())", "(()))"] + print("Balanced parentheses demonstration:\n") + for example in examples: + not_str = "" if balanced_parentheses(example) else "not " + print(f"{example} is {not_str}balanced") diff --git a/data_structures/stacks/dijkstras_two_stack_algorithm.py b/data_structures/stacks/dijkstras_two_stack_algorithm.py new file mode 100644 index 000000000000..94d19156f1c3 --- /dev/null +++ b/data_structures/stacks/dijkstras_two_stack_algorithm.py @@ -0,0 +1,84 @@ +""" +Author: Alexander Joslin +GitHub: github.com/echoaj + +Explanation: https://medium.com/@haleesammar/implemented-in-js-dijkstras-2-stack- + algorithm-for-evaluating-mathematical-expressions-fc0837dae1ea + +We can use Dijkstra's two stack algorithm to solve an equation +such as: (5 + ((4 * 2) * (2 + 3))) + +THESE ARE THE ALGORITHM'S RULES: +RULE 1: Scan the expression from left to right. When an operand is encountered, + push it onto the operand stack. + +RULE 2: When an operator is encountered in the expression, + push it onto the operator stack. + +RULE 3: When a left parenthesis is encountered in the expression, ignore it. + +RULE 4: When a right parenthesis is encountered in the expression, + pop an operator off the operator stack. The two operands it must + operate on must be the last two operands pushed onto the operand stack. + We therefore pop the operand stack twice, perform the operation, + and push the result back onto the operand stack so it will be available + for use as an operand of the next operator popped off the operator stack. + +RULE 5: When the entire infix expression has been scanned, the value left on + the operand stack represents the value of the expression. + +NOTE: It only works with whole numbers. +""" + +__author__ = "Alexander Joslin" + +import operator as op + +from .stack import Stack + + +def dijkstras_two_stack_algorithm(equation: str) -> int: + """ + DocTests + >>> dijkstras_two_stack_algorithm("(5 + 3)") + 8 + >>> dijkstras_two_stack_algorithm("((9 - (2 + 9)) + (8 - 1))") + 5 + >>> dijkstras_two_stack_algorithm("((((3 - 2) - (2 + 3)) + (2 - 4)) + 3)") + -3 + + :param equation: a string + :return: result: an integer + """ + operators = {"*": op.mul, "/": op.truediv, "+": op.add, "-": op.sub} + + operand_stack: Stack[int] = Stack() + operator_stack: Stack[str] = Stack() + + for i in equation: + if i.isdigit(): + # RULE 1 + operand_stack.push(int(i)) + elif i in operators: + # RULE 2 + operator_stack.push(i) + elif i == ")": + # RULE 4 + opr = operator_stack.peek() + operator_stack.pop() + num1 = operand_stack.peek() + operand_stack.pop() + num2 = operand_stack.peek() + operand_stack.pop() + + total = operators[opr](num2, num1) + operand_stack.push(total) + + # RULE 5 + return operand_stack.peek() + + +if __name__ == "__main__": + equation = "(5 + ((4 * 2) * (2 + 3)))" + # answer = 45 + print(f"{equation} = {dijkstras_two_stack_algorithm(equation)}") diff --git a/data_structures/stacks/infix_to_postfix_conversion.py b/data_structures/stacks/infix_to_postfix_conversion.py new file mode 100644 index 000000000000..e697061937c9 --- /dev/null +++ b/data_structures/stacks/infix_to_postfix_conversion.py @@ -0,0 +1,113 @@ +""" +https://en.wikipedia.org/wiki/Infix_notation +https://en.wikipedia.org/wiki/Reverse_Polish_notation +https://en.wikipedia.org/wiki/Shunting-yard_algorithm +""" + +from typing import Literal + +from .balanced_parentheses import balanced_parentheses +from .stack import Stack + +PRECEDENCES: dict[str, int] = { + "+": 1, + "-": 1, + "*": 2, + "/": 2, + "^": 3, +} +ASSOCIATIVITIES: dict[str, Literal["LR", "RL"]] = { + "+": "LR", + "-": "LR", + "*": "LR", + "/": "LR", + "^": "RL", +} + + +def precedence(char: str) -> int: + """ + Return integer value representing an operator's precedence, or + order of operation. + https://en.wikipedia.org/wiki/Order_of_operations + """ + return PRECEDENCES.get(char, -1) + + +def associativity(char: str) -> Literal["LR", "RL"]: + """ + Return the associativity of the operator `char`. + https://en.wikipedia.org/wiki/Operator_associativity + """ + return ASSOCIATIVITIES[char] + + +def infix_to_postfix(expression_str: str) -> str: + """ + >>> infix_to_postfix("(1*(2+3)+4))") + Traceback (most recent call last): + ... + ValueError: Mismatched parentheses + >>> infix_to_postfix("") + '' + >>> infix_to_postfix("3+2") + '3 2 +' + >>> infix_to_postfix("(3+4)*5-6") + '3 4 + 5 * 6 -' + >>> infix_to_postfix("(1+2)*3/4-5") + '1 2 + 3 * 4 / 5 -' + >>> infix_to_postfix("a+b*c+(d*e+f)*g") + 'a b c * + d e * f + g * +' + >>> infix_to_postfix("x^y/(5*z)+2") + 'x y ^ 5 z * / 2 +' + >>> infix_to_postfix("2^3^2") + '2 3 2 ^ ^' + """ + if not balanced_parentheses(expression_str): + raise ValueError("Mismatched parentheses") + stack: Stack[str] = Stack() + postfix = [] + for char in expression_str: + if char.isalpha() or char.isdigit(): + postfix.append(char) + elif char == "(": + stack.push(char) + elif char == ")": + while not stack.is_empty() and stack.peek() != "(": + postfix.append(stack.pop()) + stack.pop() + else: + while True: + if stack.is_empty(): + stack.push(char) + break + + char_precedence = precedence(char) + tos_precedence = precedence(stack.peek()) + + if char_precedence > tos_precedence: + stack.push(char) + break + if char_precedence < tos_precedence: + postfix.append(stack.pop()) + continue + # Precedences are equal + if associativity(char) == "RL": + stack.push(char) + break + postfix.append(stack.pop()) + + while not stack.is_empty(): + postfix.append(stack.pop()) + return " ".join(postfix) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + expression = "a+b*(c^d-e)^(f+g*h)-i" + + print("Infix to Postfix Notation demonstration:\n") + print("Infix notation: " + expression) + print("Postfix notation: " + infix_to_postfix(expression)) diff --git a/data_structures/stacks/infix_to_prefix_conversion.py b/data_structures/stacks/infix_to_prefix_conversion.py new file mode 100644 index 000000000000..8eb86ba60530 --- /dev/null +++ b/data_structures/stacks/infix_to_prefix_conversion.py @@ -0,0 +1,193 @@ +""" +Output: + +Enter an Infix Equation = a + b ^c + Symbol | Stack | Postfix +---------------------------- + c | | c + ^ | ^ | c + b | ^ | cb + + | + | cb^ + a | + | cb^a + | | cb^a+ + + a+b^c (Infix) -> +a^bc (Prefix) +""" + + +def infix_2_postfix(infix: str) -> str: + """ + >>> infix_2_postfix("a+b^c") # doctest: +NORMALIZE_WHITESPACE + Symbol | Stack | Postfix + ---------------------------- + a | | a + + | + | a + b | + | ab + ^ | +^ | ab + c | +^ | abc + | + | abc^ + | | abc^+ + 'abc^+' + + >>> infix_2_postfix("1*((-a)*2+b)") # doctest: +NORMALIZE_WHITESPACE + Symbol | Stack | Postfix + ------------------------------------------- + 1 | | 1 + * | * | 1 + ( | *( | 1 + ( | *(( | 1 + - | *((- | 1 + a | *((- | 1a + ) | *( | 1a- + * | *(* | 1a- + 2 | *(* | 1a-2 + + | *(+ | 1a-2* + b | *(+ | 1a-2*b + ) | * | 1a-2*b+ + | | 1a-2*b+* + '1a-2*b+*' + + >>> infix_2_postfix("") + Symbol | Stack | Postfix + ---------------------------- + '' + + >>> infix_2_postfix("(()") + Traceback (most recent call last): + ... + ValueError: invalid expression + + >>> infix_2_postfix("())") + Traceback (most recent call last): + ... + IndexError: list index out of range + """ + stack = [] + post_fix = [] + priority = { + "^": 3, + "*": 2, + "/": 2, + "%": 2, + "+": 1, + "-": 1, + } # Priority of each operator + print_width = max(len(infix), 7) + + # Print table header for output + print( + "Symbol".center(8), + "Stack".center(print_width), + "Postfix".center(print_width), + sep=" | ", + ) + print("-" * (print_width * 3 + 7)) + + for x in infix: + if x.isalpha() or x.isdigit(): + post_fix.append(x) # if x is Alphabet / Digit, add it to Postfix + elif x == "(": + stack.append(x) # if x is "(" push to Stack + elif x == ")": # if x is ")" pop stack until "(" is encountered + if len(stack) == 0: # close bracket without open bracket + raise IndexError("list index out of range") + + while stack[-1] != "(": + post_fix.append(stack.pop()) # Pop stack & add the content to Postfix + stack.pop() + elif len(stack) == 0: + stack.append(x) # If stack is empty, push x to stack + else: # while priority of x is not > priority of element in the stack + while stack and stack[-1] != "(" and priority[x] <= priority[stack[-1]]: + post_fix.append(stack.pop()) # pop stack & add to Postfix + stack.append(x) # push x to stack + + print( + x.center(8), + ("".join(stack)).ljust(print_width), + ("".join(post_fix)).ljust(print_width), + sep=" | ", + ) # Output in tabular format + + while len(stack) > 0: # while stack is not empty + if stack[-1] == "(": # open bracket with no close bracket + raise ValueError("invalid expression") + + post_fix.append(stack.pop()) # pop stack & add to Postfix + print( + " ".center(8), + ("".join(stack)).ljust(print_width), + ("".join(post_fix)).ljust(print_width), + sep=" | ", + ) # Output in tabular format + + return "".join(post_fix) # return Postfix as str + + +def infix_2_prefix(infix: str) -> str: + """ + >>> infix_2_prefix("a+b^c") # doctest: +NORMALIZE_WHITESPACE + Symbol | Stack | Postfix + ---------------------------- + c | | c + ^ | ^ | c + b | ^ | cb + + | + | cb^ + a | + | cb^a + | | cb^a+ + '+a^bc' + + >>> infix_2_prefix("1*((-a)*2+b)") # doctest: +NORMALIZE_WHITESPACE + Symbol | Stack | Postfix + ------------------------------------------- + ( | ( | + b | ( | b + + | (+ | b + 2 | (+ | b2 + * | (+* | b2 + ( | (+*( | b2 + a | (+*( | b2a + - | (+*(- | b2a + ) | (+* | b2a- + ) | | b2a-*+ + * | * | b2a-*+ + 1 | * | b2a-*+1 + | | b2a-*+1* + '*1+*-a2b' + + >>> infix_2_prefix('') + Symbol | Stack | Postfix + ---------------------------- + '' + + >>> infix_2_prefix('(()') + Traceback (most recent call last): + ... + IndexError: list index out of range + + >>> infix_2_prefix('())') + Traceback (most recent call last): + ... + ValueError: invalid expression + """ + reversed_infix = list(infix[::-1]) # reverse the infix equation + + for i in range(len(reversed_infix)): + match reversed_infix[i]: + case "(": + reversed_infix[i] = ")" # change "(" to ")" + case ")": + reversed_infix[i] = "(" # change ")" to "(" + + # call infix_2_postfix on Infix, return reverse of Postfix + return (infix_2_postfix("".join(reversed_infix)))[::-1] + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + + Infix = input("\nEnter an Infix Equation = ") # Input an Infix equation + Infix = "".join(Infix.split()) # Remove spaces from the input + print("\n\t", Infix, "(Infix) -> ", infix_2_prefix(Infix), "(Prefix)") diff --git a/data_structures/stacks/kth_next_greater_element.py b/data_structures/stacks/kth_next_greater_element.py new file mode 100644 index 000000000000..d10ff1e80863 --- /dev/null +++ b/data_structures/stacks/kth_next_greater_element.py @@ -0,0 +1,95 @@ +""" +Implement the function to find kth Next Greatest Element (NGE) for all elements. +""" + +test_k = 10 +test_array = list(range(10000)) +expected_answers = [value + test_k for value in range(10000 - test_k)] + [None] * test_k + + +def find_kth_next_greater_element( + array: list[int | float], kth_ord: int +) -> list[int | float | None]: + """ + Efficient general method to seek the kth NGE for all elements. + Approach is entirely based on k stacks, which are actually very easy to understand. + These k stacks symbolize how many NGEs an element has already found. + + For example, for 1 <= j <= k, if an element is currently at the jth stack, + it means that this element has found its (j - 1)th NGE, now looking for jth NGE. + + By processing stacks from higher to lower ordinals, we can always ensure that + each stack stays monotonically non-increasing in terms of element value. + + Time complexity: O(kn) where n is the length of input array. + However, if k >= n, all elements won't find their respective kth NGE. + As a result, worst case time complexity is O(n^2) when k < n but k ≈ n. + + Space complexity: O(n), since at any point, an element is only in one of k stacks. + + Args: + array (list[int | float]): A list for which the kth NGE is computed. + A mix of integers and floats in list is allowed. + + kth_ord (int): Ordinal of the NGE to find. kth_ord must be a positive integer. + + Returns: + A list containing each element's kth NGE. If an element can't find its kth NGE, + None, instead of -1, is put as its entry, because input array might have -1. + + Example: + >>> find_kth_next_greater_element([1, 2, 3, 4, 5], 3) == [4, 5, None, None, None] + True + >>> find_kth_next_greater_element([2.5, 1.9, 4.3, 6.0], 1) == [4.3, 4.3, 6.0, None] + True + >>> find_kth_next_greater_element([1, 2, 3], 0) + Traceback (most recent call last): + ... + ValueError: kth_ord must be a positive integer. + >>> find_kth_next_greater_element(list(range(1000)), 1000) == [None] * 1000 + True + >>> find_kth_next_greater_element(test_array, test_k) == expected_answers + True + """ + if not isinstance(kth_ord, int) or kth_ord < 1: + raise ValueError("kth_ord must be a positive integer.") + + kth_next_greater_elements: list[int | float | None] = [None] * len(array) + if kth_ord >= len(array): # Trivial cases: nobody can have kth NGE. + return kth_next_greater_elements + + # For 1 <= j <= k, the jth stack is at the jth idx of stacks list. + # stacks[0]: a transporter that transfers entries between stacks. + # Each stack's entry is a tuple of (element, idx). + stacks: list[list[tuple[int | float, int]]] = [[] for _ in range(kth_ord + 1)] + + for idx, element in enumerate(array): + # From kth stack to answer found. + while stacks[kth_ord] and stacks[kth_ord][-1][0] < element: + _, prev_idx = stacks[kth_ord].pop() + kth_next_greater_elements[prev_idx] = element + + for stack_ord in range(kth_ord - 1, 0, -1): # From (k - 1)th to 1st stack. + while stacks[stack_ord] and stacks[stack_ord][-1][0] < element: + stacks[0].append(stacks[stack_ord].pop()) + + while stacks[0]: # Move to the next ordered stack. + stacks[stack_ord + 1].append(stacks[0].pop()) + + unvisited_elements_count = len(array) - 1 - idx + if unvisited_elements_count >= kth_ord: # Element has a chance to find kth NGE. + stacks[1].append((element, idx)) # Always join 1st stack to begin search. + + return kth_next_greater_elements + + +if __name__ == "__main__": + from doctest import testmod + from timeit import timeit + + testmod() + setup = "from __main__ import test_array, test_k, find_kth_next_greater_element" + print( + "find_kth_next_greater_element():", + timeit("find_kth_next_greater_element(test_array, test_k)", setup=setup), + ) diff --git a/data_structures/stacks/largest_rectangle_histogram.py b/data_structures/stacks/largest_rectangle_histogram.py new file mode 100644 index 000000000000..7575bd9f628d --- /dev/null +++ b/data_structures/stacks/largest_rectangle_histogram.py @@ -0,0 +1,39 @@ +def largest_rectangle_area(heights: list[int]) -> int: + """ + Inputs an array of integers representing the heights of bars, + and returns the area of the largest rectangle that can be formed + + >>> largest_rectangle_area([2, 1, 5, 6, 2, 3]) + 10 + + >>> largest_rectangle_area([2, 4]) + 4 + + >>> largest_rectangle_area([6, 2, 5, 4, 5, 1, 6]) + 12 + + >>> largest_rectangle_area([1]) + 1 + """ + stack: list[int] = [] + max_area = 0 + heights = [*heights, 0] # make a new list by appending the sentinel 0 + n = len(heights) + + for i in range(n): + # make sure the stack remains in increasing order + while stack and heights[i] < heights[stack[-1]]: + h = heights[stack.pop()] # height of the bar + # if stack is empty, it means entire width can be taken from index 0 to i-1 + w = i if not stack else i - stack[-1] - 1 # calculate width + max_area = max(max_area, h * w) + + stack.append(i) + + return max_area + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/stacks/lexicographical_numbers.py b/data_structures/stacks/lexicographical_numbers.py new file mode 100644 index 000000000000..6a174e7d9e95 --- /dev/null +++ b/data_structures/stacks/lexicographical_numbers.py @@ -0,0 +1,38 @@ +from collections.abc import Iterator + + +def lexical_order(max_number: int) -> Iterator[int]: + """ + Generate numbers in lexical order from 1 to max_number. + + >>> " ".join(map(str, lexical_order(13))) + '1 10 11 12 13 2 3 4 5 6 7 8 9' + >>> list(lexical_order(1)) + [1] + >>> " ".join(map(str, lexical_order(20))) + '1 10 11 12 13 14 15 16 17 18 19 2 20 3 4 5 6 7 8 9' + >>> " ".join(map(str, lexical_order(25))) + '1 10 11 12 13 14 15 16 17 18 19 2 20 21 22 23 24 25 3 4 5 6 7 8 9' + >>> list(lexical_order(12)) + [1, 10, 11, 12, 2, 3, 4, 5, 6, 7, 8, 9] + """ + + stack = [1] + + while stack: + num = stack.pop() + if num > max_number: + continue + + yield num + if (num % 10) != 9: + stack.append(num + 1) + + stack.append(num * 10) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + print(f"Numbers from 1 to 25 in lexical order: {list(lexical_order(26))}") diff --git a/data_structures/stacks/next_greater_element.py b/data_structures/stacks/next_greater_element.py new file mode 100644 index 000000000000..216850b4b894 --- /dev/null +++ b/data_structures/stacks/next_greater_element.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +arr = [-10, -5, 0, 5, 5.1, 11, 13, 21, 3, 4, -21, -10, -5, -1, 0] +expect = [-5, 0, 5, 5.1, 11, 13, 21, -1, 4, -1, -10, -5, -1, 0, -1] + + +def next_greatest_element_slow(arr: list[float]) -> list[float]: + """ + Get the Next Greatest Element (NGE) for each element in the array + by checking all subsequent elements to find the next greater one. + + This is a brute-force implementation, and it has a time complexity + of O(n^2), where n is the size of the array. + + Args: + arr: List of numbers for which the NGE is calculated. + + Returns: + List containing the next greatest elements. If no + greater element is found, -1 is placed in the result. + + Example: + >>> next_greatest_element_slow(arr) == expect + True + """ + + result = [] + arr_size = len(arr) + + for i in range(arr_size): + next_element: float = -1 + for j in range(i + 1, arr_size): + if arr[i] < arr[j]: + next_element = arr[j] + break + result.append(next_element) + return result + + +def next_greatest_element_fast(arr: list[float]) -> list[float]: + """ + Find the Next Greatest Element (NGE) for each element in the array + using a more readable approach. This implementation utilizes + enumerate() for the outer loop and slicing for the inner loop. + + While this improves readability over next_greatest_element_slow(), + it still has a time complexity of O(n^2). + + Args: + arr: List of numbers for which the NGE is calculated. + + Returns: + List containing the next greatest elements. If no + greater element is found, -1 is placed in the result. + + Example: + >>> next_greatest_element_fast(arr) == expect + True + """ + result = [] + for i, outer in enumerate(arr): + next_item: float = -1 + for inner in arr[i + 1 :]: + if outer < inner: + next_item = inner + break + result.append(next_item) + return result + + +def next_greatest_element(arr: list[float]) -> list[float]: + """ + Efficient solution to find the Next Greatest Element (NGE) for all elements + using a stack. The time complexity is reduced to O(n), making it suitable + for larger arrays. + + The stack keeps track of elements for which the next greater element hasn't + been found yet. By iterating through the array in reverse (from the last + element to the first), the stack is used to efficiently determine the next + greatest element for each element. + + Args: + arr: List of numbers for which the NGE is calculated. + + Returns: + List containing the next greatest elements. If no + greater element is found, -1 is placed in the result. + + Example: + >>> next_greatest_element(arr) == expect + True + """ + arr_size = len(arr) + stack: list[float] = [] + result: list[float] = [-1] * arr_size + + for index in reversed(range(arr_size)): + if stack: + while stack[-1] <= arr[index]: + stack.pop() + if not stack: + break + if stack: + result[index] = stack[-1] + stack.append(arr[index]) + return result + + +if __name__ == "__main__": + from doctest import testmod + from timeit import timeit + + testmod() + print(next_greatest_element_slow(arr)) + print(next_greatest_element_fast(arr)) + print(next_greatest_element(arr)) + + setup = ( + "from __main__ import arr, next_greatest_element_slow, " + "next_greatest_element_fast, next_greatest_element" + ) + print( + "next_greatest_element_slow():", + timeit("next_greatest_element_slow(arr)", setup=setup), + ) + print( + "next_greatest_element_fast():", + timeit("next_greatest_element_fast(arr)", setup=setup), + ) + print( + " next_greatest_element():", + timeit("next_greatest_element(arr)", setup=setup), + ) diff --git a/data_structures/stacks/postfix_evaluation.py b/data_structures/stacks/postfix_evaluation.py new file mode 100644 index 000000000000..03a87b9e0fa3 --- /dev/null +++ b/data_structures/stacks/postfix_evaluation.py @@ -0,0 +1,200 @@ +""" +Reverse Polish Nation is also known as Polish postfix notation or simply postfix +notation. +https://en.wikipedia.org/wiki/Reverse_Polish_notation +Classic examples of simple stack implementations. +Valid operators are +, -, *, /. +Each operand may be an integer or another expression. + +Output: + +Enter a Postfix Equation (space separated) = 5 6 9 * + + Symbol | Action | Stack +----------------------------------- + 5 | push(5) | 5 + 6 | push(6) | 5,6 + 9 | push(9) | 5,6,9 + | pop(9) | 5,6 + | pop(6) | 5 + * | push(6*9) | 5,54 + | pop(54) | 5 + | pop(5) | + + | push(5+54) | 59 + + Result = 59 +""" + +# Defining valid unary operator symbols +UNARY_OP_SYMBOLS = ("-", "+") + +# operators & their respective operation +OPERATORS = { + "^": lambda p, q: p**q, + "*": lambda p, q: p * q, + "/": lambda p, q: p / q, + "+": lambda p, q: p + q, + "-": lambda p, q: p - q, +} + + +def parse_token(token: str | float) -> float | str: + """ + Converts the given data to the appropriate number if it is indeed a number, else + returns the data as it is with a False flag. This function also serves as a check + of whether the input is a number or not. + + Parameters + ---------- + token: The data that needs to be converted to the appropriate operator or number. + + Returns + ------- + float or str + Returns a float if `token` is a number or a str if `token` is an operator + """ + if token in OPERATORS: + return token + try: + return float(token) + except ValueError: + msg = f"{token} is neither a number nor a valid operator" + raise ValueError(msg) + + +def evaluate(post_fix: list[str], verbose: bool = False) -> float: + """ + Evaluate postfix expression using a stack. + >>> evaluate(["0"]) + 0.0 + >>> evaluate(["-0"]) + -0.0 + >>> evaluate(["1"]) + 1.0 + >>> evaluate(["-1"]) + -1.0 + >>> evaluate(["-1.1"]) + -1.1 + >>> evaluate(["2", "1", "+", "3", "*"]) + 9.0 + >>> evaluate(["2", "1.9", "+", "3", "*"]) + 11.7 + >>> evaluate(["2", "-1.9", "+", "3", "*"]) + 0.30000000000000027 + >>> evaluate(["4", "13", "5", "/", "+"]) + 6.6 + >>> evaluate(["2", "-", "3", "+"]) + 1.0 + >>> evaluate(["-4", "5", "*", "6", "-"]) + -26.0 + >>> evaluate([]) + 0 + >>> evaluate(["4", "-", "6", "7", "/", "9", "8"]) + Traceback (most recent call last): + ... + ArithmeticError: Input is not a valid postfix expression + + Parameters + ---------- + post_fix: + The postfix expression is tokenized into operators and operands and stored + as a Python list + + verbose: + Display stack contents while evaluating the expression if verbose is True + + Returns + ------- + float + The evaluated value + """ + if not post_fix: + return 0 + # Checking the list to find out whether the postfix expression is valid + valid_expression = [parse_token(token) for token in post_fix] + if verbose: + # print table header + print("Symbol".center(8), "Action".center(12), "Stack", sep=" | ") + print("-" * (30 + len(post_fix))) + stack = [] + for x in valid_expression: + if x not in OPERATORS: + stack.append(x) # append x to stack + if verbose: + # output in tabular format + print( + f"{x}".rjust(8), + f"push({x})".ljust(12), + stack, + sep=" | ", + ) + continue + # If x is operator + # If only 1 value is inside the stack and + or - is encountered + # then this is unary + or - case + if x in UNARY_OP_SYMBOLS and len(stack) < 2: + b = stack.pop() # pop stack + if x == "-": + b *= -1 # negate b + stack.append(b) + if verbose: + # output in tabular format + print( + "".rjust(8), + f"pop({b})".ljust(12), + stack, + sep=" | ", + ) + print( + str(x).rjust(8), + f"push({x}{b})".ljust(12), + stack, + sep=" | ", + ) + continue + b = stack.pop() # pop stack + if verbose: + # output in tabular format + print( + "".rjust(8), + f"pop({b})".ljust(12), + stack, + sep=" | ", + ) + + a = stack.pop() # pop stack + if verbose: + # output in tabular format + print( + "".rjust(8), + f"pop({a})".ljust(12), + stack, + sep=" | ", + ) + # evaluate the 2 values popped from stack & push result to stack + stack.append(OPERATORS[x](a, b)) # type: ignore[index] + if verbose: + # output in tabular format + print( + f"{x}".rjust(8), + f"push({a}{x}{b})".ljust(12), + stack, + sep=" | ", + ) + # If everything is executed correctly, the stack will contain + # only one element which is the result + if len(stack) != 1: + raise ArithmeticError("Input is not a valid postfix expression") + return float(stack[0]) + + +if __name__ == "__main__": + # Create a loop so that the user can evaluate postfix expressions multiple times + while True: + expression = input("Enter a Postfix Expression (space separated): ").split(" ") + prompt = "Do you want to see stack contents while evaluating? [y/N]: " + verbose = input(prompt).strip().lower() == "y" + output = evaluate(expression, verbose) + print("Result = ", output) + prompt = "Do you want to enter another expression? [y/N]: " + if input(prompt).strip().lower() != "y": + break diff --git a/data_structures/stacks/postfix_to_infix_conversion.py b/data_structures/stacks/postfix_to_infix_conversion.py new file mode 100644 index 000000000000..dd8464914cfe --- /dev/null +++ b/data_structures/stacks/postfix_to_infix_conversion.py @@ -0,0 +1,63 @@ +""" +https://math.oxford.emory.edu/site/cs171/postfixExpressions/ +https://en.wikipedia.org/wiki/Shunting_yard_algorithm +""" + + +def postfix_to_infix(postfix_expression: str) -> str: + """ + Returns the infix expression for the given postfix expression as an argument + >>> postfix_to_infix("") + Traceback (most recent call last): + ... + ValueError: Invalid postfix expression. + >>> postfix_to_infix("123+*4+") + '((1*(2+3))+4)' + >>> postfix_to_infix("abc*+de*f+g*+") + '((a+(b*c))+(((d*e)+f)*g))' + >>> postfix_to_infix("xy^5z*/2+") + '(((x^y)/(5*z))+2)' + >>> postfix_to_infix("232^^") + '(2^(3^2))' + >>> postfix_to_infix("32+") + '(3+2)' + """ + + # Check for invalid input. + if postfix_expression is None or postfix_expression == "": + raise ValueError("Invalid postfix expression.") + + # Create a stack to store the operands and operators. + stack = [] + + # Iterate over the postfix expression. + for item in postfix_expression: + # If the item is an operand, push it onto the stack. + if item not in ["+", "-", "*", "/", "^"]: + stack.append(item) + else: + # If the item is an operator, pop the top two operands from the stack + # and concatenate the operator between them. + operand_2 = stack.pop() + operand_1 = stack.pop() + infix_expression = "(" + operand_1 + item + operand_2 + ")" + + # Push the resulting infix expression onto the stack. + stack.append(infix_expression) + + # The top element of the stack is the final infix expression. + return stack.pop() + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + # Enter the posfix expression with no whitespaces. + postfix_expression = "512+4*+3-" + + try: + infix_expression = postfix_to_infix(postfix_expression) + print("Infix expression: " + infix_expression) + except ValueError as e: + print(e) diff --git a/data_structures/stacks/prefix_evaluation.py b/data_structures/stacks/prefix_evaluation.py new file mode 100644 index 000000000000..2c5c60d1a850 --- /dev/null +++ b/data_structures/stacks/prefix_evaluation.py @@ -0,0 +1,92 @@ +""" +Program to evaluate a prefix expression. +https://en.wikipedia.org/wiki/Polish_notation +""" + +operators = { + "+": lambda x, y: x + y, + "-": lambda x, y: x - y, + "*": lambda x, y: x * y, + "/": lambda x, y: x / y, +} + + +def is_operand(c) -> bool: + """ + Return True if the given char c is an operand, e.g. it is a number + + >>> is_operand("1") + True + >>> is_operand("+") + False + """ + return c.isdigit() + + +def evaluate(expression) -> float: + """ + Evaluate a given expression in prefix notation. + Asserts that the given expression is valid. + + >>> evaluate("+ 9 * 2 6") + 21 + >>> evaluate("/ * 10 2 + 4 1 ") + 4.0 + >>> evaluate("2") + 2 + >>> evaluate("+ * 2 3 / 8 4") + 8.0 + """ + stack = [] + + # iterate over the string in reverse order + for c in expression.split()[::-1]: + # push operand to stack + if is_operand(c): + stack.append(int(c)) + + else: + # pop values from stack can calculate the result + # push the result onto the stack again + o1 = stack.pop() + o2 = stack.pop() + stack.append(operators[c](o1, o2)) + + return stack.pop() + + +def evaluate_recursive(expression: list[str]) -> float: + """ + Alternative recursive implementation + + >>> evaluate_recursive(['2']) + 2 + >>> expression = ['+', '*', '2', '3', '/', '8', '4'] + >>> evaluate_recursive(expression) + 8.0 + >>> expression + [] + >>> evaluate_recursive(['+', '9', '*', '2', '6']) + 21 + >>> evaluate_recursive(['/', '*', '10', '2', '+', '4', '1']) + 4.0 + """ + + op = expression.pop(0) + if is_operand(op): + return int(op) + + operation = operators[op] + + a = evaluate_recursive(expression) + b = evaluate_recursive(expression) + return operation(a, b) + + +# Driver code +if __name__ == "__main__": + test_expression = "+ 9 * 2 6" + print(evaluate(test_expression)) + + test_expression = "/ * 10 2 + 4 1 " + print(evaluate(test_expression)) diff --git a/data_structures/stacks/stack.py b/data_structures/stacks/stack.py new file mode 100644 index 000000000000..8de2f61085c9 --- /dev/null +++ b/data_structures/stacks/stack.py @@ -0,0 +1,215 @@ +from __future__ import annotations + +from typing import TypeVar + +T = TypeVar("T") + + +class StackOverflowError(BaseException): + pass + + +class StackUnderflowError(BaseException): + pass + + +class Stack[T]: + """A stack is an abstract data type that serves as a collection of + elements with two principal operations: push() and pop(). push() adds an + element to the top of the stack, and pop() removes an element from the top + of a stack. The order in which elements come off of a stack are + Last In, First Out (LIFO). + https://en.wikipedia.org/wiki/Stack_(abstract_data_type) + """ + + def __init__(self, limit: int = 10) -> None: + self.stack: list[T] = [] + self.limit = limit + + def __bool__(self) -> bool: + return bool(self.stack) + + def __str__(self) -> str: + return str(self.stack) + + def push(self, data: T) -> None: + """ + Push an element to the top of the stack. + + >>> S = Stack(2) # stack size = 2 + >>> S.push(10) + >>> S.push(20) + >>> print(S) + [10, 20] + + >>> S = Stack(1) # stack size = 1 + >>> S.push(10) + >>> S.push(20) + Traceback (most recent call last): + ... + data_structures.stacks.stack.StackOverflowError + + """ + if len(self.stack) >= self.limit: + raise StackOverflowError + self.stack.append(data) + + def pop(self) -> T: + """ + Pop an element off of the top of the stack. + + >>> S = Stack() + >>> S.push(-5) + >>> S.push(10) + >>> S.pop() + 10 + + >>> Stack().pop() + Traceback (most recent call last): + ... + data_structures.stacks.stack.StackUnderflowError + """ + if not self.stack: + raise StackUnderflowError + return self.stack.pop() + + def peek(self) -> T: + """ + Peek at the top-most element of the stack. + + >>> S = Stack() + >>> S.push(-5) + >>> S.push(10) + >>> S.peek() + 10 + + >>> Stack().peek() + Traceback (most recent call last): + ... + data_structures.stacks.stack.StackUnderflowError + """ + if not self.stack: + raise StackUnderflowError + return self.stack[-1] + + def is_empty(self) -> bool: + """ + Check if a stack is empty. + + >>> S = Stack() + >>> S.is_empty() + True + + >>> S = Stack() + >>> S.push(10) + >>> S.is_empty() + False + """ + return not bool(self.stack) + + def is_full(self) -> bool: + """ + >>> S = Stack() + >>> S.is_full() + False + + >>> S = Stack(1) + >>> S.push(10) + >>> S.is_full() + True + """ + return self.size() == self.limit + + def size(self) -> int: + """ + Return the size of the stack. + + >>> S = Stack(3) + >>> S.size() + 0 + + >>> S = Stack(3) + >>> S.push(10) + >>> S.size() + 1 + + >>> S = Stack(3) + >>> S.push(10) + >>> S.push(20) + >>> S.size() + 2 + """ + return len(self.stack) + + def __contains__(self, item: T) -> bool: + """ + Check if item is in stack + + >>> S = Stack(3) + >>> S.push(10) + >>> 10 in S + True + + >>> S = Stack(3) + >>> S.push(10) + >>> 20 in S + False + """ + return item in self.stack + + +def test_stack() -> None: + """ + >>> test_stack() + """ + stack: Stack[int] = Stack(10) + assert bool(stack) is False + assert stack.is_empty() is True + assert stack.is_full() is False + assert str(stack) == "[]" + + try: + _ = stack.pop() + raise AssertionError # This should not happen + except StackUnderflowError: + assert True # This should happen + + try: + _ = stack.peek() + raise AssertionError # This should not happen + except StackUnderflowError: + assert True # This should happen + + for i in range(10): + assert stack.size() == i + stack.push(i) + + assert bool(stack) + assert not stack.is_empty() + assert stack.is_full() + assert str(stack) == str(list(range(10))) + assert stack.pop() == 9 + assert stack.peek() == 8 + + stack.push(100) + assert str(stack) == str([0, 1, 2, 3, 4, 5, 6, 7, 8, 100]) + + try: + stack.push(200) + raise AssertionError # This should not happen + except StackOverflowError: + assert True # This should happen + + assert not stack.is_empty() + assert stack.size() == 10 + + assert 5 in stack + assert 55 not in stack + + +if __name__ == "__main__": + test_stack() + + import doctest + + doctest.testmod() diff --git a/data_structures/stacks/stack_tracking_min_max.py b/data_structures/stacks/stack_tracking_min_max.py new file mode 100644 index 000000000000..6c638110de19 --- /dev/null +++ b/data_structures/stacks/stack_tracking_min_max.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +import math + +""" +Python3 stack implementation that retrieves min/max +elements of the stack in constant time +""" + + +class StackData: + """ + Object stored on the stack + """ + + def __init__( + self, current_value: float, min_value: float, max_value: float + ) -> None: + self.current_value = current_value + self.min_value = min_value + self.max_value = max_value + + +class MinMaxStack: + """ + Main stack implementation + """ + + def __init__(self, max_stack_size: int = 10) -> None: + self.max_size = max_stack_size + self.stack: list[StackData] = [] + + def push_value(self, value: float) -> bool: + """ + Push new value on top of stack + + >>> test_stack = MinMaxStack(3) + >>> test_stack.push_value(1) + True + >>> test_stack.push_value(2) + True + >>> test_stack.push_value(3) + True + >>> test_stack.push_value(4) + Traceback (most recent call last): + ... + Exception: Max stack size reached. + + """ + + if len(self.stack) == self.max_size: + raise Exception("Max stack size reached.") + + if len(self.stack) == 0: + self.stack.append(StackData(value, value, value)) + else: + """Store min/max values with current value""" + + new_min = min(self.stack[-1].min_value, value) + new_max = max(self.stack[-1].max_value, value) + self.stack.append(StackData(value, new_min, new_max)) + + return True + + def pop_value(self) -> float: + """ + Remove the top value from the stack. + + >>> test_stack = MinMaxStack() + >>> test_stack.push_value(1) + True + >>> test_stack.push_value(2) + True + >>> test_stack.pop_value() + 2 + >>> test_stack.pop_value() + 1 + >>> test_stack.pop_value() + Stack is empty + -inf + """ + + if self.stack_is_valid(): + return self.stack.pop().current_value + + return -math.inf + + def get_current_max(self) -> float: + """ + Get the highest value on the stack in constant time + + >>> test_stack = MinMaxStack(3) + >>> test_stack.push_value(-450.45) + True + >>> test_stack.push_value(450.45) + True + >>> test_stack.push_value(0) + True + >>> test_stack.get_current_max() + 450.45 + >>> test_stack.pop_value() + 0 + >>> test_stack.get_current_max() + 450.45 + >>> test_stack.pop_value() + 450.45 + >>> test_stack.get_current_max() + -450.45 + >>> test_stack.pop_value() + -450.45 + >>> test_stack.get_current_max() + Stack is empty + -inf + """ + if self.stack_is_valid(): + return self.stack[-1].max_value + + return -math.inf + + def get_current_min(self) -> float: + """ + Get the lowest value on the stack in constant time + + >>> test_stack = MinMaxStack(3) + >>> test_stack.push_value(123) + True + >>> test_stack.push_value(-123) + True + >>> test_stack.push_value(0) + True + >>> test_stack.get_current_min() + -123 + >>> test_stack.pop_value() + 0 + >>> test_stack.get_current_min() + -123 + >>> test_stack.pop_value() + -123 + >>> test_stack.get_current_min() + 123 + >>> test_stack.pop_value() + 123 + >>> test_stack.get_current_min() + Stack is empty + -inf + """ + if self.stack_is_valid(): + return self.stack[-1].min_value + + return -math.inf + + def stack_is_valid(self) -> bool: + """ + Validate stack is not empty + + >>> test_stack = MinMaxStack(3) + >>> test_stack.stack_is_valid() + Stack is empty + False + >>> test_stack.push_value(0) + True + >>> test_stack.stack_is_valid() + True + """ + if len(self.stack) == 0: + print("Stack is empty") + return False + + return True + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/data_structures/stacks/stack_using_two_queues.py b/data_structures/stacks/stack_using_two_queues.py new file mode 100644 index 000000000000..c6687785de37 --- /dev/null +++ b/data_structures/stacks/stack_using_two_queues.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +from collections import deque +from dataclasses import dataclass, field + + +@dataclass +class StackWithQueues: + """ + https://www.geeksforgeeks.org/implement-stack-using-queue/ + + >>> stack = StackWithQueues() + >>> stack.push(1) + >>> stack.push(2) + >>> stack.push(3) + >>> stack.peek() + 3 + >>> stack.pop() + 3 + >>> stack.peek() + 2 + >>> stack.pop() + 2 + >>> stack.pop() + 1 + >>> stack.peek() is None + True + >>> stack.pop() + Traceback (most recent call last): + ... + IndexError: pop from an empty deque + """ + + main_queue: deque[int] = field(default_factory=deque) + temp_queue: deque[int] = field(default_factory=deque) + + def push(self, item: int) -> None: + self.temp_queue.append(item) + while self.main_queue: + self.temp_queue.append(self.main_queue.popleft()) + self.main_queue, self.temp_queue = self.temp_queue, self.main_queue + + def pop(self) -> int: + return self.main_queue.popleft() + + def peek(self) -> int | None: + return self.main_queue[0] if self.main_queue else None + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + stack: StackWithQueues | None = StackWithQueues() + while stack: + print("\nChoose operation:") + print("1. Push") + print("2. Pop") + print("3. Peek") + print("4. Quit") + + choice = input("Enter choice (1/2/3/4): ") + + match choice: + case "1": + element = int(input("Enter an integer to push: ").strip()) + stack.push(element) + print(f"{element} pushed onto the stack.") + case "2": + popped_element = stack.pop() + if popped_element is not None: + print(f"Popped element: {popped_element}") + else: + print("Stack is empty.") + case "3": + peeked_element = stack.peek() + if peeked_element is not None: + print(f"Top element: {peeked_element}") + else: + print("Stack is empty.") + case "4": + del stack + stack = None + case _: + print("Invalid choice. Please try again.") diff --git a/data_structures/stacks/stack_with_doubly_linked_list.py b/data_structures/stacks/stack_with_doubly_linked_list.py new file mode 100644 index 000000000000..347ae8ec801c --- /dev/null +++ b/data_structures/stacks/stack_with_doubly_linked_list.py @@ -0,0 +1,130 @@ +# A complete working Python program to demonstrate all +# stack operations using a doubly linked list + +from __future__ import annotations + +from typing import TypeVar + +T = TypeVar("T") + + +class Node[T]: + def __init__(self, data: T) -> None: + self.data = data # Assign data + self.next: Node[T] | None = None # Initialize next as null + self.prev: Node[T] | None = None # Initialize prev as null + + +class Stack[T]: + """ + >>> stack = Stack() + >>> stack.is_empty() + True + >>> stack.print_stack() + stack elements are: + >>> for i in range(4): + ... stack.push(i) + ... + >>> stack.is_empty() + False + >>> stack.print_stack() + stack elements are: + 3->2->1->0-> + >>> stack.top() + 3 + >>> len(stack) + 4 + >>> stack.pop() + 3 + >>> stack.print_stack() + stack elements are: + 2->1->0-> + """ + + def __init__(self) -> None: + self.head: Node[T] | None = None + + def push(self, data: T) -> None: + """add a Node to the stack""" + if self.head is None: + self.head = Node(data) + else: + new_node = Node(data) + self.head.prev = new_node + new_node.next = self.head + new_node.prev = None + self.head = new_node + + def pop(self) -> T | None: + """pop the top element off the stack""" + if self.head is None: + return None + else: + assert self.head is not None + temp = self.head.data + self.head = self.head.next + if self.head is not None: + self.head.prev = None + return temp + + def top(self) -> T | None: + """return the top element of the stack""" + return self.head.data if self.head is not None else None + + def __len__(self) -> int: + temp = self.head + count = 0 + while temp is not None: + count += 1 + temp = temp.next + return count + + def is_empty(self) -> bool: + return self.head is None + + def print_stack(self) -> None: + print("stack elements are:") + temp = self.head + while temp is not None: + print(temp.data, end="->") + temp = temp.next + + +# Code execution starts here +if __name__ == "__main__": + # Start with the empty stack + stack: Stack[int] = Stack() + + # Insert 4 at the beginning. So stack becomes 4->None + print("Stack operations using Doubly LinkedList") + stack.push(4) + + # Insert 5 at the beginning. So stack becomes 4->5->None + stack.push(5) + + # Insert 6 at the beginning. So stack becomes 4->5->6->None + stack.push(6) + + # Insert 7 at the beginning. So stack becomes 4->5->6->7->None + stack.push(7) + + # Print the stack + stack.print_stack() + + # Print the top element + print("\nTop element is ", stack.top()) + + # Print the stack size + print("Size of the stack is ", len(stack)) + + # pop the top element + stack.pop() + + # pop the top element + stack.pop() + + # two elements have now been popped off + stack.print_stack() + + # Print True if the stack is empty else False + print("\nstack is empty:", stack.is_empty()) diff --git a/data_structures/stacks/stack_with_singly_linked_list.py b/data_structures/stacks/stack_with_singly_linked_list.py new file mode 100644 index 000000000000..6257c33a4ee8 --- /dev/null +++ b/data_structures/stacks/stack_with_singly_linked_list.py @@ -0,0 +1,165 @@ +"""A Stack using a linked list like structure""" + +from __future__ import annotations + +from collections.abc import Iterator +from typing import TypeVar + +T = TypeVar("T") + + +class Node[T]: + def __init__(self, data: T) -> None: + self.data = data + self.next: Node[T] | None = None + + def __str__(self) -> str: + return f"{self.data}" + + +class LinkedStack[T]: + """ + Linked List Stack implementing push (to top), + pop (from top) and is_empty + + >>> stack = LinkedStack() + >>> stack.is_empty() + True + >>> stack.push(5) + >>> stack.push(9) + >>> stack.push('python') + >>> stack.is_empty() + False + >>> stack.pop() + 'python' + >>> stack.push('algorithms') + >>> stack.pop() + 'algorithms' + >>> stack.pop() + 9 + >>> stack.pop() + 5 + >>> stack.is_empty() + True + >>> stack.pop() + Traceback (most recent call last): + ... + IndexError: pop from empty stack + """ + + def __init__(self) -> None: + self.top: Node[T] | None = None + + def __iter__(self) -> Iterator[T]: + node = self.top + while node: + yield node.data + node = node.next + + def __str__(self) -> str: + """ + >>> stack = LinkedStack() + >>> stack.push("c") + >>> stack.push("b") + >>> stack.push("a") + >>> str(stack) + 'a->b->c' + """ + return "->".join([str(item) for item in self]) + + def __len__(self) -> int: + """ + >>> stack = LinkedStack() + >>> len(stack) == 0 + True + >>> stack.push("c") + >>> stack.push("b") + >>> stack.push("a") + >>> len(stack) == 3 + True + """ + return len(tuple(iter(self))) + + def is_empty(self) -> bool: + """ + >>> stack = LinkedStack() + >>> stack.is_empty() + True + >>> stack.push(1) + >>> stack.is_empty() + False + """ + return self.top is None + + def push(self, item: T) -> None: + """ + >>> stack = LinkedStack() + >>> stack.push("Python") + >>> stack.push("Java") + >>> stack.push("C") + >>> str(stack) + 'C->Java->Python' + """ + node = Node(item) + if not self.is_empty(): + node.next = self.top + self.top = node + + def pop(self) -> T: + """ + >>> stack = LinkedStack() + >>> stack.pop() + Traceback (most recent call last): + ... + IndexError: pop from empty stack + >>> stack.push("c") + >>> stack.push("b") + >>> stack.push("a") + >>> stack.pop() == 'a' + True + >>> stack.pop() == 'b' + True + >>> stack.pop() == 'c' + True + """ + if self.is_empty(): + raise IndexError("pop from empty stack") + assert isinstance(self.top, Node) + pop_node = self.top + self.top = self.top.next + return pop_node.data + + def peek(self) -> T: + """ + >>> stack = LinkedStack() + >>> stack.push("Java") + >>> stack.push("C") + >>> stack.push("Python") + >>> stack.peek() + 'Python' + """ + if self.is_empty(): + raise IndexError("peek from empty stack") + + assert self.top is not None + return self.top.data + + def clear(self) -> None: + """ + >>> stack = LinkedStack() + >>> stack.push("Java") + >>> stack.push("C") + >>> stack.push("Python") + >>> str(stack) + 'Python->C->Java' + >>> stack.clear() + >>> len(stack) == 0 + True + """ + self.top = None + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/data_structures/stacks/stock_span_problem.py b/data_structures/stacks/stock_span_problem.py new file mode 100644 index 000000000000..7a364f0debb6 --- /dev/null +++ b/data_structures/stacks/stock_span_problem.py @@ -0,0 +1,73 @@ +""" +The stock span problem is a financial problem where we have a series of n daily +price quotes for a stock and we need to calculate span of stock's price for all n days. + +The span Si of the stock's price on a given day i is defined as the maximum +number of consecutive days just before the given day, for which the price of the stock +on the current day is less than or equal to its price on the given day. +""" + + +def calculate_span(price: list[int]) -> list[int]: + """ + Calculate the span values for a given list of stock prices. + Args: + price: List of stock prices. + Returns: + List of span values. + + >>> calculate_span([10, 4, 5, 90, 120, 80]) + [1, 1, 2, 4, 5, 1] + >>> calculate_span([100, 50, 60, 70, 80, 90]) + [1, 1, 2, 3, 4, 5] + >>> calculate_span([5, 4, 3, 2, 1]) + [1, 1, 1, 1, 1] + >>> calculate_span([1, 2, 3, 4, 5]) + [1, 2, 3, 4, 5] + >>> calculate_span([10, 20, 30, 40, 50]) + [1, 2, 3, 4, 5] + >>> calculate_span([100, 80, 60, 70, 60, 75, 85]) + [1, 1, 1, 2, 1, 4, 6] + """ + n = len(price) + s = [0] * n + # Create a stack and push index of fist element to it + st = [] + st.append(0) + + # Span value of first element is always 1 + s[0] = 1 + + # Calculate span values for rest of the elements + for i in range(1, n): + # Pop elements from stack while stack is not + # empty and top of stack is smaller than price[i] + while len(st) > 0 and price[st[-1]] <= price[i]: + st.pop() + + # If stack becomes empty, then price[i] is greater + # than all elements on left of it, i.e. price[0], + # price[1], ..price[i-1]. Else the price[i] is + # greater than elements after top of stack + s[i] = i + 1 if len(st) <= 0 else (i - st[-1]) + + # Push this element to stack + st.append(i) + + return s + + +# A utility function to print elements of array +def print_array(arr, n) -> None: + for i in range(n): + print(arr[i], end=" ") + + +# Driver program to test above function +price = [10, 4, 5, 90, 120, 80] + +# Calculate the span values +S = calculate_span(price) + +# Print the calculated span values +print_array(S, len(price)) diff --git a/data_structures/suffix_tree/__init__.py b/data_structures/suffix_tree/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/suffix_tree/example/__init__.py b/data_structures/suffix_tree/example/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/suffix_tree/example/example_usage.py b/data_structures/suffix_tree/example/example_usage.py new file mode 100644 index 000000000000..724ac57e8bfb --- /dev/null +++ b/data_structures/suffix_tree/example/example_usage.py @@ -0,0 +1,37 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11554 +# https://github.com/TheAlgorithms/Python/pull/11554 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +from data_structures.suffix_tree.suffix_tree import SuffixTree + + +def main() -> None: + """ + Demonstrate the usage of the SuffixTree class. + + - Initializes a SuffixTree with a predefined text. + - Defines a list of patterns to search for within the suffix tree. + - Searches for each pattern in the suffix tree. + + Patterns tested: + - "ana" (found) --> True + - "ban" (found) --> True + - "na" (found) --> True + - "xyz" (not found) --> False + - "mon" (found) --> True + """ + text = "monkey banana" + suffix_tree = SuffixTree(text) + + patterns = ["ana", "ban", "na", "xyz", "mon"] + for pattern in patterns: + found = suffix_tree.search(pattern) + print(f"Pattern '{pattern}' found: {found}") + + +if __name__ == "__main__": + main() diff --git a/data_structures/suffix_tree/suffix_tree.py b/data_structures/suffix_tree/suffix_tree.py new file mode 100644 index 000000000000..ad54fb0ba009 --- /dev/null +++ b/data_structures/suffix_tree/suffix_tree.py @@ -0,0 +1,66 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11554 +# https://github.com/TheAlgorithms/Python/pull/11554 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +from data_structures.suffix_tree.suffix_tree_node import SuffixTreeNode + + +class SuffixTree: + def __init__(self, text: str) -> None: + """ + Initializes the suffix tree with the given text. + + Args: + text (str): The text for which the suffix tree is to be built. + """ + self.text: str = text + self.root: SuffixTreeNode = SuffixTreeNode() + self.build_suffix_tree() + + def build_suffix_tree(self) -> None: + """ + Builds the suffix tree for the given text by adding all suffixes. + """ + text = self.text + n = len(text) + for i in range(n): + suffix = text[i:] + self._add_suffix(suffix, i) + + def _add_suffix(self, suffix: str, index: int) -> None: + """ + Adds a suffix to the suffix tree. + + Args: + suffix (str): The suffix to add. + index (int): The starting index of the suffix in the original text. + """ + node = self.root + for char in suffix: + if char not in node.children: + node.children[char] = SuffixTreeNode() + node = node.children[char] + node.is_end_of_string = True + node.start = index + node.end = index + len(suffix) - 1 + + def search(self, pattern: str) -> bool: + """ + Searches for a pattern in the suffix tree. + + Args: + pattern (str): The pattern to search for. + + Returns: + bool: True if the pattern is found, False otherwise. + """ + node = self.root + for char in pattern: + if char not in node.children: + return False + node = node.children[char] + return True diff --git a/data_structures/suffix_tree/suffix_tree_node.py b/data_structures/suffix_tree/suffix_tree_node.py new file mode 100644 index 000000000000..e5b628645063 --- /dev/null +++ b/data_structures/suffix_tree/suffix_tree_node.py @@ -0,0 +1,36 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11554 +# https://github.com/TheAlgorithms/Python/pull/11554 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +from __future__ import annotations + + +class SuffixTreeNode: + def __init__( + self, + children: dict[str, SuffixTreeNode] | None = None, + is_end_of_string: bool = False, + start: int | None = None, + end: int | None = None, + suffix_link: SuffixTreeNode | None = None, + ) -> None: + """ + Initializes a suffix tree node. + + Parameters: + children (dict[str, SuffixTreeNode] | None): The children of this node. + is_end_of_string (bool): Indicates if this node represents + the end of a string. + start (int | None): The start index of the suffix in the text. + end (int | None): The end index of the suffix in the text. + suffix_link (SuffixTreeNode | None): Link to another suffix tree node. + """ + self.children = children or {} + self.is_end_of_string = is_end_of_string + self.start = start + self.end = end + self.suffix_link = suffix_link diff --git a/data_structures/suffix_tree/tests/__init__.py b/data_structures/suffix_tree/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/suffix_tree/tests/test_suffix_tree.py b/data_structures/suffix_tree/tests/test_suffix_tree.py new file mode 100644 index 000000000000..c9dbe199d19d --- /dev/null +++ b/data_structures/suffix_tree/tests/test_suffix_tree.py @@ -0,0 +1,59 @@ +# Created by: Ramy-Badr-Ahmed (https://github.com/Ramy-Badr-Ahmed) +# in Pull Request: #11554 +# https://github.com/TheAlgorithms/Python/pull/11554 +# +# Please mention me (@Ramy-Badr-Ahmed) in any issue or pull request +# addressing bugs/corrections to this file. +# Thank you! + +import unittest + +from data_structures.suffix_tree.suffix_tree import SuffixTree + + +class TestSuffixTree(unittest.TestCase): + def setUp(self) -> None: + """Set up the initial conditions for each test.""" + self.text = "banana" + self.suffix_tree = SuffixTree(self.text) + + def test_search_existing_patterns(self) -> None: + """Test searching for patterns that exist in the suffix tree.""" + patterns = ["ana", "ban", "na"] + for pattern in patterns: + with self.subTest(pattern=pattern): + assert self.suffix_tree.search(pattern), ( + f"Pattern '{pattern}' should be found." + ) + + def test_search_non_existing_patterns(self) -> None: + """Test searching for patterns that do not exist in the suffix tree.""" + patterns = ["xyz", "apple", "cat"] + for pattern in patterns: + with self.subTest(pattern=pattern): + assert not self.suffix_tree.search(pattern), ( + f"Pattern '{pattern}' should not be found." + ) + + def test_search_empty_pattern(self) -> None: + """Test searching for an empty pattern.""" + assert self.suffix_tree.search(""), "An empty pattern should be found." + + def test_search_full_text(self) -> None: + """Test searching for the full text.""" + assert self.suffix_tree.search(self.text), ( + "The full text should be found in the suffix tree." + ) + + def test_search_substrings(self) -> None: + """Test searching for substrings of the full text.""" + substrings = ["ban", "ana", "a", "na"] + for substring in substrings: + with self.subTest(substring=substring): + assert self.suffix_tree.search(substring), ( + f"Substring '{substring}' should be found." + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/data_structures/trie/__init__.py b/data_structures/trie/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data_structures/trie/radix_tree.py b/data_structures/trie/radix_tree.py new file mode 100644 index 000000000000..11e0dc23faa9 --- /dev/null +++ b/data_structures/trie/radix_tree.py @@ -0,0 +1,252 @@ +""" +A Radix Tree is a data structure that represents a space-optimized +trie (prefix tree) in which each node that is the only child is merged +with its parent [https://en.wikipedia.org/wiki/Radix_tree] +""" + +import unittest + + +class RadixNode: + def __init__(self, prefix: str = "", is_leaf: bool = False) -> None: + # Mapping from the first character of the prefix of the node + self.nodes: dict[str, RadixNode] = {} + + # A node will be a leaf if the tree contains its word + self.is_leaf = is_leaf + + self.prefix = prefix + + def match(self, word: str) -> tuple[str, str, str]: + """Compute the common substring of the prefix of the node and a word + + Args: + word (str): word to compare + + Returns: + (str, str, str): common substring, remaining prefix, remaining word + + >>> RadixNode("myprefix").match("mystring") + ('my', 'prefix', 'string') + """ + x = 0 + for q, w in zip(self.prefix, word): + if q != w: + break + + x += 1 + + return self.prefix[:x], self.prefix[x:], word[x:] + + def insert_many(self, words: list[str]) -> None: + """Insert many words in the tree + + Args: + words (list[str]): list of words + + >>> RadixNode("myprefix").insert_many(["mystring", "hello"]) + """ + for word in words: + self.insert(word) + + def insert(self, word: str) -> None: + """Insert a word into the tree + + Args: + word (str): word to insert + + >>> RadixNode("myprefix").insert("mystring") + + >>> root = RadixNode() + >>> root.insert_many(['myprefix', 'myprefixA', 'myprefixAA']) + >>> root.print_tree() + - myprefix (leaf) + -- A (leaf) + --- A (leaf) + """ + ## Handle the Case where the word is empty by using an if branch + if word == "": + self.is_leaf = True + return + + # Case 1: If the word is the prefix of the node + # Solution: We set the current node as leaf + if self.prefix == word and not self.is_leaf: + self.is_leaf = True + + # Case 2: The node has no edges that have a prefix to the word + # Solution: We create an edge from the current node to a new one + # containing the word + elif word[0] not in self.nodes: + self.nodes[word[0]] = RadixNode(prefix=word, is_leaf=True) + + else: + incoming_node = self.nodes[word[0]] + matching_string, remaining_prefix, remaining_word = incoming_node.match( + word + ) + + # Case 3: The node prefix is equal to the matching + # Solution: We insert the remaining word on the next node + if remaining_prefix == "": + self.nodes[matching_string[0]].insert(remaining_word) + + # Case 4: The word is greater than or equal to the matching + # Solution: Create a node in between both nodes, change + # prefixes and add the new node for the remaining word + else: + incoming_node.prefix = remaining_prefix + + aux_node = self.nodes[matching_string[0]] + self.nodes[matching_string[0]] = RadixNode(matching_string, False) + self.nodes[matching_string[0]].nodes[remaining_prefix[0]] = aux_node + + if remaining_word == "": + self.nodes[matching_string[0]].is_leaf = True + else: + self.nodes[matching_string[0]].insert(remaining_word) + + def find(self, word: str) -> bool: + """Returns whether the word is on the tree + + Args: + word (str): word to check + + Returns: + bool: True if the word appears on the tree + + >>> RadixNode("myprefix").find("mystring") + False + """ + incoming_node = self.nodes.get(word[0], None) + if not incoming_node: + return False + else: + _matching_string, remaining_prefix, remaining_word = incoming_node.match( + word + ) + # If there is remaining prefix, the word can't be on the tree + if remaining_prefix != "": + return False + # This applies when the word and the prefix are equal + elif remaining_word == "": + return incoming_node.is_leaf + # We have word remaining so we check the next node + else: + return incoming_node.find(remaining_word) + + def delete(self, word: str) -> bool: + """Deletes a word from the tree if it exists + + Args: + word (str): word to be deleted + + Returns: + bool: True if the word was found and deleted. False if word is not found + + >>> RadixNode("myprefix").delete("mystring") + False + """ + incoming_node = self.nodes.get(word[0], None) + if not incoming_node: + return False + else: + _matching_string, remaining_prefix, remaining_word = incoming_node.match( + word + ) + # If there is remaining prefix, the word can't be on the tree + if remaining_prefix != "": + return False + # We have word remaining so we check the next node + elif remaining_word != "": + return incoming_node.delete(remaining_word) + # If it is not a leaf, we don't have to delete + elif not incoming_node.is_leaf: + return False + else: + # We delete the nodes if no edges go from it + if len(incoming_node.nodes) == 0: + del self.nodes[word[0]] + # We merge the current node with its only child + if len(self.nodes) == 1 and not self.is_leaf: + merging_node = next(iter(self.nodes.values())) + self.is_leaf = merging_node.is_leaf + self.prefix += merging_node.prefix + self.nodes = merging_node.nodes + # If there is more than 1 edge, we just mark it as non-leaf + elif len(incoming_node.nodes) > 1: + incoming_node.is_leaf = False + # If there is 1 edge, we merge it with its child + else: + merging_node = next(iter(incoming_node.nodes.values())) + incoming_node.is_leaf = merging_node.is_leaf + incoming_node.prefix += merging_node.prefix + incoming_node.nodes = merging_node.nodes + + return True + + def print_tree(self, height: int = 0) -> None: + """Print the tree + + Args: + height (int, optional): Height of the printed node + """ + if self.prefix != "": + print("-" * height, self.prefix, " (leaf)" if self.is_leaf else "") + + for value in self.nodes.values(): + value.print_tree(height + 1) + + +def test_trie() -> None: + words = "banana bananas bandana band apple all beast".split() + root = RadixNode() + root.insert_many(words) + + assert all(root.find(word) for word in words) + assert not root.find("bandanas") + assert not root.find("apps") + root.delete("all") + assert not root.find("all") + root.delete("banana") + assert not root.find("banana") + assert root.find("bananas") + + +class TestRadixNode(unittest.TestCase): + def test_trie(self) -> None: + words = "banana bananas bandana band apple all beast".split() + root = RadixNode() + root.insert_many(words) + + assert all(root.find(word) for word in words) + assert not root.find("bandanas") + assert not root.find("apps") + root.delete("all") + assert not root.find("all") + root.delete("banana") + assert not root.find("banana") + assert root.find("bananas") + + def test_trie_2(self) -> None: + """ + Now add a new test case that inserts + foobbb, fooaaa, foo in the given order and checks + for different assertions + """ + words = "foobbb fooaaa foo".split() + root = RadixNode() + root.insert_many(words) + + assert all(root.find(word) for word in words) + root.delete("foo") + assert not root.find("foo") + assert root.find("foobbb") + assert root.find("fooaaa") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + unittest.main() diff --git a/data_structures/trie/trie.py b/data_structures/trie/trie.py new file mode 100644 index 000000000000..46b93a499d14 --- /dev/null +++ b/data_structures/trie/trie.py @@ -0,0 +1,127 @@ +""" +A Trie/Prefix Tree is a kind of search tree used to provide quick lookup +of words/patterns in a set of words. A basic Trie however has O(n^2) space complexity +making it impractical in practice. It however provides O(max(search_string, length of +longest word)) lookup time making it an optimal approach when space is not an issue. +""" + + +class TrieNode: + def __init__(self) -> None: + self.nodes: dict[str, TrieNode] = {} # Mapping from char to TrieNode + self.is_leaf = False + + def insert_many(self, words: list[str]) -> None: + """ + Inserts a list of words into the Trie + :param words: list of string words + :return: None + """ + for word in words: + self.insert(word) + + def insert(self, word: str) -> None: + """ + Inserts a word into the Trie + :param word: word to be inserted + :return: None + """ + curr = self + for char in word: + if char not in curr.nodes: + curr.nodes[char] = TrieNode() + curr = curr.nodes[char] + curr.is_leaf = True + + def find(self, word: str) -> bool: + """ + Tries to find word in a Trie + :param word: word to look for + :return: Returns True if word is found, False otherwise + """ + curr = self + for char in word: + if char not in curr.nodes: + return False + curr = curr.nodes[char] + return curr.is_leaf + + def delete(self, word: str) -> None: + """ + Deletes a word in a Trie + :param word: word to delete + :return: None + """ + + def _delete(curr: TrieNode, word: str, index: int) -> bool: + if index == len(word): + # If word does not exist + if not curr.is_leaf: + return False + curr.is_leaf = False + return len(curr.nodes) == 0 + char = word[index] + char_node = curr.nodes.get(char) + # If char not in current trie node + if not char_node: + return False + # Flag to check if node can be deleted + delete_curr = _delete(char_node, word, index + 1) + if delete_curr: + del curr.nodes[char] + return len(curr.nodes) == 0 + return delete_curr + + _delete(self, word, 0) + + +def print_words(node: TrieNode, word: str) -> None: + """ + Prints all the words in a Trie + :param node: root node of Trie + :param word: Word variable should be empty at start + :return: None + """ + if node.is_leaf: + print(word, end=" ") + + for key, value in node.nodes.items(): + print_words(value, word + key) + + +def test_trie() -> bool: + words = "banana bananas bandana band apple all beast".split() + root = TrieNode() + root.insert_many(words) + # print_words(root, "") + assert all(root.find(word) for word in words) + assert root.find("banana") + assert not root.find("bandanas") + assert not root.find("apps") + assert root.find("apple") + assert root.find("all") + root.delete("all") + assert not root.find("all") + root.delete("banana") + assert not root.find("banana") + assert root.find("bananas") + return True + + +def print_results(msg: str, passes: bool) -> None: + print(str(msg), "works!" if passes else "doesn't work :(") + + +def pytests() -> None: + assert test_trie() + + +def main() -> None: + """ + >>> pytests() + """ + print_results("Testing trie functionality", test_trie()) + + +if __name__ == "__main__": + main() diff --git a/digital_image_processing/__init__.py b/digital_image_processing/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/digital_image_processing/change_brightness.py b/digital_image_processing/change_brightness.py new file mode 100644 index 000000000000..b6ebcc9371c6 --- /dev/null +++ b/digital_image_processing/change_brightness.py @@ -0,0 +1,53 @@ +from PIL import Image + + +def change_brightness(img: Image.Image, level: float) -> Image.Image: + """ + Change the brightness of a PIL Image to a given level. + + Args: + img: PIL Image to adjust + level: Brightness level adjustment (-255.0 to 255.0) + Negative values darken, positive values brighten + + Returns: + New PIL Image with adjusted brightness + + Raises: + ValueError: If level is not in range [-255.0, 255.0] + + >>> from PIL import Image + >>> import numpy as np + >>> img = Image.new('RGB', (2, 2), color=(100, 100, 100)) + >>> bright = change_brightness(img, 50) + >>> px = bright.load() + >>> px[0, 0] + (150, 150, 150) + >>> dark = change_brightness(img, -50) + >>> px_dark = dark.load() + >>> px_dark[0, 0] + (50, 50, 50) + >>> change_brightness(img, 300) + Traceback (most recent call last): + ... + ValueError: level must be between -255.0 (black) and 255.0 (white) + """ + + def brightness(c: int) -> float: + """ + Fundamental Transformation/Operation that'll be performed on + every bit. + """ + return 128 + level + (c - 128) + + if not -255.0 <= level <= 255.0: + raise ValueError("level must be between -255.0 (black) and 255.0 (white)") + return img.point(brightness) + + +if __name__ == "__main__": + # Load image + with Image.open("image_data/lena.jpg") as img: + # Change brightness to 100 + brigt_img = change_brightness(img, 100) + brigt_img.save("image_data/lena_brightness.png", format="png") diff --git a/digital_image_processing/change_contrast.py b/digital_image_processing/change_contrast.py new file mode 100644 index 000000000000..80ed53de6308 --- /dev/null +++ b/digital_image_processing/change_contrast.py @@ -0,0 +1,35 @@ +""" +Changing contrast with PIL + +This algorithm is used in +https://noivce.pythonanywhere.com/ Python web app. + +psf/black: True +ruff : True +""" + +from PIL import Image + + +def change_contrast(img: Image.Image, level: int) -> Image.Image: + """ + Function to change contrast + """ + factor = (259 * (level + 255)) / (255 * (259 - level)) + + def contrast(c: int) -> int: + """ + Fundamental Transformation/Operation that'll be performed on + every bit. + """ + return int(128 + factor * (c - 128)) + + return img.point(contrast) + + +if __name__ == "__main__": + # Load image + with Image.open("image_data/lena.jpg") as img: + # Change contrast to 170 + cont_img = change_contrast(img, 170) + cont_img.save("image_data/lena_high_contrast.png", format="png") diff --git a/digital_image_processing/convert_to_negative.py b/digital_image_processing/convert_to_negative.py new file mode 100644 index 000000000000..9bf2d8f2c075 --- /dev/null +++ b/digital_image_processing/convert_to_negative.py @@ -0,0 +1,30 @@ +""" +Implemented an algorithm using opencv to convert a colored image into its negative +""" + +from cv2 import destroyAllWindows, imread, imshow, waitKey + + +def convert_to_negative(img): + # getting number of pixels in the image + pixel_h, pixel_v = img.shape[0], img.shape[1] + + # converting each pixel's color to its negative + for i in range(pixel_h): + for j in range(pixel_v): + img[i][j] = [255, 255, 255] - img[i][j] + + return img + + +if __name__ == "__main__": + # read original image + img = imread("image_data/lena.jpg", 1) + + # convert to its negative + neg = convert_to_negative(img) + + # show result image + imshow("negative of original image", img) + waitKey(0) + destroyAllWindows() diff --git a/digital_image_processing/dithering/__init__.py b/digital_image_processing/dithering/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/digital_image_processing/dithering/burkes.py b/digital_image_processing/dithering/burkes.py new file mode 100644 index 000000000000..d9aa11849cc1 --- /dev/null +++ b/digital_image_processing/dithering/burkes.py @@ -0,0 +1,98 @@ +""" +Implementation Burke's algorithm (dithering) +""" + +import numpy as np +from cv2 import destroyAllWindows, imread, imshow, waitKey + + +class Burkes: + """ + Burke's algorithm is using for converting grayscale image to black and white version + Source: Source: https://en.wikipedia.org/wiki/Dither + + Note: + * Best results are given with threshold= ~1/2 * max greyscale value. + * This implementation get RGB image and converts it to greyscale in runtime. + """ + + def __init__(self, input_img, threshold: int) -> None: + self.min_threshold = 0 + # max greyscale value for #FFFFFF + self.max_threshold = int(self.get_greyscale(255, 255, 255)) + + if not self.min_threshold < threshold < self.max_threshold: + msg = f"Factor value should be from 0 to {self.max_threshold}" + raise ValueError(msg) + + self.input_img = input_img + self.threshold = threshold + self.width, self.height = self.input_img.shape[1], self.input_img.shape[0] + + # error table size (+4 columns and +1 row) greater than input image because of + # lack of if statements + self.error_table = [ + [0 for _ in range(self.height + 4)] for __ in range(self.width + 1) + ] + self.output_img = np.ones((self.width, self.height, 3), np.uint8) * 255 + + @classmethod + def get_greyscale(cls, blue: int, green: int, red: int) -> float: + """ + >>> Burkes.get_greyscale(3, 4, 5) + 4.185 + >>> Burkes.get_greyscale(0, 0, 0) + 0.0 + >>> Burkes.get_greyscale(255, 255, 255) + 255.0 + """ + """ + Formula from https://en.wikipedia.org/wiki/HSL_and_HSV + cf Lightness section, and Fig 13c. + We use the first of four possible. + """ + return 0.114 * blue + 0.587 * green + 0.299 * red + + def process(self) -> None: + for y in range(self.height): + for x in range(self.width): + greyscale = int(self.get_greyscale(*self.input_img[y][x])) + if self.threshold > greyscale + self.error_table[y][x]: + self.output_img[y][x] = (0, 0, 0) + current_error = greyscale + self.error_table[y][x] + else: + self.output_img[y][x] = (255, 255, 255) + current_error = greyscale + self.error_table[y][x] - 255 + """ + Burkes error propagation (`*` is current pixel): + + * 8/32 4/32 + 2/32 4/32 8/32 4/32 2/32 + """ + self.error_table[y][x + 1] += int(8 / 32 * current_error) + self.error_table[y][x + 2] += int(4 / 32 * current_error) + self.error_table[y + 1][x] += int(8 / 32 * current_error) + self.error_table[y + 1][x + 1] += int(4 / 32 * current_error) + self.error_table[y + 1][x + 2] += int(2 / 32 * current_error) + self.error_table[y + 1][x - 1] += int(4 / 32 * current_error) + self.error_table[y + 1][x - 2] += int(2 / 32 * current_error) + + +if __name__ == "__main__": + # create Burke's instances with original images in greyscale + burkes_instances = [ + Burkes(imread("image_data/lena.jpg", 1), threshold) + for threshold in (1, 126, 130, 140) + ] + + for burkes in burkes_instances: + burkes.process() + + for burkes in burkes_instances: + imshow( + f"Original image with dithering threshold: {burkes.threshold}", + burkes.output_img, + ) + + waitKey(0) + destroyAllWindows() diff --git a/digital_image_processing/edge_detection/__init__.py b/digital_image_processing/edge_detection/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/digital_image_processing/edge_detection/canny.py b/digital_image_processing/edge_detection/canny.py new file mode 100644 index 000000000000..450e061f712d --- /dev/null +++ b/digital_image_processing/edge_detection/canny.py @@ -0,0 +1,143 @@ +import cv2 +import numpy as np + +from digital_image_processing.filters.convolve import img_convolve +from digital_image_processing.filters.sobel_filter import sobel_filter + +PI = 180 + + +def gen_gaussian_kernel(k_size, sigma): + center = k_size // 2 + x, y = np.mgrid[0 - center : k_size - center, 0 - center : k_size - center] + g = ( + 1 + / (2 * np.pi * sigma) + * np.exp(-(np.square(x) + np.square(y)) / (2 * np.square(sigma))) + ) + return g + + +def suppress_non_maximum(image_shape, gradient_direction, sobel_grad): + """ + Non-maximum suppression. If the edge strength of the current pixel is the largest + compared to the other pixels in the mask with the same direction, the value will be + preserved. Otherwise, the value will be suppressed. + """ + destination = np.zeros(image_shape) + + for row in range(1, image_shape[0] - 1): + for col in range(1, image_shape[1] - 1): + direction = gradient_direction[row, col] + + if ( + 0 <= direction < PI / 8 + or 15 * PI / 8 <= direction <= 2 * PI + or 7 * PI / 8 <= direction <= 9 * PI / 8 + ): + w = sobel_grad[row, col - 1] + e = sobel_grad[row, col + 1] + if sobel_grad[row, col] >= w and sobel_grad[row, col] >= e: + destination[row, col] = sobel_grad[row, col] + + elif ( + PI / 8 <= direction < 3 * PI / 8 + or 9 * PI / 8 <= direction < 11 * PI / 8 + ): + sw = sobel_grad[row + 1, col - 1] + ne = sobel_grad[row - 1, col + 1] + if sobel_grad[row, col] >= sw and sobel_grad[row, col] >= ne: + destination[row, col] = sobel_grad[row, col] + + elif ( + 3 * PI / 8 <= direction < 5 * PI / 8 + or 11 * PI / 8 <= direction < 13 * PI / 8 + ): + n = sobel_grad[row - 1, col] + s = sobel_grad[row + 1, col] + if sobel_grad[row, col] >= n and sobel_grad[row, col] >= s: + destination[row, col] = sobel_grad[row, col] + + elif ( + 5 * PI / 8 <= direction < 7 * PI / 8 + or 13 * PI / 8 <= direction < 15 * PI / 8 + ): + nw = sobel_grad[row - 1, col - 1] + se = sobel_grad[row + 1, col + 1] + if sobel_grad[row, col] >= nw and sobel_grad[row, col] >= se: + destination[row, col] = sobel_grad[row, col] + + return destination + + +def detect_high_low_threshold( + image_shape, destination, threshold_low, threshold_high, weak, strong +) -> None: + """ + High-Low threshold detection. If an edge pixel's gradient value is higher + than the high threshold value, it is marked as a strong edge pixel. If an + edge pixel's gradient value is smaller than the high threshold value and + larger than the low threshold value, it is marked as a weak edge pixel. If + an edge pixel's value is smaller than the low threshold value, it will be + suppressed. + """ + for row in range(1, image_shape[0] - 1): + for col in range(1, image_shape[1] - 1): + if destination[row, col] >= threshold_high: + destination[row, col] = strong + elif destination[row, col] <= threshold_low: + destination[row, col] = 0 + else: + destination[row, col] = weak + + +def track_edge(image_shape, destination, weak, strong) -> None: + """ + Edge tracking. Usually a weak edge pixel caused from true edges will be connected + to a strong edge pixel while noise responses are unconnected. As long as there is + one strong edge pixel that is involved in its 8-connected neighborhood, that weak + edge point can be identified as one that should be preserved. + """ + for row in range(1, image_shape[0]): + for col in range(1, image_shape[1]): + if destination[row, col] == weak: + if 255 in ( + destination[row, col + 1], + destination[row, col - 1], + destination[row - 1, col], + destination[row + 1, col], + destination[row - 1, col - 1], + destination[row + 1, col - 1], + destination[row - 1, col + 1], + destination[row + 1, col + 1], + ): + destination[row, col] = strong + else: + destination[row, col] = 0 + + +def canny(image, threshold_low=15, threshold_high=30, weak=128, strong=255): + # gaussian_filter + gaussian_out = img_convolve(image, gen_gaussian_kernel(9, sigma=1.4)) + # get the gradient and degree by sobel_filter + sobel_grad, sobel_theta = sobel_filter(gaussian_out) + gradient_direction = PI + np.rad2deg(sobel_theta) + + destination = suppress_non_maximum(image.shape, gradient_direction, sobel_grad) + + detect_high_low_threshold( + image.shape, destination, threshold_low, threshold_high, weak, strong + ) + + track_edge(image.shape, destination, weak, strong) + + return destination + + +if __name__ == "__main__": + # read original image in gray mode + lena = cv2.imread(r"../image_data/lena.jpg", 0) + # canny edge detection + canny_destination = canny(lena) + cv2.imshow("canny", canny_destination) + cv2.waitKey(0) diff --git a/digital_image_processing/filters/__init__.py b/digital_image_processing/filters/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/digital_image_processing/filters/bilateral_filter.py b/digital_image_processing/filters/bilateral_filter.py new file mode 100644 index 000000000000..6ef4434d959c --- /dev/null +++ b/digital_image_processing/filters/bilateral_filter.py @@ -0,0 +1,88 @@ +""" +Implementation of Bilateral filter + +Inputs: + img: A 2d image with values in between 0 and 1 + varS: variance in space dimension. + varI: variance in Intensity. + N: Kernel size(Must be an odd number) +Output: + img:A 2d zero padded image with values in between 0 and 1 +""" + +import math +import sys + +import cv2 +import numpy as np + + +def vec_gaussian(img: np.ndarray, variance: float) -> np.ndarray: + # For applying gaussian function for each element in matrix. + sigma = math.sqrt(variance) + cons = 1 / (sigma * math.sqrt(2 * math.pi)) + return cons * np.exp(-((img / sigma) ** 2) * 0.5) + + +def get_slice(img: np.ndarray, x: int, y: int, kernel_size: int) -> np.ndarray: + half = kernel_size // 2 + return img[x - half : x + half + 1, y - half : y + half + 1] + + +def get_gauss_kernel(kernel_size: int, spatial_variance: float) -> np.ndarray: + # Creates a gaussian kernel of given dimension. + arr = np.zeros((kernel_size, kernel_size)) + for i in range(kernel_size): + for j in range(kernel_size): + arr[i, j] = math.sqrt( + abs(i - kernel_size // 2) ** 2 + abs(j - kernel_size // 2) ** 2 + ) + return vec_gaussian(arr, spatial_variance) + + +def bilateral_filter( + img: np.ndarray, + spatial_variance: float, + intensity_variance: float, + kernel_size: int, +) -> np.ndarray: + img2 = np.zeros(img.shape) + gauss_ker = get_gauss_kernel(kernel_size, spatial_variance) + size_x, size_y = img.shape + for i in range(kernel_size // 2, size_x - kernel_size // 2): + for j in range(kernel_size // 2, size_y - kernel_size // 2): + img_s = get_slice(img, i, j, kernel_size) + img_i = img_s - img_s[kernel_size // 2, kernel_size // 2] + img_ig = vec_gaussian(img_i, intensity_variance) + weights = np.multiply(gauss_ker, img_ig) + vals = np.multiply(img_s, weights) + val = np.sum(vals) / np.sum(weights) + img2[i, j] = val + return img2 + + +def parse_args(args: list) -> tuple: + filename = args[1] if args[1:] else "../image_data/lena.jpg" + spatial_variance = float(args[2]) if args[2:] else 1.0 + intensity_variance = float(args[3]) if args[3:] else 1.0 + if args[4:]: + kernel_size = int(args[4]) + kernel_size = kernel_size + abs(kernel_size % 2 - 1) + else: + kernel_size = 5 + return filename, spatial_variance, intensity_variance, kernel_size + + +if __name__ == "__main__": + filename, spatial_variance, intensity_variance, kernel_size = parse_args(sys.argv) + img = cv2.imread(filename, 0) + cv2.imshow("input image", img) + + out = img / 255 + out = out.astype("float32") + out = bilateral_filter(out, spatial_variance, intensity_variance, kernel_size) + out = out * 255 + out = np.uint8(out) + cv2.imshow("output image", out) + cv2.waitKey(0) + cv2.destroyAllWindows() diff --git a/digital_image_processing/filters/convolve.py b/digital_image_processing/filters/convolve.py new file mode 100644 index 000000000000..004402f29ba9 --- /dev/null +++ b/digital_image_processing/filters/convolve.py @@ -0,0 +1,49 @@ +# @Author : lightXu +# @File : convolve.py +# @Time : 2019/7/8 0008 下� 16:13 +from cv2 import COLOR_BGR2GRAY, cvtColor, imread, imshow, waitKey +from numpy import array, dot, pad, ravel, uint8, zeros + + +def im2col(image, block_size): + rows, cols = image.shape + dst_height = cols - block_size[1] + 1 + dst_width = rows - block_size[0] + 1 + image_array = zeros((dst_height * dst_width, block_size[1] * block_size[0])) + row = 0 + for i in range(dst_height): + for j in range(dst_width): + window = ravel(image[i : i + block_size[0], j : j + block_size[1]]) + image_array[row, :] = window + row += 1 + + return image_array + + +def img_convolve(image, filter_kernel): + height, width = image.shape[0], image.shape[1] + k_size = filter_kernel.shape[0] + pad_size = k_size // 2 + # Pads image with the edge values of array. + image_tmp = pad(image, pad_size, mode="edge") + + # im2col, turn the k_size*k_size pixels into a row and np.vstack all rows + image_array = im2col(image_tmp, (k_size, k_size)) + + # turn the kernel into shape(k*k, 1) + kernel_array = ravel(filter_kernel) + # reshape and get the dst image + dst = dot(image_array, kernel_array).reshape(height, width) + return dst + + +if __name__ == "__main__": + # read original image + img = imread(r"../image_data/lena.jpg") + # turn image in gray scale value + gray = cvtColor(img, COLOR_BGR2GRAY) + # Laplace operator + Laplace_kernel = array([[0, 1, 0], [1, -4, 1], [0, 1, 0]]) + out = img_convolve(gray, Laplace_kernel).astype(uint8) + imshow("Laplacian", out) + waitKey(0) diff --git a/digital_image_processing/filters/gabor_filter.py b/digital_image_processing/filters/gabor_filter.py new file mode 100644 index 000000000000..aaec567f4c99 --- /dev/null +++ b/digital_image_processing/filters/gabor_filter.py @@ -0,0 +1,85 @@ +# Implementation of the Gaborfilter +# https://en.wikipedia.org/wiki/Gabor_filter +import numpy as np +from cv2 import COLOR_BGR2GRAY, CV_8UC3, cvtColor, filter2D, imread, imshow, waitKey + + +def gabor_filter_kernel( + ksize: int, sigma: int, theta: int, lambd: int, gamma: int, psi: int +) -> np.ndarray: + """ + :param ksize: The kernelsize of the convolutional filter (ksize x ksize) + :param sigma: standard deviation of the gaussian bell curve + :param theta: The orientation of the normal to the parallel stripes + of Gabor function. + :param lambd: Wavelength of the sinusoidal component. + :param gamma: The spatial aspect ratio and specifies the ellipticity + of the support of Gabor function. + :param psi: The phase offset of the sinusoidal function. + + >>> gabor_filter_kernel(3, 8, 0, 10, 0, 0).tolist() + [[0.8027212023735046, 1.0, 0.8027212023735046], [0.8027212023735046, 1.0, \ +0.8027212023735046], [0.8027212023735046, 1.0, 0.8027212023735046]] + + """ + + # prepare kernel + # the kernel size have to be odd + if (ksize % 2) == 0: + ksize = ksize + 1 + gabor = np.zeros((ksize, ksize), dtype=np.float32) + + # each value + for y in range(ksize): + for x in range(ksize): + # distance from center + px = x - ksize // 2 + py = y - ksize // 2 + + # degree to radiant + _theta = theta / 180 * np.pi + cos_theta = np.cos(_theta) + sin_theta = np.sin(_theta) + + # get kernel x + _x = cos_theta * px + sin_theta * py + + # get kernel y + _y = -sin_theta * px + cos_theta * py + + # fill kernel + gabor[y, x] = np.exp(-(_x**2 + gamma**2 * _y**2) / (2 * sigma**2)) * np.cos( + 2 * np.pi * _x / lambd + psi + ) + + return gabor + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + # read original image + img = imread("../image_data/lena.jpg") + # turn image in gray scale value + gray = cvtColor(img, COLOR_BGR2GRAY) + + # Apply multiple Kernel to detect edges + out = np.zeros(gray.shape[:2]) + for theta in [0, 30, 60, 90, 120, 150]: + """ + ksize = 10 + sigma = 8 + lambd = 10 + gamma = 0 + psi = 0 + """ + kernel_10 = gabor_filter_kernel(10, 8, theta, 10, 0, 0) + out += filter2D(gray, CV_8UC3, kernel_10) + out = out / out.max() * 255 + out = out.astype(np.uint8) + + imshow("Original", gray) + imshow("Gabor filter with 20x20 mask and 6 directions", out) + + waitKey(0) diff --git a/digital_image_processing/filters/gaussian_filter.py b/digital_image_processing/filters/gaussian_filter.py new file mode 100644 index 000000000000..0c34e59fafe5 --- /dev/null +++ b/digital_image_processing/filters/gaussian_filter.py @@ -0,0 +1,53 @@ +""" +Implementation of gaussian filter algorithm +""" + +from itertools import product + +from cv2 import COLOR_BGR2GRAY, cvtColor, imread, imshow, waitKey +from numpy import dot, exp, mgrid, pi, ravel, square, uint8, zeros + + +def gen_gaussian_kernel(k_size, sigma): + center = k_size // 2 + x, y = mgrid[0 - center : k_size - center, 0 - center : k_size - center] + g = 1 / (2 * pi * sigma) * exp(-(square(x) + square(y)) / (2 * square(sigma))) + return g + + +def gaussian_filter(image, k_size, sigma): + height, width = image.shape[0], image.shape[1] + # dst image height and width + dst_height = height - k_size + 1 + dst_width = width - k_size + 1 + + # im2col, turn the k_size*k_size pixels into a row and np.vstack all rows + image_array = zeros((dst_height * dst_width, k_size * k_size)) + for row, (i, j) in enumerate(product(range(dst_height), range(dst_width))): + window = ravel(image[i : i + k_size, j : j + k_size]) + image_array[row, :] = window + + # turn the kernel into shape(k*k, 1) + gaussian_kernel = gen_gaussian_kernel(k_size, sigma) + filter_array = ravel(gaussian_kernel) + + # reshape and get the dst image + dst = dot(image_array, filter_array).reshape(dst_height, dst_width).astype(uint8) + + return dst + + +if __name__ == "__main__": + # read original image + img = imread(r"../image_data/lena.jpg") + # turn image in gray scale value + gray = cvtColor(img, COLOR_BGR2GRAY) + + # get values with two different mask size + gaussian3x3 = gaussian_filter(gray, 3, sigma=1) + gaussian5x5 = gaussian_filter(gray, 5, sigma=0.8) + + # show result images + imshow("gaussian filter with 3x3 mask", gaussian3x3) + imshow("gaussian filter with 5x5 mask", gaussian5x5) + waitKey() diff --git a/digital_image_processing/filters/laplacian_filter.py b/digital_image_processing/filters/laplacian_filter.py new file mode 100644 index 000000000000..69b9616e4d30 --- /dev/null +++ b/digital_image_processing/filters/laplacian_filter.py @@ -0,0 +1,81 @@ +# @Author : ojas-wani +# @File : laplacian_filter.py +# @Date : 10/04/2023 + +import numpy as np +from cv2 import ( + BORDER_DEFAULT, + COLOR_BGR2GRAY, + CV_64F, + cvtColor, + filter2D, + imread, + imshow, + waitKey, +) + +from digital_image_processing.filters.gaussian_filter import gaussian_filter + + +def my_laplacian(src: np.ndarray, ksize: int) -> np.ndarray: + """ + :param src: the source image, which should be a grayscale or color image. + :param ksize: the size of the kernel used to compute the Laplacian filter, + which can be 1, 3, 5, or 7. + + >>> my_laplacian(src=np.array([]), ksize=0) + Traceback (most recent call last): + ... + ValueError: ksize must be in (1, 3, 5, 7) + """ + kernels = { + 1: np.array([[0, -1, 0], [-1, 4, -1], [0, -1, 0]]), + 3: np.array([[0, 1, 0], [1, -4, 1], [0, 1, 0]]), + 5: np.array( + [ + [0, 0, -1, 0, 0], + [0, -1, -2, -1, 0], + [-1, -2, 16, -2, -1], + [0, -1, -2, -1, 0], + [0, 0, -1, 0, 0], + ] + ), + 7: np.array( + [ + [0, 0, 0, -1, 0, 0, 0], + [0, 0, -2, -3, -2, 0, 0], + [0, -2, -7, -10, -7, -2, 0], + [-1, -3, -10, 68, -10, -3, -1], + [0, -2, -7, -10, -7, -2, 0], + [0, 0, -2, -3, -2, 0, 0], + [0, 0, 0, -1, 0, 0, 0], + ] + ), + } + if ksize not in kernels: + msg = f"ksize must be in {tuple(kernels)}" + raise ValueError(msg) + + # Apply the Laplacian kernel using convolution + return filter2D( + src, CV_64F, kernels[ksize], 0, borderType=BORDER_DEFAULT, anchor=(0, 0) + ) + + +if __name__ == "__main__": + # read original image + img = imread(r"../image_data/lena.jpg") + + # turn image in gray scale value + gray = cvtColor(img, COLOR_BGR2GRAY) + + # Applying gaussian filter + blur_image = gaussian_filter(gray, 3, sigma=1) + + # Apply multiple Kernel to detect edges + laplacian_image = my_laplacian(ksize=3, src=blur_image) + + imshow("Original image", img) + imshow("Detected edges using laplacian filter", laplacian_image) + + waitKey(0) diff --git a/digital_image_processing/filters/local_binary_pattern.py b/digital_image_processing/filters/local_binary_pattern.py new file mode 100644 index 000000000000..ac54ecce755c --- /dev/null +++ b/digital_image_processing/filters/local_binary_pattern.py @@ -0,0 +1,80 @@ +import cv2 +import numpy as np + + +def get_neighbors_pixel( + image: np.ndarray, x_coordinate: int, y_coordinate: int, center: int +) -> int: + """ + Comparing local neighborhood pixel value with threshold value of centre pixel. + Exception is required when neighborhood value of a center pixel value is null. + i.e. values present at boundaries. + + :param image: The image we're working with + :param x_coordinate: x-coordinate of the pixel + :param y_coordinate: The y coordinate of the pixel + :param center: center pixel value + :return: The value of the pixel is being returned. + """ + + try: + return int(image[x_coordinate][y_coordinate] >= center) + except IndexError, TypeError: + return 0 + + +def local_binary_value(image: np.ndarray, x_coordinate: int, y_coordinate: int) -> int: + """ + It takes an image, an x and y coordinate, and returns the + decimal value of the local binary patternof the pixel + at that coordinate + + :param image: the image to be processed + :param x_coordinate: x coordinate of the pixel + :param y_coordinate: the y coordinate of the pixel + :return: The decimal value of the binary value of the pixels + around the center pixel. + """ + center = image[x_coordinate][y_coordinate] + powers = [1, 2, 4, 8, 16, 32, 64, 128] + + # skip get_neighbors_pixel if center is null + if center is None: + return 0 + + # Starting from the top right, assigning value to pixels clockwise + binary_values = [ + get_neighbors_pixel(image, x_coordinate - 1, y_coordinate + 1, center), + get_neighbors_pixel(image, x_coordinate, y_coordinate + 1, center), + get_neighbors_pixel(image, x_coordinate - 1, y_coordinate, center), + get_neighbors_pixel(image, x_coordinate + 1, y_coordinate + 1, center), + get_neighbors_pixel(image, x_coordinate + 1, y_coordinate, center), + get_neighbors_pixel(image, x_coordinate + 1, y_coordinate - 1, center), + get_neighbors_pixel(image, x_coordinate, y_coordinate - 1, center), + get_neighbors_pixel(image, x_coordinate - 1, y_coordinate - 1, center), + ] + + # Converting the binary value to decimal. + return sum( + binary_value * power for binary_value, power in zip(binary_values, powers) + ) + + +if __name__ == "__main__": + # Reading the image and converting it to grayscale. + image = cv2.imread( + "digital_image_processing/image_data/lena.jpg", cv2.IMREAD_GRAYSCALE + ) + + # Create a numpy array as the same height and width of read image + lbp_image = np.zeros((image.shape[0], image.shape[1])) + + # Iterating through the image and calculating the + # local binary pattern value for each pixel. + for i in range(image.shape[0]): + for j in range(image.shape[1]): + lbp_image[i][j] = local_binary_value(image, i, j) + + cv2.imshow("local binary pattern", lbp_image) + cv2.waitKey(0) + cv2.destroyAllWindows() diff --git a/digital_image_processing/filters/median_filter.py b/digital_image_processing/filters/median_filter.py new file mode 100644 index 000000000000..fc8b582ef67a --- /dev/null +++ b/digital_image_processing/filters/median_filter.py @@ -0,0 +1,42 @@ +""" +Implementation of median filter algorithm +""" + +from cv2 import COLOR_BGR2GRAY, cvtColor, imread, imshow, waitKey +from numpy import divide, int8, multiply, ravel, sort, zeros_like + + +def median_filter(gray_img, mask=3): + """ + :param gray_img: gray image + :param mask: mask size + :return: image with median filter + """ + # set image borders + bd = int(mask / 2) + # copy image size + median_img = zeros_like(gray_img) + for i in range(bd, gray_img.shape[0] - bd): + for j in range(bd, gray_img.shape[1] - bd): + # get mask according with mask + kernel = ravel(gray_img[i - bd : i + bd + 1, j - bd : j + bd + 1]) + # calculate mask median + median = sort(kernel)[int8(divide((multiply(mask, mask)), 2) + 1)] + median_img[i, j] = median + return median_img + + +if __name__ == "__main__": + # read original image + img = imread("../image_data/lena.jpg") + # turn image in gray scale value + gray = cvtColor(img, COLOR_BGR2GRAY) + + # get values with two different mask size + median3x3 = median_filter(gray, 3) + median5x5 = median_filter(gray, 5) + + # show result images + imshow("median filter with 3x3 mask", median3x3) + imshow("median filter with 5x5 mask", median5x5) + waitKey(0) diff --git a/digital_image_processing/filters/sobel_filter.py b/digital_image_processing/filters/sobel_filter.py new file mode 100644 index 000000000000..33284a32f424 --- /dev/null +++ b/digital_image_processing/filters/sobel_filter.py @@ -0,0 +1,39 @@ +# @Author : lightXu +# @File : sobel_filter.py +# @Time : 2019/7/8 0008 下� 16:26 +import numpy as np +from cv2 import COLOR_BGR2GRAY, cvtColor, imread, imshow, waitKey + +from digital_image_processing.filters.convolve import img_convolve + + +def sobel_filter(image): + kernel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]) + kernel_y = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]]) + + dst_x = np.abs(img_convolve(image, kernel_x)) + dst_y = np.abs(img_convolve(image, kernel_y)) + # modify the pix within [0, 255] + dst_x = dst_x * 255 / np.max(dst_x) + dst_y = dst_y * 255 / np.max(dst_y) + + dst_xy = np.sqrt((np.square(dst_x)) + (np.square(dst_y))) + dst_xy = dst_xy * 255 / np.max(dst_xy) + dst = dst_xy.astype(np.uint8) + + theta = np.arctan2(dst_y, dst_x) + return dst, theta + + +if __name__ == "__main__": + # read original image + img = imread("../image_data/lena.jpg") + # turn image in gray scale value + gray = cvtColor(img, COLOR_BGR2GRAY) + + sobel_grad, sobel_theta = sobel_filter(gray) + + # show result images + imshow("sobel filter", sobel_grad) + imshow("sobel theta", sobel_theta) + waitKey(0) diff --git a/digital_image_processing/histogram_equalization/__init__.py b/digital_image_processing/histogram_equalization/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/digital_image_processing/histogram_equalization/histogram_stretch.py b/digital_image_processing/histogram_equalization/histogram_stretch.py new file mode 100644 index 000000000000..f5bab685b746 --- /dev/null +++ b/digital_image_processing/histogram_equalization/histogram_stretch.py @@ -0,0 +1,64 @@ +""" +Created on Fri Sep 28 15:22:29 2018 + +@author: Binish125 +""" + +import copy +import os + +import cv2 +import numpy as np +from matplotlib import pyplot as plt + + +class ConstantStretch: + def __init__(self) -> None: + self.img = "" + self.original_image = "" + self.last_list = [] + self.rem = 0 + self.L = 256 + self.sk = 0 + self.k = 0 + self.number_of_rows = 0 + self.number_of_cols = 0 + + def stretch(self, input_image) -> None: + self.img = cv2.imread(input_image, 0) + self.original_image = copy.deepcopy(self.img) + x, _, _ = plt.hist(self.img.ravel(), 256, [0, 256], label="x") + self.k = np.sum(x) + for i in range(len(x)): + prk = x[i] / self.k + self.sk += prk + last = (self.L - 1) * self.sk + if self.rem != 0: + self.rem = int(last % last) + last = int(last + 1 if self.rem >= 0.5 else last) + self.last_list.append(last) + self.number_of_rows = int(np.ma.count(self.img) / self.img[1].size) + self.number_of_cols = self.img[1].size + for i in range(self.number_of_cols): + for j in range(self.number_of_rows): + num = self.img[j][i] + if num != self.last_list[num]: + self.img[j][i] = self.last_list[num] + cv2.imwrite("output_data/output.jpg", self.img) + + def plot_histogram(self) -> None: + plt.hist(self.img.ravel(), 256, [0, 256]) + + def show_image(self) -> None: + cv2.imshow("Output-Image", self.img) + cv2.imshow("Input-Image", self.original_image) + cv2.waitKey(5000) + cv2.destroyAllWindows() + + +if __name__ == "__main__": + file_path = os.path.join(os.path.basename(__file__), "image_data/input.jpg") + stretcher = ConstantStretch() + stretcher.stretch(file_path) + stretcher.plot_histogram() + stretcher.show_image() diff --git a/digital_image_processing/histogram_equalization/image_data/__init__.py b/digital_image_processing/histogram_equalization/image_data/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/digital_image_processing/histogram_equalization/image_data/input.jpg b/digital_image_processing/histogram_equalization/image_data/input.jpg new file mode 100644 index 000000000000..483da6fd97f4 Binary files /dev/null and b/digital_image_processing/histogram_equalization/image_data/input.jpg differ diff --git a/digital_image_processing/histogram_equalization/output_data/__init__.py b/digital_image_processing/histogram_equalization/output_data/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/digital_image_processing/histogram_equalization/output_data/output.jpg b/digital_image_processing/histogram_equalization/output_data/output.jpg new file mode 100644 index 000000000000..48c70d8ae17d Binary files /dev/null and b/digital_image_processing/histogram_equalization/output_data/output.jpg differ diff --git a/digital_image_processing/image_data/__init__.py b/digital_image_processing/image_data/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/digital_image_processing/image_data/lena.jpg b/digital_image_processing/image_data/lena.jpg new file mode 100644 index 000000000000..15c4d9764eff Binary files /dev/null and b/digital_image_processing/image_data/lena.jpg differ diff --git a/digital_image_processing/image_data/lena_small.jpg b/digital_image_processing/image_data/lena_small.jpg new file mode 100644 index 000000000000..b85144e9f65c Binary files /dev/null and b/digital_image_processing/image_data/lena_small.jpg differ diff --git a/digital_image_processing/index_calculation.py b/digital_image_processing/index_calculation.py new file mode 100644 index 000000000000..b09573f4a521 --- /dev/null +++ b/digital_image_processing/index_calculation.py @@ -0,0 +1,579 @@ +# Author: João Gustavo A. Amorim +# Author email: joaogustavoamorim@gmail.com +# Coding date: jan 2019 +# python/black: True + +# Imports +import numpy as np + + +# Class implemented to calculus the index +class IndexCalculation: + """ + # Class Summary + This algorithm consists in calculating vegetation indices, these + indices can be used for precision agriculture for example (or remote + sensing). There are functions to define the data and to calculate the + implemented indices. + + # Vegetation index + https://en.wikipedia.org/wiki/Vegetation_Index + A Vegetation Index (VI) is a spectral transformation of two or more bands + designed to enhance the contribution of vegetation properties and allow + reliable spatial and temporal inter-comparisons of terrestrial + photosynthetic activity and canopy structural variations + + # Information about channels (Wavelength range for each) + * nir - near-infrared + https://www.malvernpanalytical.com/br/products/technology/near-infrared-spectroscopy + Wavelength Range 700 nm to 2500 nm + * Red Edge + https://en.wikipedia.org/wiki/Red_edge + Wavelength Range 680 nm to 730 nm + * red + https://en.wikipedia.org/wiki/Color + Wavelength Range 635 nm to 700 nm + * blue + https://en.wikipedia.org/wiki/Color + Wavelength Range 450 nm to 490 nm + * green + https://en.wikipedia.org/wiki/Color + Wavelength Range 520 nm to 560 nm + + + # Implemented index list + #"abbreviationOfIndexName" -- list of channels used + + #"ARVI2" -- red, nir + #"CCCI" -- red, redEdge, nir + #"CVI" -- red, green, nir + #"GLI" -- red, green, blue + #"NDVI" -- red, nir + #"BNDVI" -- blue, nir + #"redEdgeNDVI" -- red, redEdge + #"GNDVI" -- green, nir + #"GBNDVI" -- green, blue, nir + #"GRNDVI" -- red, green, nir + #"RBNDVI" -- red, blue, nir + #"PNDVI" -- red, green, blue, nir + #"ATSAVI" -- red, nir + #"BWDRVI" -- blue, nir + #"CIgreen" -- green, nir + #"CIrededge" -- redEdge, nir + #"CI" -- red, blue + #"CTVI" -- red, nir + #"GDVI" -- green, nir + #"EVI" -- red, blue, nir + #"GEMI" -- red, nir + #"GOSAVI" -- green, nir + #"GSAVI" -- green, nir + #"Hue" -- red, green, blue + #"IVI" -- red, nir + #"IPVI" -- red, nir + #"I" -- red, green, blue + #"RVI" -- red, nir + #"MRVI" -- red, nir + #"MSAVI" -- red, nir + #"NormG" -- red, green, nir + #"NormNIR" -- red, green, nir + #"NormR" -- red, green, nir + #"NGRDI" -- red, green + #"RI" -- red, green + #"S" -- red, green, blue + #"IF" -- red, green, blue + #"DVI" -- red, nir + #"TVI" -- red, nir + #"NDRE" -- redEdge, nir + + #list of all index implemented + #allIndex = ["ARVI2", "CCCI", "CVI", "GLI", "NDVI", "BNDVI", "redEdgeNDVI", + "GNDVI", "GBNDVI", "GRNDVI", "RBNDVI", "PNDVI", "ATSAVI", + "BWDRVI", "CIgreen", "CIrededge", "CI", "CTVI", "GDVI", "EVI", + "GEMI", "GOSAVI", "GSAVI", "Hue", "IVI", "IPVI", "I", "RVI", + "MRVI", "MSAVI", "NormG", "NormNIR", "NormR", "NGRDI", "RI", + "S", "IF", "DVI", "TVI", "NDRE"] + + #list of index with not blue channel + #notBlueIndex = ["ARVI2", "CCCI", "CVI", "NDVI", "redEdgeNDVI", "GNDVI", + "GRNDVI", "ATSAVI", "CIgreen", "CIrededge", "CTVI", "GDVI", + "GEMI", "GOSAVI", "GSAVI", "IVI", "IPVI", "RVI", "MRVI", + "MSAVI", "NormG", "NormNIR", "NormR", "NGRDI", "RI", "DVI", + "TVI", "NDRE"] + + #list of index just with RGB channels + #RGBIndex = ["GLI", "CI", "Hue", "I", "NGRDI", "RI", "S", "IF"] + """ + + def __init__( + self, red=None, green=None, blue=None, red_edge=None, nir=None + ) -> None: + self.set_matricies(red=red, green=green, blue=blue, red_edge=red_edge, nir=nir) + + def set_matricies( + self, red=None, green=None, blue=None, red_edge=None, nir=None + ) -> bool: + if red is not None: + self.red = red + if green is not None: + self.green = green + if blue is not None: + self.blue = blue + if red_edge is not None: + self.redEdge = red_edge + if nir is not None: + self.nir = nir + return True + + def calculation( + self, index="", red=None, green=None, blue=None, red_edge=None, nir=None + ): + """ + performs the calculation of the index with the values instantiated in the class + :str index: abbreviation of index name to perform + """ + self.set_matricies(red=red, green=green, blue=blue, red_edge=red_edge, nir=nir) + funcs = { + "ARVI2": self.arv12, + "CCCI": self.ccci, + "CVI": self.cvi, + "GLI": self.gli, + "NDVI": self.ndvi, + "BNDVI": self.bndvi, + "redEdgeNDVI": self.red_edge_ndvi, + "GNDVI": self.gndvi, + "GBNDVI": self.gbndvi, + "GRNDVI": self.grndvi, + "RBNDVI": self.rbndvi, + "PNDVI": self.pndvi, + "ATSAVI": self.atsavi, + "BWDRVI": self.bwdrvi, + "CIgreen": self.ci_green, + "CIrededge": self.ci_rededge, + "CI": self.ci, + "CTVI": self.ctvi, + "GDVI": self.gdvi, + "EVI": self.evi, + "GEMI": self.gemi, + "GOSAVI": self.gosavi, + "GSAVI": self.gsavi, + "Hue": self.hue, + "IVI": self.ivi, + "IPVI": self.ipvi, + "I": self.i, + "RVI": self.rvi, + "MRVI": self.mrvi, + "MSAVI": self.m_savi, + "NormG": self.norm_g, + "NormNIR": self.norm_nir, + "NormR": self.norm_r, + "NGRDI": self.ngrdi, + "RI": self.ri, + "S": self.s, + "IF": self._if, + "DVI": self.dvi, + "TVI": self.tvi, + "NDRE": self.ndre, + } + + try: + return funcs[index]() + except KeyError: + print("Index not in the list!") + return False + + def arv12(self): + """ + Atmospherically Resistant Vegetation Index 2 + https://www.indexdatabase.de/db/i-single.php?id=396 + :return: index + -0.18+1.17*(self.nir-self.red)/(self.nir+self.red) + """ + return -0.18 + (1.17 * ((self.nir - self.red) / (self.nir + self.red))) + + def ccci(self): + """ + Canopy Chlorophyll Content Index + https://www.indexdatabase.de/db/i-single.php?id=224 + :return: index + """ + return ((self.nir - self.redEdge) / (self.nir + self.redEdge)) / ( + (self.nir - self.red) / (self.nir + self.red) + ) + + def cvi(self): + """ + Chlorophyll vegetation index + https://www.indexdatabase.de/db/i-single.php?id=391 + :return: index + """ + return self.nir * (self.red / (self.green**2)) + + def gli(self): + """ + self.green leaf index + https://www.indexdatabase.de/db/i-single.php?id=375 + :return: index + """ + return (2 * self.green - self.red - self.blue) / ( + 2 * self.green + self.red + self.blue + ) + + def ndvi(self): + """ + Normalized Difference self.nir/self.red Normalized Difference Vegetation + Index, Calibrated NDVI - CDVI + https://www.indexdatabase.de/db/i-single.php?id=58 + :return: index + """ + return (self.nir - self.red) / (self.nir + self.red) + + def bndvi(self): + """ + Normalized Difference self.nir/self.blue self.blue-normalized difference + vegetation index + https://www.indexdatabase.de/db/i-single.php?id=135 + :return: index + """ + return (self.nir - self.blue) / (self.nir + self.blue) + + def red_edge_ndvi(self): + """ + Normalized Difference self.rededge/self.red + https://www.indexdatabase.de/db/i-single.php?id=235 + :return: index + """ + return (self.redEdge - self.red) / (self.redEdge + self.red) + + def gndvi(self): + """ + Normalized Difference self.nir/self.green self.green NDVI + https://www.indexdatabase.de/db/i-single.php?id=401 + :return: index + """ + return (self.nir - self.green) / (self.nir + self.green) + + def gbndvi(self): + """ + self.green-self.blue NDVI + https://www.indexdatabase.de/db/i-single.php?id=186 + :return: index + """ + return (self.nir - (self.green + self.blue)) / ( + self.nir + (self.green + self.blue) + ) + + def grndvi(self): + """ + self.green-self.red NDVI + https://www.indexdatabase.de/db/i-single.php?id=185 + :return: index + """ + return (self.nir - (self.green + self.red)) / ( + self.nir + (self.green + self.red) + ) + + def rbndvi(self): + """ + self.red-self.blue NDVI + https://www.indexdatabase.de/db/i-single.php?id=187 + :return: index + """ + return (self.nir - (self.blue + self.red)) / (self.nir + (self.blue + self.red)) + + def pndvi(self): + """ + Pan NDVI + https://www.indexdatabase.de/db/i-single.php?id=188 + :return: index + """ + return (self.nir - (self.green + self.red + self.blue)) / ( + self.nir + (self.green + self.red + self.blue) + ) + + def atsavi(self, x=0.08, a=1.22, b=0.03): + """ + Adjusted transformed soil-adjusted VI + https://www.indexdatabase.de/db/i-single.php?id=209 + :return: index + """ + return a * ( + (self.nir - a * self.red - b) + / (a * self.nir + self.red - a * b + x * (1 + a**2)) + ) + + def bwdrvi(self): + """ + self.blue-wide dynamic range vegetation index + https://www.indexdatabase.de/db/i-single.php?id=136 + :return: index + """ + return (0.1 * self.nir - self.blue) / (0.1 * self.nir + self.blue) + + def ci_green(self): + """ + Chlorophyll Index self.green + https://www.indexdatabase.de/db/i-single.php?id=128 + :return: index + """ + return (self.nir / self.green) - 1 + + def ci_rededge(self): + """ + Chlorophyll Index self.redEdge + https://www.indexdatabase.de/db/i-single.php?id=131 + :return: index + """ + return (self.nir / self.redEdge) - 1 + + def ci(self): + """ + Coloration Index + https://www.indexdatabase.de/db/i-single.php?id=11 + :return: index + """ + return (self.red - self.blue) / self.red + + def ctvi(self): + """ + Corrected Transformed Vegetation Index + https://www.indexdatabase.de/db/i-single.php?id=244 + :return: index + """ + ndvi = self.ndvi() + return ((ndvi + 0.5) / (abs(ndvi + 0.5))) * (abs(ndvi + 0.5) ** (1 / 2)) + + def gdvi(self): + """ + Difference self.nir/self.green self.green Difference Vegetation Index + https://www.indexdatabase.de/db/i-single.php?id=27 + :return: index + """ + return self.nir - self.green + + def evi(self): + """ + Enhanced Vegetation Index + https://www.indexdatabase.de/db/i-single.php?id=16 + :return: index + """ + return 2.5 * ( + (self.nir - self.red) / (self.nir + 6 * self.red - 7.5 * self.blue + 1) + ) + + def gemi(self): + """ + Global Environment Monitoring Index + https://www.indexdatabase.de/db/i-single.php?id=25 + :return: index + """ + n = (2 * (self.nir**2 - self.red**2) + 1.5 * self.nir + 0.5 * self.red) / ( + self.nir + self.red + 0.5 + ) + return n * (1 - 0.25 * n) - (self.red - 0.125) / (1 - self.red) + + def gosavi(self, y=0.16): + """ + self.green Optimized Soil Adjusted Vegetation Index + https://www.indexdatabase.de/db/i-single.php?id=29 + mit Y = 0,16 + :return: index + """ + return (self.nir - self.green) / (self.nir + self.green + y) + + def gsavi(self, n=0.5): + """ + self.green Soil Adjusted Vegetation Index + https://www.indexdatabase.de/db/i-single.php?id=31 + mit N = 0,5 + :return: index + """ + return ((self.nir - self.green) / (self.nir + self.green + n)) * (1 + n) + + def hue(self): + """ + Hue + https://www.indexdatabase.de/db/i-single.php?id=34 + :return: index + """ + return np.arctan( + ((2 * self.red - self.green - self.blue) / 30.5) * (self.green - self.blue) + ) + + def ivi(self, a=None, b=None): + """ + Ideal vegetation index + https://www.indexdatabase.de/db/i-single.php?id=276 + b=intercept of vegetation line + a=soil line slope + :return: index + """ + return (self.nir - b) / (a * self.red) + + def ipvi(self): + """ + Infraself.red percentage vegetation index + https://www.indexdatabase.de/db/i-single.php?id=35 + :return: index + """ + return (self.nir / ((self.nir + self.red) / 2)) * (self.ndvi() + 1) + + def i(self): + """ + Intensity + https://www.indexdatabase.de/db/i-single.php?id=36 + :return: index + """ + return (self.red + self.green + self.blue) / 30.5 + + def rvi(self): + """ + Ratio-Vegetation-Index + http://www.seos-project.eu/modules/remotesensing/remotesensing-c03-s01-p01.html + :return: index + """ + return self.nir / self.red + + def mrvi(self): + """ + Modified Normalized Difference Vegetation Index RVI + https://www.indexdatabase.de/db/i-single.php?id=275 + :return: index + """ + return (self.rvi() - 1) / (self.rvi() + 1) + + def m_savi(self): + """ + Modified Soil Adjusted Vegetation Index + https://www.indexdatabase.de/db/i-single.php?id=44 + :return: index + """ + return ( + (2 * self.nir + 1) + - ((2 * self.nir + 1) ** 2 - 8 * (self.nir - self.red)) ** (1 / 2) + ) / 2 + + def norm_g(self): + """ + Norm G + https://www.indexdatabase.de/db/i-single.php?id=50 + :return: index + """ + return self.green / (self.nir + self.red + self.green) + + def norm_nir(self): + """ + Norm self.nir + https://www.indexdatabase.de/db/i-single.php?id=51 + :return: index + """ + return self.nir / (self.nir + self.red + self.green) + + def norm_r(self): + """ + Norm R + https://www.indexdatabase.de/db/i-single.php?id=52 + :return: index + """ + return self.red / (self.nir + self.red + self.green) + + def ngrdi(self): + """ + Normalized Difference self.green/self.red Normalized self.green self.red + difference index, Visible Atmospherically Resistant Indices self.green + (VIself.green) + https://www.indexdatabase.de/db/i-single.php?id=390 + :return: index + """ + return (self.green - self.red) / (self.green + self.red) + + def ri(self): + """ + Normalized Difference self.red/self.green self.redness Index + https://www.indexdatabase.de/db/i-single.php?id=74 + :return: index + """ + return (self.red - self.green) / (self.red + self.green) + + def s(self): + """ + Saturation + https://www.indexdatabase.de/db/i-single.php?id=77 + :return: index + """ + max_value = np.max([np.max(self.red), np.max(self.green), np.max(self.blue)]) + min_value = np.min([np.min(self.red), np.min(self.green), np.min(self.blue)]) + return (max_value - min_value) / max_value + + def _if(self): + """ + Shape Index + https://www.indexdatabase.de/db/i-single.php?id=79 + :return: index + """ + return (2 * self.red - self.green - self.blue) / (self.green - self.blue) + + def dvi(self): + """ + Simple Ratio self.nir/self.red Difference Vegetation Index, Vegetation Index + Number (VIN) + https://www.indexdatabase.de/db/i-single.php?id=12 + :return: index + """ + return self.nir / self.red + + def tvi(self): + """ + Transformed Vegetation Index + https://www.indexdatabase.de/db/i-single.php?id=98 + :return: index + """ + return (self.ndvi() + 0.5) ** (1 / 2) + + def ndre(self): + return (self.nir - self.redEdge) / (self.nir + self.redEdge) + + +""" +# genering a random matrices to test this class +red = np.ones((1000,1000, 1),dtype="float64") * 46787 +green = np.ones((1000,1000, 1),dtype="float64") * 23487 +blue = np.ones((1000,1000, 1),dtype="float64") * 14578 +redEdge = np.ones((1000,1000, 1),dtype="float64") * 51045 +nir = np.ones((1000,1000, 1),dtype="float64") * 52200 + +# Examples of how to use the class + +# instantiating the class +cl = IndexCalculation() + +# instantiating the class with the values +#cl = indexCalculation(red=red, green=green, blue=blue, redEdge=redEdge, nir=nir) + +# how set the values after instantiate the class cl, (for update the data or when don't +# instantiating the class with the values) +cl.setMatrices(red=red, green=green, blue=blue, redEdge=redEdge, nir=nir) + +# calculating the indices for the instantiated values in the class + # Note: the CCCI index can be changed to any index implemented in the class. +indexValue_form1 = cl.calculation("CCCI", red=red, green=green, blue=blue, + redEdge=redEdge, nir=nir).astype(np.float64) +indexValue_form2 = cl.CCCI() + +# calculating the index with the values directly -- you can set just the values +# preferred note: the *calculation* function performs the function *setMatrices* +indexValue_form3 = cl.calculation("CCCI", red=red, green=green, blue=blue, + redEdge=redEdge, nir=nir).astype(np.float64) + +print("Form 1: "+np.array2string(indexValue_form1, precision=20, separator=', ', + floatmode='maxprec_equal')) +print("Form 2: "+np.array2string(indexValue_form2, precision=20, separator=', ', + floatmode='maxprec_equal')) +print("Form 3: "+np.array2string(indexValue_form3, precision=20, separator=', ', + floatmode='maxprec_equal')) + +# A list of examples results for different type of data at NDVI +# float16 -> 0.31567383 #NDVI (red = 50, nir = 100) +# float32 -> 0.31578946 #NDVI (red = 50, nir = 100) +# float64 -> 0.3157894736842105 #NDVI (red = 50, nir = 100) +# longdouble -> 0.3157894736842105 #NDVI (red = 50, nir = 100) +""" diff --git a/digital_image_processing/morphological_operations/__init__.py b/digital_image_processing/morphological_operations/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/digital_image_processing/morphological_operations/dilation_operation.py b/digital_image_processing/morphological_operations/dilation_operation.py new file mode 100644 index 000000000000..e49b955c1480 --- /dev/null +++ b/digital_image_processing/morphological_operations/dilation_operation.py @@ -0,0 +1,75 @@ +from pathlib import Path + +import numpy as np +from PIL import Image + + +def rgb_to_gray(rgb: np.ndarray) -> np.ndarray: + """ + Return gray image from rgb image + >>> rgb_to_gray(np.array([[[127, 255, 0]]])) + array([[187.6453]]) + >>> rgb_to_gray(np.array([[[0, 0, 0]]])) + array([[0.]]) + >>> rgb_to_gray(np.array([[[2, 4, 1]]])) + array([[3.0598]]) + >>> rgb_to_gray(np.array([[[26, 255, 14], [5, 147, 20], [1, 200, 0]]])) + array([[159.0524, 90.0635, 117.6989]]) + """ + r, g, b = rgb[:, :, 0], rgb[:, :, 1], rgb[:, :, 2] + return 0.2989 * r + 0.5870 * g + 0.1140 * b + + +def gray_to_binary(gray: np.ndarray) -> np.ndarray: + """ + Return binary image from gray image + >>> gray_to_binary(np.array([[127, 255, 0]])) + array([[False, True, False]]) + >>> gray_to_binary(np.array([[0]])) + array([[False]]) + >>> gray_to_binary(np.array([[26.2409, 4.9315, 1.4729]])) + array([[False, False, False]]) + >>> gray_to_binary(np.array([[26, 255, 14], [5, 147, 20], [1, 200, 0]])) + array([[False, True, False], + [False, True, False], + [False, True, False]]) + """ + return (gray > 127) & (gray <= 255) + + +def dilation(image: np.ndarray, kernel: np.ndarray) -> np.ndarray: + """ + Return dilated image + >>> dilation(np.array([[True, False, True]]), np.array([[0, 1, 0]])) + array([[False, False, False]]) + >>> dilation(np.array([[False, False, True]]), np.array([[1, 0, 1]])) + array([[False, False, False]]) + """ + output = np.zeros_like(image) + image_padded = np.zeros( + (image.shape[0] + kernel.shape[0] - 1, image.shape[1] + kernel.shape[1] - 1) + ) + + # Copy image to padded image + image_padded[kernel.shape[0] - 2 : -1 :, kernel.shape[1] - 2 : -1 :] = image + + # Iterate over image & apply kernel + for x in range(image.shape[1]): + for y in range(image.shape[0]): + summation = ( + kernel * image_padded[y : y + kernel.shape[0], x : x + kernel.shape[1]] + ).sum() + output[y, x] = int(summation > 0) + return output + + +if __name__ == "__main__": + # read original image + lena_path = Path(__file__).resolve().parent / "image_data" / "lena.jpg" + lena = np.array(Image.open(lena_path)) + # kernel to be applied + structuring_element = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]) + output = dilation(gray_to_binary(rgb_to_gray(lena)), structuring_element) + # Save the output image + pil_img = Image.fromarray(output).convert("RGB") + pil_img.save("result_dilation.png") diff --git a/digital_image_processing/morphological_operations/erosion_operation.py b/digital_image_processing/morphological_operations/erosion_operation.py new file mode 100644 index 000000000000..53001da83468 --- /dev/null +++ b/digital_image_processing/morphological_operations/erosion_operation.py @@ -0,0 +1,82 @@ +from pathlib import Path + +import numpy as np +from PIL import Image + + +def rgb_to_gray(rgb: np.ndarray) -> np.ndarray: + """ + Return gray image from rgb image + + >>> rgb_to_gray(np.array([[[127, 255, 0]]])) + array([[187.6453]]) + >>> rgb_to_gray(np.array([[[0, 0, 0]]])) + array([[0.]]) + >>> rgb_to_gray(np.array([[[2, 4, 1]]])) + array([[3.0598]]) + >>> rgb_to_gray(np.array([[[26, 255, 14], [5, 147, 20], [1, 200, 0]]])) + array([[159.0524, 90.0635, 117.6989]]) + """ + r, g, b = rgb[:, :, 0], rgb[:, :, 1], rgb[:, :, 2] + return 0.2989 * r + 0.5870 * g + 0.1140 * b + + +def gray_to_binary(gray: np.ndarray) -> np.ndarray: + """ + Return binary image from gray image + + >>> gray_to_binary(np.array([[127, 255, 0]])) + array([[False, True, False]]) + >>> gray_to_binary(np.array([[0]])) + array([[False]]) + >>> gray_to_binary(np.array([[26.2409, 4.9315, 1.4729]])) + array([[False, False, False]]) + >>> gray_to_binary(np.array([[26, 255, 14], [5, 147, 20], [1, 200, 0]])) + array([[False, True, False], + [False, True, False], + [False, True, False]]) + """ + return (gray > 127) & (gray <= 255) + + +def erosion(image: np.ndarray, kernel: np.ndarray) -> np.ndarray: + """ + Return eroded image + + >>> erosion(np.array([[True, True, False]]), np.array([[0, 1, 0]])) + array([[False, False, False]]) + >>> erosion(np.array([[True, False, False]]), np.array([[1, 1, 0]])) + array([[False, False, False]]) + """ + output = np.zeros_like(image) + image_padded = np.zeros( + (image.shape[0] + kernel.shape[0] - 1, image.shape[1] + kernel.shape[1] - 1) + ) + + # Copy image to padded image + image_padded[kernel.shape[0] - 2 : -1 :, kernel.shape[1] - 2 : -1 :] = image + + # Iterate over image & apply kernel + for x in range(image.shape[1]): + for y in range(image.shape[0]): + summation = ( + kernel * image_padded[y : y + kernel.shape[0], x : x + kernel.shape[1]] + ).sum() + output[y, x] = int(summation == 5) + return output + + +if __name__ == "__main__": + # read original image + lena_path = Path(__file__).resolve().parent / "image_data" / "lena.jpg" + lena = np.array(Image.open(lena_path)) + + # kernel to be applied + structuring_element = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]) + + # Apply erosion operation to a binary image + output = erosion(gray_to_binary(rgb_to_gray(lena)), structuring_element) + + # Save the output image + pil_img = Image.fromarray(output).convert("RGB") + pil_img.save("result_erosion.png") diff --git a/digital_image_processing/resize/__init__.py b/digital_image_processing/resize/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/digital_image_processing/resize/resize.py b/digital_image_processing/resize/resize.py new file mode 100644 index 000000000000..d977a1ab7418 --- /dev/null +++ b/digital_image_processing/resize/resize.py @@ -0,0 +1,72 @@ +"""Multiple image resizing techniques""" + +import numpy as np +from cv2 import destroyAllWindows, imread, imshow, waitKey + + +class NearestNeighbour: + """ + Simplest and fastest version of image resizing. + Source: https://en.wikipedia.org/wiki/Nearest-neighbor_interpolation + """ + + def __init__(self, img, dst_width: int, dst_height: int) -> None: + if dst_width < 0 or dst_height < 0: + raise ValueError("Destination width/height should be > 0") + + self.img = img + self.src_w = img.shape[1] + self.src_h = img.shape[0] + self.dst_w = dst_width + self.dst_h = dst_height + + self.ratio_x = self.src_w / self.dst_w + self.ratio_y = self.src_h / self.dst_h + + self.output = self.output_img = ( + np.ones((self.dst_h, self.dst_w, 3), np.uint8) * 255 + ) + + def process(self) -> None: + for i in range(self.dst_h): + for j in range(self.dst_w): + self.output[i][j] = self.img[self.get_y(i)][self.get_x(j)] + + def get_x(self, x: int) -> int: + """ + Get parent X coordinate for destination X + :param x: Destination X coordinate + :return: Parent X coordinate based on `x ratio` + >>> nn = NearestNeighbour(imread("digital_image_processing/image_data/lena.jpg", + ... 1), 100, 100) + >>> nn.ratio_x = 0.5 + >>> nn.get_x(4) + 2 + """ + return int(self.ratio_x * x) + + def get_y(self, y: int) -> int: + """ + Get parent Y coordinate for destination Y + :param y: Destination X coordinate + :return: Parent X coordinate based on `y ratio` + >>> nn = NearestNeighbour(imread("digital_image_processing/image_data/lena.jpg", + ... 1), 100, 100) + >>> nn.ratio_y = 0.5 + >>> nn.get_y(4) + 2 + """ + return int(self.ratio_y * y) + + +if __name__ == "__main__": + dst_w, dst_h = 800, 600 + im = imread("image_data/lena.jpg", 1) + n = NearestNeighbour(im, dst_w, dst_h) + n.process() + + imshow( + f"Image resized from: {im.shape[1]}x{im.shape[0]} to {dst_w}x{dst_h}", n.output + ) + waitKey(0) + destroyAllWindows() diff --git a/digital_image_processing/rotation/__init__.py b/digital_image_processing/rotation/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/digital_image_processing/rotation/rotation.py b/digital_image_processing/rotation/rotation.py new file mode 100644 index 000000000000..0f5e36ddd5be --- /dev/null +++ b/digital_image_processing/rotation/rotation.py @@ -0,0 +1,56 @@ +from pathlib import Path + +import cv2 +import numpy as np +from matplotlib import pyplot as plt + + +def get_rotation( + img: np.ndarray, pt1: np.ndarray, pt2: np.ndarray, rows: int, cols: int +) -> np.ndarray: + """ + Get image rotation + :param img: np.ndarray + :param pt1: 3x2 list + :param pt2: 3x2 list + :param rows: columns image shape + :param cols: rows image shape + :return: np.ndarray + """ + matrix = cv2.getAffineTransform(pt1, pt2) + return cv2.warpAffine(img, matrix, (rows, cols)) + + +if __name__ == "__main__": + # read original image + image = cv2.imread( + str(Path(__file__).resolve().parent.parent / "image_data" / "lena.jpg") + ) + # turn image in gray scale value + gray_img = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + # get image shape + img_rows, img_cols = gray_img.shape + + # set different points to rotate image + pts1 = np.array([[50, 50], [200, 50], [50, 200]], np.float32) + pts2 = np.array([[10, 100], [200, 50], [100, 250]], np.float32) + pts3 = np.array([[50, 50], [150, 50], [120, 200]], np.float32) + pts4 = np.array([[10, 100], [80, 50], [180, 250]], np.float32) + + # add all rotated images in a list + images = [ + gray_img, + get_rotation(gray_img, pts1, pts2, img_rows, img_cols), + get_rotation(gray_img, pts2, pts3, img_rows, img_cols), + get_rotation(gray_img, pts2, pts4, img_rows, img_cols), + ] + + # plot different image rotations + fig = plt.figure(1) + titles = ["Original", "Rotation 1", "Rotation 2", "Rotation 3"] + for i, image in enumerate(images): + plt.subplot(2, 2, i + 1), plt.imshow(image, "gray") + plt.title(titles[i]) + plt.axis("off") + plt.subplots_adjust(left=0.0, bottom=0.05, right=1.0, top=0.95) + plt.show() diff --git a/digital_image_processing/sepia.py b/digital_image_processing/sepia.py new file mode 100644 index 000000000000..1924a80451e5 --- /dev/null +++ b/digital_image_processing/sepia.py @@ -0,0 +1,51 @@ +""" +Implemented an algorithm using opencv to tone an image with sepia technique +""" + +from cv2 import destroyAllWindows, imread, imshow, waitKey + + +def make_sepia(img, factor: int): + """ + Function create sepia tone. + Source: https://en.wikipedia.org/wiki/Sepia_(color) + """ + pixel_h, pixel_v = img.shape[0], img.shape[1] + + def to_grayscale(blue, green, red): + """ + Helper function to create pixel's greyscale representation + Src: https://pl.wikipedia.org/wiki/YUV + """ + return 0.2126 * red + 0.587 * green + 0.114 * blue + + def normalize(value): + """Helper function to normalize R/G/B value -> return 255 if value > 255""" + return min(value, 255) + + for i in range(pixel_h): + for j in range(pixel_v): + greyscale = int(to_grayscale(*img[i][j])) + img[i][j] = [ + normalize(greyscale), + normalize(greyscale + factor), + normalize(greyscale + 2 * factor), + ] + + return img + + +if __name__ == "__main__": + # read original image + images = { + percentage: imread("image_data/lena.jpg", 1) for percentage in (10, 20, 30, 40) + } + + for percentage, img in images.items(): + make_sepia(img, percentage) + + for percentage, img in images.items(): + imshow(f"Original image with sepia (factor: {percentage})", img) + + waitKey(0) + destroyAllWindows() diff --git a/digital_image_processing/test_digital_image_processing.py b/digital_image_processing/test_digital_image_processing.py new file mode 100644 index 000000000000..4bc0fb94b900 --- /dev/null +++ b/digital_image_processing/test_digital_image_processing.py @@ -0,0 +1,136 @@ +""" +PyTest's for Digital Image Processing +""" + +import numpy as np +from cv2 import COLOR_BGR2GRAY, cvtColor, imread +from numpy import array, uint8 +from PIL import Image + +from digital_image_processing import change_contrast as cc +from digital_image_processing import convert_to_negative as cn +from digital_image_processing import sepia as sp +from digital_image_processing.dithering import burkes as bs +from digital_image_processing.edge_detection import canny +from digital_image_processing.filters import convolve as conv +from digital_image_processing.filters import gaussian_filter as gg +from digital_image_processing.filters import local_binary_pattern as lbp +from digital_image_processing.filters import median_filter as med +from digital_image_processing.filters import sobel_filter as sob +from digital_image_processing.resize import resize as rs + +img = imread(r"digital_image_processing/image_data/lena_small.jpg") +gray = cvtColor(img, COLOR_BGR2GRAY) + + +# Test: convert_to_negative() +def test_convert_to_negative() -> None: + negative_img = cn.convert_to_negative(img) + # assert negative_img array for at least one True + assert negative_img.any() + + +# Test: change_contrast() +def test_change_contrast() -> None: + with Image.open("digital_image_processing/image_data/lena_small.jpg") as img: + # Work around assertion for response + assert str(cc.change_contrast(img, 110)).startswith( + " None: + resp = canny.gen_gaussian_kernel(9, sigma=1.4) + # Assert ambiguous array + assert resp.all() + + +# canny.py +def test_canny() -> None: + canny_img = imread("digital_image_processing/image_data/lena_small.jpg", 0) + # assert ambiguous array for all == True + assert canny_img.all() + canny_array = canny.canny(canny_img) + # assert canny array for at least one True + assert canny_array.any() + + +# filters/gaussian_filter.py +def test_gen_gaussian_kernel_filter() -> None: + assert gg.gaussian_filter(gray, 5, sigma=0.9).all() + + +def test_convolve_filter() -> None: + # laplace diagonals + laplace = array([[0.25, 0.5, 0.25], [0.5, -3, 0.5], [0.25, 0.5, 0.25]]) + res = conv.img_convolve(gray, laplace).astype(uint8) + assert res.any() + + +def test_median_filter() -> None: + assert med.median_filter(gray, 3).any() + + +def test_sobel_filter() -> None: + grad, theta = sob.sobel_filter(gray) + assert grad.any() + assert theta.any() + + +def test_sepia() -> None: + sepia = sp.make_sepia(img, 20) + assert sepia.all() + + +def test_burkes( + file_path: str = "digital_image_processing/image_data/lena_small.jpg", +) -> None: + burkes = bs.Burkes(imread(file_path, 1), 120) + burkes.process() + assert burkes.output_img.any() + + +def test_nearest_neighbour( + file_path: str = "digital_image_processing/image_data/lena_small.jpg", +) -> None: + nn = rs.NearestNeighbour(imread(file_path, 1), 400, 200) + nn.process() + assert nn.output.any() + + +def test_local_binary_pattern() -> None: + # pull request 10161 before: + # "digital_image_processing/image_data/lena.jpg" + # after: "digital_image_processing/image_data/lena_small.jpg" + + from os import getenv # Speed up our Continuous Integration tests + + file_name = "lena_small.jpg" if getenv("CI") else "lena.jpg" + file_path = f"digital_image_processing/image_data/{file_name}" + + # Reading the image and converting it to grayscale + image = imread(file_path, 0) + + # Test for get_neighbors_pixel function() return not None + x_coordinate = 0 + y_coordinate = 0 + center = image[x_coordinate][y_coordinate] + + neighbors_pixels = lbp.get_neighbors_pixel( + image, x_coordinate, y_coordinate, center + ) + + assert neighbors_pixels is not None + + # Test for local_binary_pattern function() + # Create a numpy array as the same height and width of read image + lbp_image = np.zeros((image.shape[0], image.shape[1])) + + # Iterating through the image and calculating the local binary pattern value + # for each pixel. + for i in range(image.shape[0]): + for j in range(image.shape[1]): + lbp_image[i][j] = lbp.local_binary_value(image, i, j) + + assert lbp_image.any() diff --git a/divide_and_conquer/__init__.py b/divide_and_conquer/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/divide_and_conquer/closest_pair_of_points.py b/divide_and_conquer/closest_pair_of_points.py new file mode 100644 index 000000000000..534cbba9b718 --- /dev/null +++ b/divide_and_conquer/closest_pair_of_points.py @@ -0,0 +1,141 @@ +""" +The algorithm finds distance between closest pair of points +in the given n points. +Approach used -> Divide and conquer +The points are sorted based on Xco-ords and +then based on Yco-ords separately. +And by applying divide and conquer approach, +minimum distance is obtained recursively. + +>> Closest points can lie on different sides of partition. +This case handled by forming a strip of points +whose Xco-ords distance is less than closest_pair_dis +from mid-point's Xco-ords. Points sorted based on Yco-ords +are used in this step to reduce sorting time. +Closest pair distance is found in the strip of points. (closest_in_strip) + +min(closest_pair_dis, closest_in_strip) would be the final answer. + +Time complexity: O(n * log n) +""" + + +def euclidean_distance_sqr(point1, point2): + """ + >>> euclidean_distance_sqr([1,2],[2,4]) + 5 + """ + return (point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2 + + +def column_based_sort(array, column=0): + """ + >>> column_based_sort([(5, 1), (4, 2), (3, 0)], 1) + [(3, 0), (5, 1), (4, 2)] + """ + return sorted(array, key=lambda x: x[column]) + + +def dis_between_closest_pair(points, points_counts, min_dis=float("inf")): + """ + brute force approach to find distance between closest pair points + + Parameters : + points, points_count, min_dis (list(tuple(int, int)), int, int) + + Returns : + min_dis (float): distance between closest pair of points + + >>> dis_between_closest_pair([[1,2],[2,4],[5,7],[8,9],[11,0]],5) + 5 + + """ + + for i in range(points_counts - 1): + for j in range(i + 1, points_counts): + current_dis = euclidean_distance_sqr(points[i], points[j]) + min_dis = min(min_dis, current_dis) + return min_dis + + +def dis_between_closest_in_strip(points, points_counts, min_dis=float("inf")): + """ + closest pair of points in strip + + Parameters : + points, points_count, min_dis (list(tuple(int, int)), int, int) + + Returns : + min_dis (float): distance btw closest pair of points in the strip (< min_dis) + + >>> dis_between_closest_in_strip([[1,2],[2,4],[5,7],[8,9],[11,0]],5) + 85 + """ + + for i in range(min(6, points_counts - 1), points_counts): + for j in range(max(0, i - 6), i): + current_dis = euclidean_distance_sqr(points[i], points[j]) + min_dis = min(min_dis, current_dis) + return min_dis + + +def closest_pair_of_points_sqr(points_sorted_on_x, points_sorted_on_y, points_counts): + """divide and conquer approach + + Parameters : + points, points_count (list(tuple(int, int)), int) + + Returns : + (float): distance btw closest pair of points + + >>> closest_pair_of_points_sqr([(1, 2), (3, 4)], [(5, 6), (7, 8)], 2) + 8 + """ + + # base case + if points_counts <= 3: + return dis_between_closest_pair(points_sorted_on_x, points_counts) + + # recursion + mid = points_counts // 2 + closest_in_left = closest_pair_of_points_sqr( + points_sorted_on_x, points_sorted_on_y[:mid], mid + ) + closest_in_right = closest_pair_of_points_sqr( + points_sorted_on_y, points_sorted_on_y[mid:], points_counts - mid + ) + closest_pair_dis = min(closest_in_left, closest_in_right) + + """ + cross_strip contains the points, whose Xcoords are at a + distance(< closest_pair_dis) from mid's Xcoord + """ + + cross_strip = [] + for point in points_sorted_on_x: + if abs(point[0] - points_sorted_on_x[mid][0]) < closest_pair_dis: + cross_strip.append(point) + + closest_in_strip = dis_between_closest_in_strip( + cross_strip, len(cross_strip), closest_pair_dis + ) + return min(closest_pair_dis, closest_in_strip) + + +def closest_pair_of_points(points, points_counts): + """ + >>> closest_pair_of_points([(2, 3), (12, 30)], len([(2, 3), (12, 30)])) + 28.792360097775937 + """ + points_sorted_on_x = column_based_sort(points, column=0) + points_sorted_on_y = column_based_sort(points, column=1) + return ( + closest_pair_of_points_sqr( + points_sorted_on_x, points_sorted_on_y, points_counts + ) + ) ** 0.5 + + +if __name__ == "__main__": + points = [(2, 3), (12, 30), (40, 50), (5, 1), (12, 10), (3, 4)] + print("Distance:", closest_pair_of_points(points, len(points))) diff --git a/divide_and_conquer/convex_hull.py b/divide_and_conquer/convex_hull.py new file mode 100644 index 000000000000..24eec4af23d8 --- /dev/null +++ b/divide_and_conquer/convex_hull.py @@ -0,0 +1,506 @@ +""" +The convex hull problem is problem of finding all the vertices of convex polygon, P of +a set of points in a plane such that all the points are either on the vertices of P or +inside P. TH convex hull problem has several applications in geometrical problems, +computer graphics and game development. + +Two algorithms have been implemented for the convex hull problem here. +1. A brute-force algorithm which runs in O(n^3) +2. A divide-and-conquer algorithm which runs in O(n log(n)) + +There are other several other algorithms for the convex hull problem +which have not been implemented here, yet. + +""" + +from __future__ import annotations + +from collections.abc import Iterable + + +class Point: + """ + Defines a 2-d point for use by all convex-hull algorithms. + + Parameters + ---------- + x: an int or a float, the x-coordinate of the 2-d point + y: an int or a float, the y-coordinate of the 2-d point + + Examples + -------- + >>> Point(1, 2) + (1.0, 2.0) + >>> Point("1", "2") + (1.0, 2.0) + >>> Point(1, 2) > Point(0, 1) + True + >>> Point(1, 1) == Point(1, 1) + True + >>> Point(-0.5, 1) == Point(0.5, 1) + False + >>> Point("pi", "e") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'pi' + """ + + def __init__(self, x, y) -> None: + self.x, self.y = float(x), float(y) + + def __eq__(self, other) -> bool: + return self.x == other.x and self.y == other.y + + def __ne__(self, other) -> bool: + return not self == other + + def __gt__(self, other) -> bool: + if self.x > other.x: + return True + elif self.x == other.x: + return self.y > other.y + return False + + def __lt__(self, other) -> bool: + return not self > other + + def __ge__(self, other) -> bool: + if self.x > other.x: + return True + elif self.x == other.x: + return self.y >= other.y + return False + + def __le__(self, other) -> bool: + if self.x < other.x: + return True + elif self.x == other.x: + return self.y <= other.y + return False + + def __repr__(self) -> str: + return f"({self.x}, {self.y})" + + def __hash__(self) -> int: + return hash(self.x) + + +def _construct_points( + list_of_tuples: list[Point] | list[list[float]] | Iterable[list[float]], +) -> list[Point]: + """ + constructs a list of points from an array-like object of numbers + + Arguments + --------- + + list_of_tuples: array-like object of type numbers. Acceptable types so far + are lists, tuples and sets. + + Returns + -------- + points: a list where each item is of type Point. This contains only objects + which can be converted into a Point. + + Examples + ------- + >>> _construct_points([[1, 1], [2, -1], [0.3, 4]]) + [(1.0, 1.0), (2.0, -1.0), (0.3, 4.0)] + >>> _construct_points([1, 2]) + Ignoring deformed point 1. All points must have at least 2 coordinates. + Ignoring deformed point 2. All points must have at least 2 coordinates. + [] + >>> _construct_points([]) + [] + >>> _construct_points(None) + [] + """ + + points: list[Point] = [] + if list_of_tuples: + for p in list_of_tuples: + if isinstance(p, Point): + points.append(p) + else: + try: + points.append(Point(p[0], p[1])) + except IndexError, TypeError: + print( + f"Ignoring deformed point {p}. All points" + " must have at least 2 coordinates." + ) + return points + + +def _validate_input(points: list[Point] | list[list[float]]) -> list[Point]: + """ + validates an input instance before a convex-hull algorithms uses it + + Parameters + --------- + points: array-like, the 2d points to validate before using with + a convex-hull algorithm. The elements of points must be either lists, tuples or + Points. + + Returns + ------- + points: array_like, an iterable of all well-defined Points constructed passed in. + + + Exception + --------- + ValueError: if points is empty or None, or if a wrong data structure like a scalar + is passed + + TypeError: if an iterable but non-indexable object (eg. dictionary) is passed. + The exception to this a set which we'll convert to a list before using + + + Examples + ------- + >>> _validate_input([[1, 2]]) + [(1.0, 2.0)] + >>> _validate_input([(1, 2)]) + [(1.0, 2.0)] + >>> _validate_input([Point(2, 1), Point(-1, 2)]) + [(2.0, 1.0), (-1.0, 2.0)] + >>> _validate_input([]) + Traceback (most recent call last): + ... + ValueError: Expecting a list of points but got [] + >>> _validate_input(1) + Traceback (most recent call last): + ... + ValueError: Expecting an iterable object but got an non-iterable type 1 + """ + + if not hasattr(points, "__iter__"): + msg = f"Expecting an iterable object but got an non-iterable type {points}" + raise ValueError(msg) + + if not points: + msg = f"Expecting a list of points but got {points}" + raise ValueError(msg) + + return _construct_points(points) + + +def _det(a: Point, b: Point, c: Point) -> float: + """ + Computes the sign perpendicular distance of a 2d point c from a line segment + ab. The sign indicates the direction of c relative to ab. + A Positive value means c is above ab (to the left), while a negative value + means c is below ab (to the right). 0 means all three points are on a straight line. + + As a side note, 0.5 * abs|det| is the area of triangle abc + + Parameters + ---------- + a: point, the point on the left end of line segment ab + b: point, the point on the right end of line segment ab + c: point, the point for which the direction and location is desired. + + Returns + -------- + det: float, abs(det) is the distance of c from ab. The sign + indicates which side of line segment ab c is. det is computed as + (a_xb_y + c_xa_y + b_xc_y) - (a_yb_x + c_ya_x + b_yc_x) + + Examples + ---------- + >>> _det(Point(1, 1), Point(1, 2), Point(1, 5)) + 0.0 + >>> _det(Point(0, 0), Point(10, 0), Point(0, 10)) + 100.0 + >>> _det(Point(0, 0), Point(10, 0), Point(0, -10)) + -100.0 + """ + + det = (a.x * b.y + b.x * c.y + c.x * a.y) - (a.y * b.x + b.y * c.x + c.y * a.x) + return det + + +def convex_hull_bf(points: list[Point]) -> list[Point]: + """ + Constructs the convex hull of a set of 2D points using a brute force algorithm. + The algorithm basically considers all combinations of points (i, j) and uses the + definition of convexity to determine whether (i, j) is part of the convex hull or + not. (i, j) is part of the convex hull if and only iff there are no points on both + sides of the line segment connecting the ij, and there is no point k such that k is + on either end of the ij. + + Runtime: O(n^3) - definitely horrible + + Parameters + --------- + points: array-like of object of Points, lists or tuples. + The set of 2d points for which the convex-hull is needed + + Returns + ------ + convex_set: list, the convex-hull of points sorted in non-decreasing order. + + See Also + -------- + convex_hull_recursive, + + Examples + --------- + >>> convex_hull_bf([[0, 0], [1, 0], [10, 1]]) + [(0.0, 0.0), (1.0, 0.0), (10.0, 1.0)] + >>> convex_hull_bf([[0, 0], [1, 0], [10, 0]]) + [(0.0, 0.0), (10.0, 0.0)] + >>> convex_hull_bf([[-1, 1],[-1, -1], [0, 0], [0.5, 0.5], [1, -1], [1, 1], + ... [-0.75, 1]]) + [(-1.0, -1.0), (-1.0, 1.0), (1.0, -1.0), (1.0, 1.0)] + >>> convex_hull_bf([(0, 3), (2, 2), (1, 1), (2, 1), (3, 0), (0, 0), (3, 3), + ... (2, -1), (2, -4), (1, -3)]) + [(0.0, 0.0), (0.0, 3.0), (1.0, -3.0), (2.0, -4.0), (3.0, 0.0), (3.0, 3.0)] + """ + + points = sorted(_validate_input(points)) + n = len(points) + convex_set = set() + + for i in range(n - 1): + for j in range(i + 1, n): + points_left_of_ij = points_right_of_ij = False + ij_part_of_convex_hull = True + for k in range(n): + if k not in {i, j}: + det_k = _det(points[i], points[j], points[k]) + + if det_k > 0: + points_left_of_ij = True + elif det_k < 0: + points_right_of_ij = True + # point[i], point[j], point[k] all lie on a straight line + # if point[k] is to the left of point[i] or it's to the + # right of point[j], then point[i], point[j] cannot be + # part of the convex hull of A + elif points[k] < points[i] or points[k] > points[j]: + ij_part_of_convex_hull = False + break + + if points_left_of_ij and points_right_of_ij: + ij_part_of_convex_hull = False + break + + if ij_part_of_convex_hull: + convex_set.update([points[i], points[j]]) + + return sorted(convex_set) + + +def convex_hull_recursive(points: list[Point]) -> list[Point]: + """ + Constructs the convex hull of a set of 2D points using a divide-and-conquer strategy + The algorithm exploits the geometric properties of the problem by repeatedly + partitioning the set of points into smaller hulls, and finding the convex hull of + these smaller hulls. The union of the convex hull from smaller hulls is the + solution to the convex hull of the larger problem. + + Parameter + --------- + points: array-like of object of Points, lists or tuples. + The set of 2d points for which the convex-hull is needed + + Runtime: O(n log n) + + Returns + ------- + convex_set: list, the convex-hull of points sorted in non-decreasing order. + + Examples + --------- + >>> convex_hull_recursive([[0, 0], [1, 0], [10, 1]]) + [(0.0, 0.0), (1.0, 0.0), (10.0, 1.0)] + >>> convex_hull_recursive([[0, 0], [1, 0], [10, 0]]) + [(0.0, 0.0), (10.0, 0.0)] + >>> convex_hull_recursive([[-1, 1],[-1, -1], [0, 0], [0.5, 0.5], [1, -1], [1, 1], + ... [-0.75, 1]]) + [(-1.0, -1.0), (-1.0, 1.0), (1.0, -1.0), (1.0, 1.0)] + >>> convex_hull_recursive([(0, 3), (2, 2), (1, 1), (2, 1), (3, 0), (0, 0), (3, 3), + ... (2, -1), (2, -4), (1, -3)]) + [(0.0, 0.0), (0.0, 3.0), (1.0, -3.0), (2.0, -4.0), (3.0, 0.0), (3.0, 3.0)] + + """ + points = sorted(_validate_input(points)) + n = len(points) + + # divide all the points into an upper hull and a lower hull + # the left most point and the right most point are definitely + # members of the convex hull by definition. + # use these two anchors to divide all the points into two hulls, + # an upper hull and a lower hull. + + # all points to the left (above) the line joining the extreme points belong to the + # upper hull + # all points to the right (below) the line joining the extreme points below to the + # lower hull + # ignore all points on the line joining the extreme points since they cannot be + # part of the convex hull + + left_most_point = points[0] + right_most_point = points[n - 1] + + convex_set = {left_most_point, right_most_point} + upper_hull = [] + lower_hull = [] + + for i in range(1, n - 1): + det = _det(left_most_point, right_most_point, points[i]) + + if det > 0: + upper_hull.append(points[i]) + elif det < 0: + lower_hull.append(points[i]) + + _construct_hull(upper_hull, left_most_point, right_most_point, convex_set) + _construct_hull(lower_hull, right_most_point, left_most_point, convex_set) + + return sorted(convex_set) + + +def _construct_hull( + points: list[Point], left: Point, right: Point, convex_set: set[Point] +) -> None: + """ + + Parameters + --------- + points: list or None, the hull of points from which to choose the next convex-hull + point + left: Point, the point to the left of line segment joining left and right + right: The point to the right of the line segment joining left and right + convex_set: set, the current convex-hull. The state of convex-set gets updated by + this function + + Note + ---- + For the line segment 'ab', 'a' is on the left and 'b' on the right. + but the reverse is true for the line segment 'ba'. + + Returns + ------- + Nothing, only updates the state of convex-set + """ + if points: + extreme_point = None + extreme_point_distance = float("-inf") + candidate_points = [] + + for p in points: + det = _det(left, right, p) + + if det > 0: + candidate_points.append(p) + + if det > extreme_point_distance: + extreme_point_distance = det + extreme_point = p + + if extreme_point: + _construct_hull(candidate_points, left, extreme_point, convex_set) + convex_set.add(extreme_point) + _construct_hull(candidate_points, extreme_point, right, convex_set) + + +def convex_hull_melkman(points: list[Point]) -> list[Point]: + """ + Constructs the convex hull of a set of 2D points using the melkman algorithm. + The algorithm works by iteratively inserting points of a simple polygonal chain + (meaning that no line segments between two consecutive points cross each other). + Sorting the points yields such a polygonal chain. + + For a detailed description, see http://cgm.cs.mcgill.ca/~athens/cs601/Melkman.html + + Runtime: O(n log n) - O(n) if points are already sorted in the input + + Parameters + --------- + points: array-like of object of Points, lists or tuples. + The set of 2d points for which the convex-hull is needed + + Returns + ------ + convex_set: list, the convex-hull of points sorted in non-decreasing order. + + See Also + -------- + + Examples + --------- + >>> convex_hull_melkman([[0, 0], [1, 0], [10, 1]]) + [(0.0, 0.0), (1.0, 0.0), (10.0, 1.0)] + >>> convex_hull_melkman([[0, 0], [1, 0], [10, 0]]) + [(0.0, 0.0), (10.0, 0.0)] + >>> convex_hull_melkman([[-1, 1],[-1, -1], [0, 0], [0.5, 0.5], [1, -1], [1, 1], + ... [-0.75, 1]]) + [(-1.0, -1.0), (-1.0, 1.0), (1.0, -1.0), (1.0, 1.0)] + >>> convex_hull_melkman([(0, 3), (2, 2), (1, 1), (2, 1), (3, 0), (0, 0), (3, 3), + ... (2, -1), (2, -4), (1, -3)]) + [(0.0, 0.0), (0.0, 3.0), (1.0, -3.0), (2.0, -4.0), (3.0, 0.0), (3.0, 3.0)] + """ + points = sorted(_validate_input(points)) + n = len(points) + + convex_hull = points[:2] + for i in range(2, n): + det = _det(convex_hull[1], convex_hull[0], points[i]) + if det > 0: + convex_hull.insert(0, points[i]) + break + if det < 0: + convex_hull.append(points[i]) + break + convex_hull[1] = points[i] + i += 1 + + for j in range(i, n): + if ( + _det(convex_hull[0], convex_hull[-1], points[j]) > 0 + and _det(convex_hull[-1], convex_hull[0], points[1]) < 0 + ): + # The point lies within the convex hull + continue + + convex_hull.insert(0, points[j]) + convex_hull.append(points[j]) + while _det(convex_hull[0], convex_hull[1], convex_hull[2]) >= 0: + del convex_hull[1] + while _det(convex_hull[-1], convex_hull[-2], convex_hull[-3]) <= 0: + del convex_hull[-2] + + # `convex_hull` is contains the convex hull in circular order + return sorted(convex_hull[1:] if len(convex_hull) > 3 else convex_hull) + + +def main() -> None: + points = [ + (0, 3), + (2, 2), + (1, 1), + (2, 1), + (3, 0), + (0, 0), + (3, 3), + (2, -1), + (2, -4), + (1, -3), + ] + # the convex set of points is + # [(0, 0), (0, 3), (1, -3), (2, -4), (3, 0), (3, 3)] + results_bf = convex_hull_bf(points) + + results_recursive = convex_hull_recursive(points) + assert results_bf == results_recursive + + results_melkman = convex_hull_melkman(points) + assert results_bf == results_melkman + + print(results_bf) + + +if __name__ == "__main__": + main() diff --git a/divide_and_conquer/heaps_algorithm.py b/divide_and_conquer/heaps_algorithm.py new file mode 100644 index 000000000000..3235d59e5d90 --- /dev/null +++ b/divide_and_conquer/heaps_algorithm.py @@ -0,0 +1,56 @@ +""" +Heap's algorithm returns the list of all permutations possible from a list. +It minimizes movement by generating each permutation from the previous one +by swapping only two elements. +More information: +https://en.wikipedia.org/wiki/Heap%27s_algorithm. +""" + + +def heaps(arr: list) -> list: + """ + Pure python implementation of the Heap's algorithm (recursive version), + returning all permutations of a list. + >>> heaps([]) + [()] + >>> heaps([0]) + [(0,)] + >>> heaps([-1, 1]) + [(-1, 1), (1, -1)] + >>> heaps([1, 2, 3]) + [(1, 2, 3), (2, 1, 3), (3, 1, 2), (1, 3, 2), (2, 3, 1), (3, 2, 1)] + >>> from itertools import permutations + >>> sorted(heaps([1,2,3])) == sorted(permutations([1,2,3])) + True + >>> all(sorted(heaps(x)) == sorted(permutations(x)) + ... for x in ([], [0], [-1, 1], [1, 2, 3])) + True + """ + + if len(arr) <= 1: + return [tuple(arr)] + + res = [] + + def generate(k: int, arr: list) -> None: + if k == 1: + res.append(tuple(arr[:])) + return + + generate(k - 1, arr) + + for i in range(k - 1): + if k % 2 == 0: # k is even + arr[i], arr[k - 1] = arr[k - 1], arr[i] + else: # k is odd + arr[0], arr[k - 1] = arr[k - 1], arr[0] + generate(k - 1, arr) + + generate(len(arr), arr) + return res + + +if __name__ == "__main__": + user_input = input("Enter numbers separated by a comma:\n").strip() + arr = [int(item) for item in user_input.split(",")] + print(heaps(arr)) diff --git a/divide_and_conquer/heaps_algorithm_iterative.py b/divide_and_conquer/heaps_algorithm_iterative.py new file mode 100644 index 000000000000..6701489b304c --- /dev/null +++ b/divide_and_conquer/heaps_algorithm_iterative.py @@ -0,0 +1,60 @@ +""" +Heap's (iterative) algorithm returns the list of all permutations possible from a list. +It minimizes movement by generating each permutation from the previous one +by swapping only two elements. +More information: +https://en.wikipedia.org/wiki/Heap%27s_algorithm. +""" + + +def heaps(arr: list) -> list: + """ + Pure python implementation of the iterative Heap's algorithm, + returning all permutations of a list. + >>> heaps([]) + [()] + >>> heaps([0]) + [(0,)] + >>> heaps([-1, 1]) + [(-1, 1), (1, -1)] + >>> heaps([1, 2, 3]) + [(1, 2, 3), (2, 1, 3), (3, 1, 2), (1, 3, 2), (2, 3, 1), (3, 2, 1)] + >>> from itertools import permutations + >>> sorted(heaps([1,2,3])) == sorted(permutations([1,2,3])) + True + >>> all(sorted(heaps(x)) == sorted(permutations(x)) + ... for x in ([], [0], [-1, 1], [1, 2, 3])) + True + """ + + if len(arr) <= 1: + return [tuple(arr)] + + res = [] + + def generate(n: int, arr: list) -> None: + c = [0] * n + res.append(tuple(arr)) + + i = 0 + while i < n: + if c[i] < i: + if i % 2 == 0: + arr[0], arr[i] = arr[i], arr[0] + else: + arr[c[i]], arr[i] = arr[i], arr[c[i]] + res.append(tuple(arr)) + c[i] += 1 + i = 0 + else: + c[i] = 0 + i += 1 + + generate(len(arr), arr) + return res + + +if __name__ == "__main__": + user_input = input("Enter numbers separated by a comma:\n").strip() + arr = [int(item) for item in user_input.split(",")] + print(heaps(arr)) diff --git a/divide_and_conquer/inversions.py b/divide_and_conquer/inversions.py new file mode 100644 index 000000000000..6fe1fd9c7cb7 --- /dev/null +++ b/divide_and_conquer/inversions.py @@ -0,0 +1,153 @@ +""" +Given an array-like data structure A[1..n], how many pairs +(i, j) for all 1 <= i < j <= n such that A[i] > A[j]? These pairs are +called inversions. Counting the number of such inversions in an array-like +object is the important. Among other things, counting inversions can help +us determine how close a given array is to being sorted. +In this implementation, I provide two algorithms, a divide-and-conquer +algorithm which runs in nlogn and the brute-force n^2 algorithm. +""" + + +def count_inversions_bf(arr: list[int]) -> int: + """ + Counts the number of inversions using a naive brute-force algorithm + Parameters + ---------- + arr: arr: array-like, the list containing the items for which the number + of inversions is desired. The elements of `arr` must be comparable. + Returns + ------- + num_inversions: The total number of inversions in `arr` + Examples + --------- + >>> count_inversions_bf([1, 4, 2, 4, 1]) + 4 + >>> count_inversions_bf([1, 1, 2, 4, 4]) + 0 + >>> count_inversions_bf([]) + 0 + """ + + num_inversions = 0 + n = len(arr) + + for i in range(n - 1): + for j in range(i + 1, n): + if arr[i] > arr[j]: + num_inversions += 1 + + return num_inversions + + +def count_inversions_recursive(arr: list[int]) -> tuple[list[int], int]: + """ + Counts the number of inversions using a divide-and-conquer algorithm + Parameters + ----------- + arr: array-like, the list containing the items for which the number + of inversions is desired. The elements of `arr` must be comparable. + Returns + ------- + C: a sorted copy of `arr`. + num_inversions: int, the total number of inversions in 'arr' + Examples + -------- + >>> count_inversions_recursive([1, 4, 2, 4, 1]) + ([1, 1, 2, 4, 4], 4) + >>> count_inversions_recursive([1, 1, 2, 4, 4]) + ([1, 1, 2, 4, 4], 0) + >>> count_inversions_recursive([]) + ([], 0) + """ + if len(arr) <= 1: + return arr, 0 + mid = len(arr) // 2 + p = arr[0:mid] + q = arr[mid:] + + a, inversion_p = count_inversions_recursive(p) + b, inversions_q = count_inversions_recursive(q) + c, cross_inversions = _count_cross_inversions(a, b) + + num_inversions = inversion_p + inversions_q + cross_inversions + return c, num_inversions + + +def _count_cross_inversions(p: list[int], q: list[int]) -> tuple[list[int], int]: + """ + Counts the inversions across two sorted arrays. + And combine the two arrays into one sorted array + For all 1<= i<=len(P) and for all 1 <= j <= len(Q), + if P[i] > Q[j], then (i, j) is a cross inversion + Parameters + ---------- + P: array-like, sorted in non-decreasing order + Q: array-like, sorted in non-decreasing order + Returns + ------ + R: array-like, a sorted array of the elements of `P` and `Q` + num_inversion: int, the number of inversions across `P` and `Q` + Examples + -------- + >>> _count_cross_inversions([1, 2, 3], [0, 2, 5]) + ([0, 1, 2, 2, 3, 5], 4) + >>> _count_cross_inversions([1, 2, 3], [3, 4, 5]) + ([1, 2, 3, 3, 4, 5], 0) + """ + + r = [] + i = j = num_inversion = 0 + while i < len(p) and j < len(q): + if p[i] > q[j]: + # if P[1] > Q[j], then P[k] > Q[k] for all i < k <= len(P) + # These are all inversions. The claim emerges from the + # property that P is sorted. + num_inversion += len(p) - i + r.append(q[j]) + j += 1 + else: + r.append(p[i]) + i += 1 + + if i < len(p): + r.extend(p[i:]) + else: + r.extend(q[j:]) + + return r, num_inversion + + +def main() -> None: + arr_1 = [10, 2, 1, 5, 5, 2, 11] + + # this arr has 8 inversions: + # (10, 2), (10, 1), (10, 5), (10, 5), (10, 2), (2, 1), (5, 2), (5, 2) + + num_inversions_bf = count_inversions_bf(arr_1) + _, num_inversions_recursive = count_inversions_recursive(arr_1) + + assert num_inversions_bf == num_inversions_recursive == 8 + + print("number of inversions = ", num_inversions_bf) + + # testing an array with zero inversion (a sorted arr_1) + + arr_1.sort() + num_inversions_bf = count_inversions_bf(arr_1) + _, num_inversions_recursive = count_inversions_recursive(arr_1) + + assert num_inversions_bf == num_inversions_recursive == 0 + print("number of inversions = ", num_inversions_bf) + + # an empty list should also have zero inversions + arr_1 = [] + num_inversions_bf = count_inversions_bf(arr_1) + _, num_inversions_recursive = count_inversions_recursive(arr_1) + + assert num_inversions_bf == num_inversions_recursive == 0 + print("number of inversions = ", num_inversions_bf) + + +if __name__ == "__main__": + main() diff --git a/divide_and_conquer/kth_order_statistic.py b/divide_and_conquer/kth_order_statistic.py new file mode 100644 index 000000000000..23fd8be5ea47 --- /dev/null +++ b/divide_and_conquer/kth_order_statistic.py @@ -0,0 +1,66 @@ +""" +Find the kth smallest element in linear time using divide and conquer. +Recall we can do this trivially in O(nlogn) time. Sort the list and +access kth element in constant time. + +This is a divide and conquer algorithm that can find a solution in O(n) time. + +For more information of this algorithm: +https://web.stanford.edu/class/archive/cs/cs161/cs161.1138/lectures/08/Small08.pdf +""" + +from __future__ import annotations + +from random import choice + + +def random_pivot(lst): + """ + Choose a random pivot for the list. + We can use a more sophisticated algorithm here, such as the median-of-medians + algorithm. + """ + return choice(lst) + + +def kth_number(lst: list[int], k: int) -> int: + """ + Return the kth smallest number in lst. + >>> kth_number([2, 1, 3, 4, 5], 3) + 3 + >>> kth_number([2, 1, 3, 4, 5], 1) + 1 + >>> kth_number([2, 1, 3, 4, 5], 5) + 5 + >>> kth_number([3, 2, 5, 6, 7, 8], 2) + 3 + >>> kth_number([25, 21, 98, 100, 76, 22, 43, 60, 89, 87], 4) + 43 + """ + # pick a pivot and separate into list based on pivot. + pivot = random_pivot(lst) + + # partition based on pivot + # linear time + small = [e for e in lst if e < pivot] + big = [e for e in lst if e > pivot] + + # if we get lucky, pivot might be the element we want. + # we can easily see this: + # small (elements smaller than k) + # + pivot (kth element) + # + big (elements larger than k) + if len(small) == k - 1: + return pivot + # pivot is in elements bigger than k + elif len(small) < k - 1: + return kth_number(big, k - len(small) - 1) + # pivot is in elements smaller than k + else: + return kth_number(small, k) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/divide_and_conquer/max_difference_pair.py b/divide_and_conquer/max_difference_pair.py new file mode 100644 index 000000000000..ffc4b76a7154 --- /dev/null +++ b/divide_and_conquer/max_difference_pair.py @@ -0,0 +1,44 @@ +def max_difference(a: list[int]) -> tuple[int, int]: + """ + We are given an array A[1..n] of integers, n >= 1. We want to + find a pair of indices (i, j) such that + 1 <= i <= j <= n and A[j] - A[i] is as large as possible. + + Explanation: + https://www.geeksforgeeks.org/maximum-difference-between-two-elements/ + + >>> max_difference([5, 11, 2, 1, 7, 9, 0, 7]) + (1, 9) + """ + # base case + if len(a) == 1: + return a[0], a[0] + else: + # split A into half. + first = a[: len(a) // 2] + second = a[len(a) // 2 :] + + # 2 sub problems, 1/2 of original size. + small1, big1 = max_difference(first) + small2, big2 = max_difference(second) + + # get min of first and max of second + # linear time + min_first = min(first) + max_second = max(second) + + # 3 cases, either (small1, big1), + # (min_first, max_second), (small2, big2) + # constant comparisons + if big2 - small2 > max_second - min_first and big2 - small2 > big1 - small1: + return small2, big2 + elif big1 - small1 > max_second - min_first: + return small1, big1 + else: + return min_first, max_second + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/divide_and_conquer/max_subarray.py b/divide_and_conquer/max_subarray.py new file mode 100644 index 000000000000..0fad7ab5d920 --- /dev/null +++ b/divide_and_conquer/max_subarray.py @@ -0,0 +1,113 @@ +""" +The maximum subarray problem is the task of finding the continuous subarray that has the +maximum sum within a given array of numbers. For example, given the array +[-2, 1, -3, 4, -1, 2, 1, -5, 4], the contiguous subarray with the maximum sum is +[4, -1, 2, 1], which has a sum of 6. + +This divide-and-conquer algorithm finds the maximum subarray in O(n log n) time. +""" + +from __future__ import annotations + +import time +from collections.abc import Sequence +from random import randint + +from matplotlib import pyplot as plt + + +def max_subarray( + arr: Sequence[float], low: int, high: int +) -> tuple[int | None, int | None, float]: + """ + Solves the maximum subarray problem using divide and conquer. + :param arr: the given array of numbers + :param low: the start index + :param high: the end index + :return: the start index of the maximum subarray, the end index of the + maximum subarray, and the maximum subarray sum + + >>> nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4] + >>> max_subarray(nums, 0, len(nums) - 1) + (3, 6, 6) + >>> nums = [2, 8, 9] + >>> max_subarray(nums, 0, len(nums) - 1) + (0, 2, 19) + >>> nums = [0, 0] + >>> max_subarray(nums, 0, len(nums) - 1) + (0, 0, 0) + >>> nums = [-1.0, 0.0, 1.0] + >>> max_subarray(nums, 0, len(nums) - 1) + (2, 2, 1.0) + >>> nums = [-2, -3, -1, -4, -6] + >>> max_subarray(nums, 0, len(nums) - 1) + (2, 2, -1) + >>> max_subarray([], 0, 0) + (None, None, 0) + """ + if not arr: + return None, None, 0 + if low == high: + return low, high, arr[low] + + mid = (low + high) // 2 + left_low, left_high, left_sum = max_subarray(arr, low, mid) + right_low, right_high, right_sum = max_subarray(arr, mid + 1, high) + cross_left, cross_right, cross_sum = max_cross_sum(arr, low, mid, high) + if left_sum >= right_sum and left_sum >= cross_sum: + return left_low, left_high, left_sum + elif right_sum >= left_sum and right_sum >= cross_sum: + return right_low, right_high, right_sum + return cross_left, cross_right, cross_sum + + +def max_cross_sum( + arr: Sequence[float], low: int, mid: int, high: int +) -> tuple[int, int, float]: + left_sum, max_left = float("-inf"), -1 + right_sum, max_right = float("-inf"), -1 + + summ: int | float = 0 + for i in range(mid, low - 1, -1): + summ += arr[i] + if summ > left_sum: + left_sum = summ + max_left = i + + summ = 0 + for i in range(mid + 1, high + 1): + summ += arr[i] + if summ > right_sum: + right_sum = summ + max_right = i + + return max_left, max_right, (left_sum + right_sum) + + +def time_max_subarray(input_size: int) -> float: + arr = [randint(1, input_size) for _ in range(input_size)] + start = time.time() + max_subarray(arr, 0, input_size - 1) + end = time.time() + return end - start + + +def plot_runtimes() -> None: + input_sizes = [10, 100, 1000, 10000, 50000, 100000, 200000, 300000, 400000, 500000] + runtimes = [time_max_subarray(input_size) for input_size in input_sizes] + print("No of Inputs\t\tTime Taken") + for input_size, runtime in zip(input_sizes, runtimes): + print(input_size, "\t\t", runtime) + plt.plot(input_sizes, runtimes) + plt.xlabel("Number of Inputs") + plt.ylabel("Time taken in seconds") + plt.show() + + +if __name__ == "__main__": + """ + A random simulation of this algorithm. + """ + from doctest import testmod + + testmod() diff --git a/divide_and_conquer/mergesort.py b/divide_and_conquer/mergesort.py new file mode 100644 index 000000000000..628080cefc9b --- /dev/null +++ b/divide_and_conquer/mergesort.py @@ -0,0 +1,112 @@ +from __future__ import annotations + + +def merge(left_half: list, right_half: list) -> list: + """Helper function for mergesort. + + >>> left_half = [-2] + >>> right_half = [-1] + >>> merge(left_half, right_half) + [-2, -1] + + >>> left_half = [1,2,3] + >>> right_half = [4,5,6] + >>> merge(left_half, right_half) + [1, 2, 3, 4, 5, 6] + + >>> left_half = [-2] + >>> right_half = [-1] + >>> merge(left_half, right_half) + [-2, -1] + + >>> left_half = [12, 15] + >>> right_half = [13, 14] + >>> merge(left_half, right_half) + [12, 13, 14, 15] + + >>> left_half = [] + >>> right_half = [] + >>> merge(left_half, right_half) + [] + """ + sorted_array = [None] * (len(right_half) + len(left_half)) + + pointer1 = 0 # pointer to current index for left Half + pointer2 = 0 # pointer to current index for the right Half + index = 0 # pointer to current index for the sorted array Half + + while pointer1 < len(left_half) and pointer2 < len(right_half): + if left_half[pointer1] < right_half[pointer2]: + sorted_array[index] = left_half[pointer1] + pointer1 += 1 + index += 1 + else: + sorted_array[index] = right_half[pointer2] + pointer2 += 1 + index += 1 + while pointer1 < len(left_half): + sorted_array[index] = left_half[pointer1] + pointer1 += 1 + index += 1 + + while pointer2 < len(right_half): + sorted_array[index] = right_half[pointer2] + pointer2 += 1 + index += 1 + + return sorted_array + + +def merge_sort(array: list) -> list: + """Returns a list of sorted array elements using merge sort. + + >>> from random import shuffle + >>> array = [-2, 3, -10, 11, 99, 100000, 100, -200] + >>> shuffle(array) + >>> merge_sort(array) + [-200, -10, -2, 3, 11, 99, 100, 100000] + + >>> shuffle(array) + >>> merge_sort(array) + [-200, -10, -2, 3, 11, 99, 100, 100000] + + >>> array = [-200] + >>> merge_sort(array) + [-200] + + >>> array = [-2, 3, -10, 11, 99, 100000, 100, -200] + >>> shuffle(array) + >>> sorted(array) == merge_sort(array) + True + + >>> array = [-2] + >>> merge_sort(array) + [-2] + + >>> array = [] + >>> merge_sort(array) + [] + + >>> array = [10000000, 1, -1111111111, 101111111112, 9000002] + >>> sorted(array) == merge_sort(array) + True + """ + if len(array) <= 1: + return array + # the actual formula to calculate the middle element = left + (right - left) // 2 + # this avoids integer overflow in case of large N + middle = 0 + (len(array) - 0) // 2 + + # Split the array into halves till the array length becomes equal to One + # merge the arrays of single length returned by mergeSort function and + # pass them into the merge arrays function which merges the array + left_half = array[:middle] + right_half = array[middle:] + + return merge(merge_sort(left_half), merge_sort(right_half)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/divide_and_conquer/minimum_element_of_array.py b/divide_and_conquer/minimum_element_of_array.py new file mode 100644 index 000000000000..b0f5ac03d031 --- /dev/null +++ b/divide_and_conquer/minimum_element_of_array.py @@ -0,0 +1,129 @@ +""" +Return the minimum element of an array using the +divide-and-conquer algorithm for selection sort. +Like quicksort, it partitions the input array recursively. +But unlike quicksort, +which recursively processes both sides of the partition, +this algorithm works on only one side of the partition. +The expected running time of this selection sort algorithm is 0(n), +assuming that the elements are distinct. +It returns the ith smallest element of the array A[p: r], where 1 ≤ i ≤ r-p+1. +(From Introduction to Algorithms, Fourth Edition, Cormen, 2022: Chapter 9.2) +""" + +from __future__ import annotations + +import random + + +def partition(array: list, starting_index: int, ending_index: int) -> int: + """ + Partition the array. + Args: + array: list of elements + starting_index: starting index of the array + ending_index: ending index of the array + + Returns: + index of the pivot + + >>> arr = [-2, 3, -10, 11, 99, 100000, 100, -200] + >>> partition(arr, 0, len(arr) - 1) + 0 + """ + pivot = array[ending_index] + i = starting_index - 1 + for j in range(starting_index, ending_index): + if array[j] <= pivot: + i += 1 + array[i], array[j] = array[j], array[i] + array[i + 1], array[ending_index] = array[ending_index], array[i + 1] + return i + 1 + + +def randomized_partition(array: list, starting_index: int, ending_index: int) -> int: + """ + Randomized partition of the array. + Args: + array: list of elements + starting_index: starting index of the array + ending_index: ending index of the array + + Returns: + call to partition function + + >>> arr = [-2, 3, -10, 11, 99, 100000, 100, -200] + >>> arr1 = randomized_partition(arr, 0, len(arr) - 1) + >>> arr == arr1 + False + """ + + rand_idx = random.randint(starting_index, ending_index) + array[rand_idx], array[ending_index] = array[ending_index], array[rand_idx] + return partition(array, starting_index, ending_index) + + +def selection_sort( + array: list, starting_index: int, ending_index: int, smallest_element: int +) -> list | None: + """ + Returns a list of sorted array elements using selection sort. + Using selection to find a minimum is O(n) overkill vs. a linear scan — the + value here is the DAC/partition demonstration. + + Args: + array: list of elements + starting_index: starting index of the array + ending_index: ending index of the array + smallest_element: the ith smallest element of + the array A[p: r], where 1 ≤ i ≤ r-p+1 + + Returns: + sorted array + + >>> from random import shuffle + >>> arr = [-2, 3, -10, 11, 99, 100000, 100, -200] + >>> shuffle(arr) + >>> selection_sort(arr, 0, len(arr) - 1, 1) + -200 + + >>> shuffle(arr) + >>> selection_sort(arr, 0, len(arr) - 1, 1) + -200 + + >>> arr = [-200] + >>> selection_sort(arr, 0, len(arr) - 1, 1) + -200 + + >>> arr = [-2] + >>> selection_sort(arr, 0, len(arr) - 1, 1) + -2 + + >>> arr = [] + >>> selection_sort(arr, 0, len(arr) - 1, 1) + [] + """ + + if not array: + return array + + if starting_index == ending_index: + # 1 <= i <= r - p + 1 when p == r means that i == 1 + return array[starting_index] + + q = randomized_partition(array, starting_index, ending_index) + + k = q - starting_index + 1 + + if smallest_element == k: + return array[q] # the pivot value is the answer + if smallest_element < k: + return selection_sort(array, starting_index, q - 1, smallest_element) + else: + return selection_sort(array, q + 1, ending_index, smallest_element - k) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/divide_and_conquer/peak.py b/divide_and_conquer/peak.py new file mode 100644 index 000000000000..71ab5ac86574 --- /dev/null +++ b/divide_and_conquer/peak.py @@ -0,0 +1,54 @@ +""" +Finding the peak of a unimodal list using divide and conquer. +A unimodal array is defined as follows: array is increasing up to index p, +then decreasing afterwards. (for p >= 1) +An obvious solution can be performed in O(n), +to find the maximum of the array. +(From Kleinberg and Tardos. Algorithm Design. +Addison Wesley 2006: Chapter 5 Solved Exercise 1) +""" + +from __future__ import annotations + + +def peak(lst: list[int]) -> int: + """ + Return the peak value of `lst`. + >>> peak([1, 2, 3, 4, 5, 4, 3, 2, 1]) + 5 + >>> peak([1, 10, 9, 8, 7, 6, 5, 4]) + 10 + >>> peak([1, 9, 8, 7]) + 9 + >>> peak([1, 2, 3, 4, 5, 6, 7, 0]) + 7 + >>> peak([1, 2, 3, 4, 3, 2, 1, 0, -1, -2]) + 4 + """ + # middle index + m = len(lst) // 2 + + # choose the middle 3 elements + three = lst[m - 1 : m + 2] + + # if middle element is peak + if three[1] > three[0] and three[1] > three[2]: + return three[1] + + # if increasing, recurse on right + elif three[0] < three[2]: + if len(lst[:m]) == 2: + m -= 1 + return peak(lst[m:]) + + # decreasing + else: + if len(lst[:m]) == 2: + m += 1 + return peak(lst[:m]) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/divide_and_conquer/power.py b/divide_and_conquer/power.py new file mode 100644 index 000000000000..492ee6dd12f0 --- /dev/null +++ b/divide_and_conquer/power.py @@ -0,0 +1,53 @@ +def actual_power(a: int, b: int) -> int: + """ + Function using divide and conquer to calculate a^b. + It only works for integer a,b. + + :param a: The base of the power operation, an integer. + :param b: The exponent of the power operation, a non-negative integer. + :return: The result of a^b. + + Examples: + >>> actual_power(3, 2) + 9 + >>> actual_power(5, 3) + 125 + >>> actual_power(2, 5) + 32 + >>> actual_power(7, 0) + 1 + """ + if b == 0: + return 1 + half = actual_power(a, b // 2) + + if (b % 2) == 0: + return half * half + else: + return a * half * half + + +def power(a: int, b: int) -> float: + """ + :param a: The base (integer). + :param b: The exponent (integer). + :return: The result of a^b, as a float for negative exponents. + + >>> power(4,6) + 4096 + >>> power(2,3) + 8 + >>> power(-2,3) + -8 + >>> power(2,-3) + 0.125 + >>> power(-2,-3) + -0.125 + """ + if b < 0: + return 1 / actual_power(a, -b) + return actual_power(a, b) + + +if __name__ == "__main__": + print(power(-2, -3)) # output -0.125 diff --git a/divide_and_conquer/strassen_matrix_multiplication.py b/divide_and_conquer/strassen_matrix_multiplication.py new file mode 100644 index 000000000000..8b0ab3b7ded1 --- /dev/null +++ b/divide_and_conquer/strassen_matrix_multiplication.py @@ -0,0 +1,225 @@ +""" +https://en.wikipedia.org/wiki/Strassen_algorithm +""" + +from __future__ import annotations + +import math + + +def default_matrix_multiplication(a: list, b: list) -> list: + """ + Multiplication only for 2x2 matrices + """ + if len(a) != 2 or len(a[0]) != 2 or len(b) != 2 or len(b[0]) != 2: + raise Exception("Matrices are not 2x2") + new_matrix = [ + [a[0][0] * b[0][0] + a[0][1] * b[1][0], a[0][0] * b[0][1] + a[0][1] * b[1][1]], + [a[1][0] * b[0][0] + a[1][1] * b[1][0], a[1][0] * b[0][1] + a[1][1] * b[1][1]], + ] + return new_matrix + + +def matrix_addition(matrix_a: list, matrix_b: list): + return [ + [matrix_a[row][col] + matrix_b[row][col] for col in range(len(matrix_a[row]))] + for row in range(len(matrix_a)) + ] + + +def matrix_subtraction(matrix_a: list, matrix_b: list): + return [ + [matrix_a[row][col] - matrix_b[row][col] for col in range(len(matrix_a[row]))] + for row in range(len(matrix_a)) + ] + + +def split_matrix(a: list) -> tuple[list, list, list, list]: + """ + Given an even-length matrix, returns the top_left, top_right, bot_left, bot_right + quadrant. + + >>> split_matrix([[4,3,2,4],[2,3,1,1],[6,5,4,3],[8,4,1,6]]) + ([[4, 3], [2, 3]], [[2, 4], [1, 1]], [[6, 5], [8, 4]], [[4, 3], [1, 6]]) + >>> split_matrix([ + ... [4,3,2,4,4,3,2,4],[2,3,1,1,2,3,1,1],[6,5,4,3,6,5,4,3],[8,4,1,6,8,4,1,6], + ... [4,3,2,4,4,3,2,4],[2,3,1,1,2,3,1,1],[6,5,4,3,6,5,4,3],[8,4,1,6,8,4,1,6] + ... ]) # doctest: +NORMALIZE_WHITESPACE + ([[4, 3, 2, 4], [2, 3, 1, 1], [6, 5, 4, 3], [8, 4, 1, 6]], [[4, 3, 2, 4], + [2, 3, 1, 1], [6, 5, 4, 3], [8, 4, 1, 6]], [[4, 3, 2, 4], [2, 3, 1, 1], + [6, 5, 4, 3], [8, 4, 1, 6]], [[4, 3, 2, 4], [2, 3, 1, 1], [6, 5, 4, 3], + [8, 4, 1, 6]]) + """ + if len(a) % 2 != 0 or len(a[0]) % 2 != 0: + raise Exception("Odd matrices are not supported!") + + def extract_submatrix(rows, cols): + return [[a[i][j] for j in cols] for i in rows] + + mid = len(a) // 2 + + rows_top, rows_bot = range(mid), range(mid, len(a)) + cols_left, cols_right = range(mid), range(mid, len(a)) + + return ( + extract_submatrix(rows_top, cols_left), # Top-left + extract_submatrix(rows_top, cols_right), # Top-right + extract_submatrix(rows_bot, cols_left), # Bottom-left + extract_submatrix(rows_bot, cols_right), # Bottom-right + ) + + +def matrix_dimensions(matrix: list) -> tuple[int, int]: + return len(matrix), len(matrix[0]) + + +def print_matrix(matrix: list) -> None: + print("\n".join(str(line) for line in matrix)) + + +def actual_strassen(matrix_a: list, matrix_b: list) -> list: + """ + Recursive function to calculate the product of two matrices, using the Strassen + Algorithm. + + Time complexity: + The recurrence is T(n) = 7 T(n/2) + \u0398(n^2), which solves to + T(n) = \u0398(n^{log_2 7}) \u2248 \u0398(n^{2.8074}). This is asymptotically + faster than the naive \u0398(n^3) algorithm for sufficiently large n. + + Space complexity: + Uses additional memory for temporary submatrices and padding; overall + space complexity is O(n^2). + + Notes: + This function expects square matrices whose size is a power of two. + Matrices of other sizes are handled by `strassen` which pads to the + next power of two. + + It only supports square matrices of any size that is a power of 2. + + Strassen's algorithm reduces the number of recursive multiplications needed to + multiply two n x n matrices from the 8 required by the naive divide-and-conquer + approach down to 7, at the cost of a few extra matrix additions/subtractions + (which are cheaper, O(n^2), operations). Each matrix is split into four + (n/2) x (n/2) quadrants; 7 products of quadrant combinations are computed + recursively, and those products are combined with additions/subtractions to + form the four quadrants of the result. + + Time complexity: O(n^log2(7)) ~= O(n^2.807), an improvement over the O(n^3) of + the standard/naive matrix multiplication algorithm. + Space complexity: O(n^2) for storing the intermediate quadrant matrices, plus + O(log n) recursion stack depth. + """ + if matrix_dimensions(matrix_a) == (2, 2): + return default_matrix_multiplication(matrix_a, matrix_b) + + a, b, c, d = split_matrix(matrix_a) + e, f, g, h = split_matrix(matrix_b) + + t1 = actual_strassen(a, matrix_subtraction(f, h)) + t2 = actual_strassen(matrix_addition(a, b), h) + t3 = actual_strassen(matrix_addition(c, d), e) + t4 = actual_strassen(d, matrix_subtraction(g, e)) + t5 = actual_strassen(matrix_addition(a, d), matrix_addition(e, h)) + t6 = actual_strassen(matrix_subtraction(b, d), matrix_addition(g, h)) + t7 = actual_strassen(matrix_subtraction(a, c), matrix_addition(e, f)) + + top_left = matrix_addition(matrix_subtraction(matrix_addition(t5, t4), t2), t6) + top_right = matrix_addition(t1, t2) + bot_left = matrix_addition(t3, t4) + bot_right = matrix_subtraction(matrix_subtraction(matrix_addition(t1, t5), t3), t7) + + # construct the new matrix from our 4 quadrants + new_matrix = [] + for i in range(len(top_right)): + new_matrix.append(top_left[i] + top_right[i]) + for i in range(len(bot_right)): + new_matrix.append(bot_left[i] + bot_right[i]) + return new_matrix + + +def strassen(matrix1: list, matrix2: list) -> list: + """ + Multiply two matrices using Strassen's divide-and-conquer algorithm. + + Time complexity: + \u0398(n^{log_2 7}) \u2248 \u0398(n^{2.8074}) + (recurrence T(n) = 7 T(n/2) + \u0398(n^2)). + + Space complexity: + O(n^2) due to padding and temporary matrices used during recursion. + + Multiply two matrices using Strassen's algorithm, which runs in + O(n^log2(7)) ~= O(n^2.807) time, compared to O(n^3) for naive matrix + multiplication. This implementation pads both input matrices with zeros + until they are square matrices whose dimension is a power of 2 (required + by the divide-and-conquer recursion in actual_strassen), performs the + multiplication, then trims the padding back off the result. + + Examples: + + >>> strassen([[2,1,3],[3,4,6],[1,4,2],[7,6,7]], [[4,2,3,4],[2,1,1,1],[8,6,4,2]]) + [[34, 23, 19, 15], [68, 46, 37, 28], [28, 18, 15, 12], [96, 62, 55, 48]] + >>> strassen([[3,7,5,6,9],[1,5,3,7,8],[1,4,4,5,7]], [[2,4],[5,2],[1,7],[5,5],[7,8]]) + [[139, 163], [121, 134], [100, 121]] + """ + if matrix_dimensions(matrix1)[1] != matrix_dimensions(matrix2)[0]: + msg = ( + "Unable to multiply these matrices, please check the dimensions.\n" + f"Matrix A: {matrix1}\n" + f"Matrix B: {matrix2}" + ) + raise Exception(msg) + dimension1 = matrix_dimensions(matrix1) + dimension2 = matrix_dimensions(matrix2) + + if dimension1[0] == dimension1[1] and dimension2[0] == dimension2[1]: + return [matrix1, matrix2] + + maximum = max(*dimension1, *dimension2) + maxim = int(math.pow(2, math.ceil(math.log2(maximum)))) + new_matrix1 = matrix1 + new_matrix2 = matrix2 + + # Adding zeros to the matrices to convert them both into square matrices of equal + # dimensions that are a power of 2 + for i in range(maxim): + if i < dimension1[0]: + for _ in range(dimension1[1], maxim): + new_matrix1[i].append(0) + else: + new_matrix1.append([0] * maxim) + if i < dimension2[0]: + for _ in range(dimension2[1], maxim): + new_matrix2[i].append(0) + else: + new_matrix2.append([0] * maxim) + + final_matrix = actual_strassen(new_matrix1, new_matrix2) + + # Removing the additional zeros + for i in range(maxim): + if i < dimension1[0]: + for _ in range(dimension2[1], maxim): + final_matrix[i].pop() + else: + final_matrix.pop() + return final_matrix + + +if __name__ == "__main__": + matrix1 = [ + [2, 3, 4, 5], + [6, 4, 3, 1], + [2, 3, 6, 7], + [3, 1, 2, 4], + [2, 3, 4, 5], + [6, 4, 3, 1], + [2, 3, 6, 7], + [3, 1, 2, 4], + [2, 3, 4, 5], + [6, 2, 3, 1], + ] + matrix2 = [[0, 2, 1, 1], [16, 2, 3, 3], [2, 2, 7, 7], [13, 11, 22, 4]] + print(strassen(matrix1, matrix2)) diff --git a/divide_and_conquer/tests/__init__.py b/divide_and_conquer/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/divide_and_conquer/tests/test_strassen_matrix_multiplication.py b/divide_and_conquer/tests/test_strassen_matrix_multiplication.py new file mode 100644 index 000000000000..d3ed399adfbd --- /dev/null +++ b/divide_and_conquer/tests/test_strassen_matrix_multiplication.py @@ -0,0 +1,47 @@ +import pytest + +from divide_and_conquer.strassen_matrix_multiplication import split_matrix + + +def test_4x4_matrix(): + matrix = [[4, 3, 2, 4], [2, 3, 1, 1], [6, 5, 4, 3], [8, 4, 1, 6]] + expected = ([[4, 3], [2, 3]], [[2, 4], [1, 1]], [[6, 5], [8, 4]], [[4, 3], [1, 6]]) + assert split_matrix(matrix) == expected + + +def test_8x8_matrix(): + matrix = [ + [4, 3, 2, 4, 4, 3, 2, 4], + [2, 3, 1, 1, 2, 3, 1, 1], + [6, 5, 4, 3, 6, 5, 4, 3], + [8, 4, 1, 6, 8, 4, 1, 6], + [4, 3, 2, 4, 4, 3, 2, 4], + [2, 3, 1, 1, 2, 3, 1, 1], + [6, 5, 4, 3, 6, 5, 4, 3], + [8, 4, 1, 6, 8, 4, 1, 6], + ] + expected = ( + [[4, 3, 2, 4], [2, 3, 1, 1], [6, 5, 4, 3], [8, 4, 1, 6]], + [[4, 3, 2, 4], [2, 3, 1, 1], [6, 5, 4, 3], [8, 4, 1, 6]], + [[4, 3, 2, 4], [2, 3, 1, 1], [6, 5, 4, 3], [8, 4, 1, 6]], + [[4, 3, 2, 4], [2, 3, 1, 1], [6, 5, 4, 3], [8, 4, 1, 6]], + ) + assert split_matrix(matrix) == expected + + +def test_invalid_odd_matrix(): + matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + with pytest.raises(Exception, match="Odd matrices are not supported!"): + split_matrix(matrix) + + +def test_invalid_non_square_matrix(): + matrix = [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + [13, 14, 15, 16], + [17, 18, 19, 20], + ] + with pytest.raises(Exception, match="Odd matrices are not supported!"): + split_matrix(matrix) diff --git a/docs/__init__.py b/docs/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000000..5e3653860b8a --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,102 @@ +# Configuration file for the Sphinx documentation builder. +# https://www.sphinx-doc.org/en/master/usage/configuration.html +# +# Project metadata (name/version/authors) is single-sourced from pyproject.toml +# so it never drifts from the packaging metadata. +import tomllib +from pathlib import Path + +_pyproject = tomllib.loads( + (Path(__file__).parent.parent / "pyproject.toml").read_text(encoding="utf-8") +) +_project = _pyproject["project"] + +project = _project["name"] +author = ", ".join(a["name"] for a in _project.get("authors", [])) +release = version = _project["version"] +copyright = "2014, TheAlgorithms" # noqa: A001 # Sphinx requires this exact name + +extensions = [ + "autoapi.extension", + "myst_parser", +] + +autoapi_dirs = [ + "audio_filters", + "backtracking", + "bit_manipulation", + "blockchain", + "boolean_algebra", + "cellular_automata", + "ciphers", + "computer_vision", + "conversions", + "data_compression", + "data_structures", + "digital_image_processing", + "divide_and_conquer", + "dynamic_programming", + "electronics", + "file_transfer", + "financial", + "fractals", + "fuzzy_logic", + "genetic_algorithm", + "geodesy", + "geometry", + "graphics", + "graphs", + "greedy_methods", + "hashes", + "knapsack", + "linear_algebra", + "linear_programming", + "machine_learning", + "maths", + "matrix", + "networking_flow", + "neural_network", + "other", + "physics", + "project_euler", + "quantum", + "scheduling", + "searches", + "sorts", + "strings", + "web_programming", +] +autoapi_member_order = "groupwise" + +exclude_patterns = [ + ".*/*", + "docs/", +] + +html_theme = "alabaster" +html_static_path = ["_static"] +templates_path = ["_templates"] + +source_suffix = { + ".rst": "restructuredtext", + ".md": "markdown", +} + +myst_enable_extensions = [ + "amsmath", + "attrs_inline", + "colon_fence", + "deflist", + "dollarmath", + "fieldlist", + "html_admonition", + "html_image", + "replacements", + "smartquotes", + "strikethrough", + "substitution", + "tasklist", +] +myst_fence_as_directive = [ + "include", +] diff --git a/docs/hacktober_2026_prep.md b/docs/hacktober_2026_prep.md new file mode 100644 index 000000000000..f89bdcc9daf3 --- /dev/null +++ b/docs/hacktober_2026_prep.md @@ -0,0 +1,2076 @@ +# Hacktoberfest 2026 preparation — open PR cleanup tracker + +This file tracks the cleanup of open pull requests ahead of [Hacktoberfest 2026](https://hacktoberfest.com) so the queue is ready for the October influx of contributions. + +Each of the `scripts/close_pull_requests_with_*.sh` jobs is run one at a time by a maintainer. After each run, the affected lines below are checked off (`[ ]` -> `[x]`), their labels replaced with "closed by + +Below a Django function for the views.py file contains a login form for demonstrating +recaptcha verification. +""" + +# /// script +# requires-python = ">=3.13" +# dependencies = [ +# "httpx2", +# ] +# /// + +import httpx2 + +try: + from django.contrib.auth import authenticate, login + from django.http import HttpResponse + from django.shortcuts import redirect, render +except ImportError: + authenticate = login = render = redirect = print + HttpResponse = None + + +def login_using_recaptcha(request) -> HttpResponse: + # Enter your recaptcha secret key here + secret_key = "secretKey" # noqa: S105 + url = "https://www.google.com/recaptcha/api/siteverify" + + # when method is not POST, direct user to login page + if request.method != "POST": + return render(request, "login.html") + + # from the frontend, get username, password, and client_key + username = request.POST.get("username") + password = request.POST.get("password") + client_key = request.POST.get("g-recaptcha-response") + + # post recaptcha response to Google's recaptcha api + response = httpx2.post( + url, data={"secret": secret_key, "response": client_key}, timeout=10 + ) + # if the recaptcha api verified our keys + if response.json().get("success", False): + # authenticate the user + user_in_database = authenticate(request, username=username, password=password) + if user_in_database: + login(request, user_in_database) + return redirect("/your-webpage") + return render(request, "login.html") diff --git a/web_programming/reddit.py b/web_programming/reddit.py new file mode 100644 index 000000000000..e68db596174e --- /dev/null +++ b/web_programming/reddit.py @@ -0,0 +1,65 @@ +# /// script +# requires-python = ">=3.13" +# dependencies = [ +# "httpx2", +# ] +# /// + +from typing import Literal + +import httpx2 + +valid_terms = set( + """approved_at_utc approved_by author_flair_background_color +author_flair_css_class author_flair_richtext author_flair_template_id author_fullname +author_premium can_mod_post category clicked content_categories created_utc downs +edited gilded gildings hidden hide_score is_created_from_ads_ui is_meta +is_original_content is_reddit_media_domain is_video link_flair_css_class +link_flair_richtext link_flair_text link_flair_text_color media_embed mod_reason_title +name permalink pwls quarantine saved score secure_media secure_media_embed selftext +subreddit subreddit_name_prefixed subreddit_type thumbnail title top_awarded_type +total_awards_received ups upvote_ratio url user_reports""".split() +) + + +def get_subreddit_data( + subreddit: str, + limit: int = 1, + age: Literal["new", "top", "hot"] = "new", + wanted_data: list | None = None, +) -> dict: + """ + subreddit : Subreddit to query + limit : Number of posts to fetch + age : ["new", "top", "hot"] + wanted_data : Get only the required data in the list + """ + wanted_data = wanted_data or [] + if invalid_search_terms := ", ".join(sorted(set(wanted_data) - valid_terms)): + msg = f"Invalid search term: {invalid_search_terms}" + raise ValueError(msg) + # raise_for_status() already raises httpx2.HTTPStatusError for any 4xx/5xx + # response (including 429), so no extra status check is needed here. + data = ( + httpx2.get( + f"https://www.reddit.com/r/{subreddit}/{age}.json?limit={limit}", + headers={"User-agent": "A random string"}, + timeout=10, + ) + .raise_for_status() + .json() + ) + if not wanted_data: + return {id_: data["data"]["children"][id_] for id_ in range(limit)} + + data_dict = {} + for id_ in range(limit): + data_dict[id_] = { + item: data["data"]["children"][id_]["data"][item] for item in wanted_data + } + return data_dict + + +if __name__ == "__main__": + # If you get Error 429, that means you are rate limited.Try after some time + print(get_subreddit_data("learnpython", wanted_data=["title", "url", "selftext"])) diff --git a/web_programming/search_books_by_isbn.py b/web_programming/search_books_by_isbn.py new file mode 100644 index 000000000000..a4bf6de267a4 --- /dev/null +++ b/web_programming/search_books_by_isbn.py @@ -0,0 +1,82 @@ +""" +Get book and author data from https://openlibrary.org + +ISBN: https://en.wikipedia.org/wiki/International_Standard_Book_Number +""" + +# /// script +# requires-python = ">=3.13" +# dependencies = [ +# "httpx2", +# ] +# /// + +from json import JSONDecodeError + +import httpx2 + + +def get_openlibrary_data(olid: str = "isbn/0140328726") -> dict: + """ + Given an 'isbn/0140328726', return book data from Open Library as a Python dict. + Given an '/authors/OL34184A', return authors data as a Python dict. + This code must work for olids with or without a leading slash ('/'). + + # Comment out doctests if they take too long or have results that may change + # >>> get_openlibrary_data(olid='isbn/0140328726') # doctest: +ELLIPSIS + {'publishers': ['Puffin'], 'number_of_pages': 96, 'isbn_10': ['0140328726'], ... + # >>> get_openlibrary_data(olid='/authors/OL7353617A') # doctest: +ELLIPSIS + {'name': 'Adrian Brisku', 'created': {'type': '/type/datetime', ... + """ + new_olid = olid.strip().strip("/") # Remove leading/trailing whitespace & slashes + if new_olid.count("/") != 1: + msg = f"{olid} is not a valid Open Library olid" + raise ValueError(msg) + return httpx2.get( + f"https://openlibrary.org/{new_olid}.json", timeout=10, follow_redirects=True + ).json() + + +def summarize_book(ol_book_data: dict) -> dict: + """ + Given Open Library book data, return a summary as a Python dict. + """ + desired_keys = { + "title": "Title", + "publish_date": "Publish date", + "authors": "Authors", + "number_of_pages": "Number of pages", + "isbn_10": "ISBN (10)", + "isbn_13": "ISBN (13)", + } + data = {better_key: ol_book_data[key] for key, better_key in desired_keys.items()} + data["Authors"] = [ + get_openlibrary_data(author["key"])["name"] for author in data["Authors"] + ] + for key, value in data.items(): + if isinstance(value, list): + data[key] = ", ".join(value) + return data + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + while True: + isbn = input("\nEnter the ISBN code to search (or 'quit' to stop): ").strip() + if isbn.lower() in ("", "q", "quit", "exit", "stop"): + break + + if len(isbn) not in (10, 13) or not isbn.isdigit(): + print(f"Sorry, {isbn} is not a valid ISBN. Please, input a valid ISBN.") + continue + + print(f"\nSearching Open Library for ISBN: {isbn}...\n") + + try: + book_summary = summarize_book(get_openlibrary_data(f"isbn/{isbn}")) + print("\n".join(f"{key}: {value}" for key, value in book_summary.items())) + except JSONDecodeError: + print(f"Sorry, there are no results for ISBN: {isbn}.") diff --git a/web_programming/slack_message.py b/web_programming/slack_message.py new file mode 100644 index 000000000000..9b9fa4144f43 --- /dev/null +++ b/web_programming/slack_message.py @@ -0,0 +1,29 @@ +# Created by sarathkaul on 12/11/19 + +# /// script +# requires-python = ">=3.13" +# dependencies = [ +# "httpx2", +# ] +# /// + +import httpx2 + + +def send_slack_message(message_body: str, slack_url: str) -> None: + headers = {"Content-Type": "application/json"} + response = httpx2.post( + slack_url, json={"text": message_body}, headers=headers, timeout=10 + ) + if response.status_code != 200: + msg = ( + "Request to slack returned an error " + f"{response.status_code}, the response is:\n{response.text}" + ) + raise ValueError(msg) + + +if __name__ == "__main__": + # Set the slack url to the one provided by Slack when you create the webhook at + # https://my.slack.com/services/new/incoming-webhook/ + send_slack_message("", "") diff --git a/web_programming/test_fetch_github_info.py b/web_programming/test_fetch_github_info.py new file mode 100644 index 000000000000..fbbb56359292 --- /dev/null +++ b/web_programming/test_fetch_github_info.py @@ -0,0 +1,28 @@ +import json +from typing import Any + +import httpx2 + +from .fetch_github_info import AUTHENTICATED_USER_ENDPOINT, fetch_github_info + + +def test_fetch_github_info(monkeypatch) -> None: + class FakeResponse: + def __init__(self, content) -> None: + assert isinstance(content, (bytes, str)) + self.content = content + + def json(self) -> Any: # noqa: ANN401 + return json.loads(self.content) + + def mock_response(*args, **kwargs) -> FakeResponse: # noqa: ANN002,ANN003 + assert args[0] == AUTHENTICATED_USER_ENDPOINT + assert "Authorization" in kwargs["headers"] + assert kwargs["headers"]["Authorization"].startswith("token ") + assert "Accept" in kwargs["headers"] + return FakeResponse(b'{"login":"test","id":1}') + + monkeypatch.setattr(httpx2, "get", mock_response) + result = fetch_github_info("token") + assert result["login"] == "test" + assert result["id"] == 1 diff --git a/web_programming/world_covid19_stats.py b/web_programming/world_covid19_stats.py new file mode 100644 index 000000000000..057931cd65e5 --- /dev/null +++ b/web_programming/world_covid19_stats.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +""" +Provide the current worldwide COVID-19 statistics. +This data is being scrapped from 'https://www.worldometers.info/coronavirus/'. +""" + +# /// script +# requires-python = ">=3.13" +# dependencies = [ +# "beautifulsoup4", +# "httpx2", +# ] +# /// + +import httpx2 +from bs4 import BeautifulSoup + + +def world_covid19_stats( + url: str = "https://www.worldometers.info/coronavirus/", +) -> dict: + """ + Return a dict of current worldwide COVID-19 statistics + """ + soup = BeautifulSoup( + httpx2.get(url, timeout=10, follow_redirects=True).text, "html.parser" + ) + keys = soup.find_all("h1") + values = soup.find_all("div", {"class": "maincounter-number"}) + keys += soup.find_all("span", {"class": "panel-title"}) + values += soup.find_all("div", {"class": "number-table-main"}) + return {key.text.strip(): value.text.strip() for key, value in zip(keys, values)} + + +if __name__ == "__main__": + print("\033[1m COVID-19 Status of the World \033[0m\n") + print("\n".join(f"{key}\n{value}" for key, value in world_covid19_stats().items()))