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/.travis.yml b/.travis.yml deleted file mode 100644 index 5fba6987bb66..000000000000 --- a/.travis.yml +++ /dev/null @@ -1,26 +0,0 @@ -language: python -cache: pip -python: - - 2.7 - - 3.6 - #- nightly - #- pypy - #- pypy3 -matrix: - allow_failures: - - python: nightly - - python: pypy - - python: pypy3 -install: - #- pip install -r requirements.txt - - pip install flake8 # pytest # add another testing frameworks later -before_script: - # stop the build if there are Python syntax errors or undefined names - - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics -script: - - true # pytest --capture=sys # add other tests here -notifications: - on_success: change - on_failure: change # `always` will be the setting once code changes slow down 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/ArithmeticAnalysis/Bisection.py b/ArithmeticAnalysis/Bisection.py deleted file mode 100644 index a6af547db03b..000000000000 --- a/ArithmeticAnalysis/Bisection.py +++ /dev/null @@ -1,33 +0,0 @@ -import math - - -def bisection(function, a, b): # finds where the function becomes 0 in [a,b] using bolzano - - start = a - end = b - if function(a) == 0: # one of the a or b is a root for the function - return a - elif function(b) == 0: - return b - elif function(a) * function(b) > 0: # if none of these are root and they are both positive or negative, - # then his algorithm can't find the root - print("couldn't find root in [a,b]") - return - else: - mid = (start + end) / 2 - while abs(start - mid) > 0.0000001: # until we achieve precise equals to 10^-7 - if function(mid) == 0: - return mid - elif function(mid) * function(start) < 0: - end = mid - else: - start = mid - mid = (start + end) / 2 - return mid - - -def f(x): - return math.pow(x, 3) - 2*x - 5 - - -print(bisection(f, 1, 1000)) diff --git a/ArithmeticAnalysis/Intersection.py b/ArithmeticAnalysis/Intersection.py deleted file mode 100644 index 22c50f2ecafd..000000000000 --- a/ArithmeticAnalysis/Intersection.py +++ /dev/null @@ -1,16 +0,0 @@ -import math - -def intersection(function,x0,x1): #function is the f we want to find its root and x0 and x1 are two random starting points - x_n = x0 - x_n1 = x1 - while True: - x_n2 = x_n1-(function(x_n1)/((function(x_n1)-function(x_n))/(x_n1-x_n))) - if abs(x_n2 - x_n1)<0.00001 : - return x_n2 - x_n=x_n1 - x_n1=x_n2 - -def f(x): - return math.pow(x,3)-2*x-5 - -print(intersection(f,3,3.5)) diff --git a/ArithmeticAnalysis/LUdecomposition.py b/ArithmeticAnalysis/LUdecomposition.py deleted file mode 100644 index bb0827bb39b1..000000000000 --- a/ArithmeticAnalysis/LUdecomposition.py +++ /dev/null @@ -1,34 +0,0 @@ -import math -import numpy - -def LUDecompose (table): #table that contains our data - #table has to be a square array so we need to check first - rows,columns=numpy.shape(table) - L=numpy.zeros((rows,columns)) - U=numpy.zeros((rows,columns)) - if rows!=columns: - return - for i in range (columns): - for j in range(i-1): - sum=0 - for k in range (j-1): - sum+=L[i][k]*U[k][j] - L[i][j]=(table[i][j]-sum)/U[j][j] - L[i][i]=1 - for j in range(i-1,columns): - sum1=0 - for k in range(i-1): - sum1+=L[i][k]*U[k][j] - U[i][j]=table[i][j]-sum1 - return L,U - - - - - - - -matrix =numpy.array([[2,-2,1],[0,1,2],[5,3,1]]) -L,U = LUDecompose(matrix) -print(L) -print(U) \ No newline at end of file diff --git a/ArithmeticAnalysis/NeutonMethod.py b/ArithmeticAnalysis/NeutonMethod.py deleted file mode 100644 index c3d5efb47d01..000000000000 --- a/ArithmeticAnalysis/NeutonMethod.py +++ /dev/null @@ -1,15 +0,0 @@ -def newton(function,function1,startingInt): #function is the f(x) and function1 is the f'(x) - x_n=startingInt - while True: - x_n1=x_n-function(x_n)/function1(x_n) - if abs(x_n-x_n1)<0.00001: - return x_n1 - x_n=x_n1 - -def f(x): - return (x**3)-2*x-5 - -def f1(x): - return 3*(x**2)-2 - -print(newton(f,f1,3)) diff --git a/ArithmeticAnalysis/NewtonRaphsonMethod.py b/ArithmeticAnalysis/NewtonRaphsonMethod.py deleted file mode 100644 index 40501134e28a..000000000000 --- a/ArithmeticAnalysis/NewtonRaphsonMethod.py +++ /dev/null @@ -1,38 +0,0 @@ -# Implementing Newton Raphson method in python -# Author: Haseeb - -from sympy import diff -from decimal import Decimal -from math import sin, cos, exp - -def NewtonRaphson(func, a): - ''' Finds root from the point 'a' onwards by Newton-Raphson method ''' - while True: - x = a - c = Decimal(a) - ( Decimal(eval(func)) / Decimal(eval(str(diff(func)))) ) - - x = c - a = c - # This number dictates the accuracy of the answer - if abs(eval(func)) < 10**-15: - return c - - -# Let's Execute -if __name__ == '__main__': - # Find root of trignometric fucntion - # Find value of pi - print ('sin(x) = 0', NewtonRaphson('sin(x)', 2)) - - # Find root of polynomial - print ('x**2 - 5*x +2 = 0', NewtonRaphson('x**2 - 5*x +2', 0.4)) - - # Find Square Root of 5 - print ('x**2 - 5 = 0', NewtonRaphson('x**2 - 5', 0.1)) - - # Exponential Roots - print ('exp(x) - 1 = 0', NewtonRaphson('exp(x) - 1', 0)) - - - - 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..52b15110f236 --- /dev/null +++ b/DIRECTORY.md @@ -0,0 +1,1686 @@ +## 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) + * [Keith Number](maths/keith_number.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) + * [Weighted Interval Scheduling](scheduling/weighted_interval_scheduling.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 Find And Fix Pep798](tests/test_find_and_fix_pep798.py) + * [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/File_Transfer_Protocol/ftp_client_server.py b/File_Transfer_Protocol/ftp_client_server.py deleted file mode 100644 index f73f858431f3..000000000000 --- a/File_Transfer_Protocol/ftp_client_server.py +++ /dev/null @@ -1,58 +0,0 @@ -# server - -import socket # Import socket module - -port = 60000 # Reserve a port for your service. -s = socket.socket() # Create a socket object -host = socket.gethostname() # Get local machine name -s.bind((host, port)) # Bind to the port -s.listen(5) # Now wait for client connection. - -print('Server listening....') - -while True: - conn, addr = s.accept() # Establish connection with client. - print('Got connection from', addr) - data = conn.recv(1024) - print('Server received', repr(data)) - - filename = 'mytext.txt' - f = open(filename, 'rb') - in_data = f.read(1024) - while (in_data): - conn.send(in_data) - print('Sent ', repr(in_data)) - in_data = f.read(1024) - f.close() - - print('Done sending') - conn.send('Thank you for connecting') - conn.close() - - -# client side server - -import socket # Import socket module - -s = socket.socket() # Create a socket object -host = socket.gethostname() # Get local machine name -port = 60000 # Reserve a port for your service. - -s.connect((host, port)) -s.send("Hello server!") - -with open('received_file', 'wb') as f: - print('file opened') - while True: - print('receiving data...') - data = s.recv(1024) - print('data=%s', (data)) - if not data: - break - # write data to a file - f.write(data) - -f.close() -print('Successfully get the file') -s.close() -print('connection closed') diff --git a/File_Transfer_Protocol/ftp_send_receive.py b/File_Transfer_Protocol/ftp_send_receive.py deleted file mode 100644 index d4919158a02e..000000000000 --- a/File_Transfer_Protocol/ftp_send_receive.py +++ /dev/null @@ -1,36 +0,0 @@ -""" - File transfer protocol used to send and receive files using FTP server. - Use credentials to provide access to the FTP client - - Note: Do not use root username & password for security reasons - Create a seperate user and provide access to a home directory of the user - Use login id and password of the user created - cwd here stands for current working directory -""" - -from ftplib import FTP -ftp = FTP('xxx.xxx.x.x') # Enter the ip address or the domain name here -ftp.login(user='username', passwd='password') -ftp.cwd('/Enter the directory here/') - -""" - The file which will be received via the FTP server - Enter the location of the file where the file is received -""" - -def ReceiveFile(): - FileName = 'example.txt' """ Enter the location of the file """ - LocalFile = open(FileName, 'wb') - ftp.retrbinary('RETR ' + FileName, LocalFile.write, 1024) - ftp.quit() - LocalFile.close() - -""" - The file which will be sent via the FTP server - The file send will be send to the current working directory -""" - -def SendFile(): - FileName = 'example.txt' """ Enter the name of the file """ - ftp.storbinary('STOR ' + FileName, open(FileName, 'rb')) - ftp.quit() diff --git a/Graphs/a_star.py b/Graphs/a_star.py deleted file mode 100644 index 584222e6f62b..000000000000 --- a/Graphs/a_star.py +++ /dev/null @@ -1,102 +0,0 @@ -from __future__ import print_function - -grid = [[0, 1, 0, 0, 0, 0], - [0, 1, 0, 0, 0, 0],#0 are free path whereas 1's are obstacles - [0, 1, 0, 0, 0, 0], - [0, 1, 0, 0, 1, 0], - [0, 0, 0, 0, 1, 0]] - -''' -heuristic = [[9, 8, 7, 6, 5, 4], - [8, 7, 6, 5, 4, 3], - [7, 6, 5, 4, 3, 2], - [6, 5, 4, 3, 2, 1], - [5, 4, 3, 2, 1, 0]]''' - -init = [0, 0] -goal = [len(grid)-1, len(grid[0])-1] #all coordinates are given in format [y,x] -cost = 1 - -#the cost map which pushes the path closer to the goal -heuristic = [[0 for row in range(len(grid[0]))] for col in range(len(grid))] -for i in range(len(grid)): - for j in range(len(grid[0])): - heuristic[i][j] = abs(i - goal[0]) + abs(j - goal[1]) - if grid[i][j] == 1: - heuristic[i][j] = 99 #added extra penalty in the heuristic map - - -#the actions we can take -delta = [[-1, 0 ], # go up - [ 0, -1], # go left - [ 1, 0 ], # go down - [ 0, 1 ]] # go right - - -#function to search the path -def search(grid,init,goal,cost,heuristic): - - closed = [[0 for col in range(len(grid[0]))] for row in range(len(grid))]# the referrence grid - closed[init[0]][init[1]] = 1 - action = [[0 for col in range(len(grid[0]))] for row in range(len(grid))]#the action grid - - x = init[0] - y = init[1] - g = 0 - f = g + heuristic[init[0]][init[0]] - cell = [[f, g, x, y]] - - found = False # flag that is set when search is complete - resign = False # flag set if we can't find expand - - while not found and not resign: - if len(cell) == 0: - resign = True - return "FAIL" - else: - cell.sort()#to choose the least costliest action so as to move closer to the goal - cell.reverse() - next = cell.pop() - x = next[2] - y = next[3] - g = next[1] - f = next[0] - - - if x == goal[0] and y == goal[1]: - found = True - else: - for i in range(len(delta)):#to try out different valid actions - x2 = x + delta[i][0] - y2 = y + delta[i][1] - if x2 >= 0 and x2 < len(grid) and y2 >=0 and y2 < len(grid[0]): - if closed[x2][y2] == 0 and grid[x2][y2] == 0: - g2 = g + cost - f2 = g2 + heuristic[x2][y2] - cell.append([f2, g2, x2, y2]) - closed[x2][y2] = 1 - action[x2][y2] = i - invpath = [] - x = goal[0] - y = goal[1] - invpath.append([x, y])#we get the reverse path from here - while x != init[0] or y != init[1]: - x2 = x - delta[action[x][y]][0] - y2 = y - delta[action[x][y]][1] - x = x2 - y = y2 - invpath.append([x, y]) - - path = [] - for i in range(len(invpath)): - path.append(invpath[len(invpath) - 1 - i]) - print("ACTION MAP") - for i in range(len(action)): - print(action[i]) - - return path - -a = search(grid,init,goal,cost,heuristic) -for i in range(len(a)): - print(a[i]) - diff --git a/Graphs/basic-graphs.py b/Graphs/basic-graphs.py deleted file mode 100644 index 6e433b5bd725..000000000000 --- a/Graphs/basic-graphs.py +++ /dev/null @@ -1,281 +0,0 @@ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 - -try: - xrange # Python 2 -except NameError: - xrange = range # Python 3 - -# Accept No. of Nodes and edges -n, m = map(int, raw_input().split(" ")) - -# Initialising Dictionary of edges -g = {} -for i in xrange(n): - g[i + 1] = [] - -""" --------------------------------------------------------------------------------- - Accepting edges of Unweighted Directed Graphs --------------------------------------------------------------------------------- -""" -for _ in xrange(m): - x, y = map(int, raw_input().split(" ")) - g[x].append(y) - -""" --------------------------------------------------------------------------------- - Accepting edges of Unweighted Undirected Graphs --------------------------------------------------------------------------------- -""" -for _ in xrange(m): - x, y = map(int, raw_input().split(" ")) - g[x].append(y) - g[y].append(x) - -""" --------------------------------------------------------------------------------- - Accepting edges of Weighted Undirected Graphs --------------------------------------------------------------------------------- -""" -for _ in xrange(m): - x, y, r = map(int, raw_input().split(" ")) - g[x].append([y, r]) - g[y].append([x, r]) - -""" --------------------------------------------------------------------------------- - Depth First Search. - Args : G - Dictionary of edges - s - Starting Node - Vars : vis - Set of visited nodes - S - Traversal Stack --------------------------------------------------------------------------------- -""" - - -def dfs(G, s): - vis, S = set([s]), [s] - print(s) - while S: - flag = 0 - for i in G[S[-1]]: - if i not in vis: - S.append(i) - vis.add(i) - flag = 1 - print(i) - break - if not flag: - S.pop() - - -""" --------------------------------------------------------------------------------- - Breadth First Search. - Args : G - Dictionary of edges - s - Starting Node - Vars : vis - Set of visited nodes - Q - Traveral Stack --------------------------------------------------------------------------------- -""" -from collections import deque - - -def bfs(G, s): - vis, Q = set([s]), deque([s]) - print(s) - while Q: - u = Q.popleft() - for v in G[u]: - if v not in vis: - vis.add(v) - Q.append(v) - print(v) - - -""" --------------------------------------------------------------------------------- - Dijkstra's shortest path Algorithm - Args : G - Dictionary of edges - s - Starting Node - Vars : dist - Dictionary storing shortest distance from s to every other node - known - Set of knows nodes - path - Preceding node in path --------------------------------------------------------------------------------- -""" - - -def dijk(G, s): - dist, known, path = {s: 0}, set(), {s: 0} - while True: - if len(known) == len(G) - 1: - break - mini = 100000 - for i in dist: - if i not in known and dist[i] < mini: - mini = dist[i] - u = i - known.add(u) - for v in G[u]: - if v[0] not in known: - if dist[u] + v[1] < dist.get(v[0], 100000): - dist[v[0]] = dist[u] + v[1] - path[v[0]] = u - for i in dist: - if i != s: - print(dist[i]) - - -""" --------------------------------------------------------------------------------- - Topological Sort --------------------------------------------------------------------------------- -""" -from collections import deque - - -def topo(G, ind=None, Q=[1]): - if ind == None: - ind = [0] * (len(G) + 1) # SInce oth Index is ignored - for u in G: - for v in G[u]: - ind[v] += 1 - Q = deque() - for i in G: - if ind[i] == 0: - Q.append(i) - if len(Q) == 0: - return - v = Q.popleft() - print(v) - for w in G[v]: - ind[w] -= 1 - if ind[w] == 0: - Q.append(w) - topo(G, ind, Q) - - -""" --------------------------------------------------------------------------------- - Reading an Adjacency matrix --------------------------------------------------------------------------------- -""" - - -def adjm(): - n, a = input(), [] - for i in xrange(n): - a.append(map(int, raw_input().split())) - return a, n - - -""" --------------------------------------------------------------------------------- - Floyd Warshall's algorithm - Args : G - Dictionary of edges - s - Starting Node - Vars : dist - Dictionary storing shortest distance from s to every other node - known - Set of knows nodes - path - Preceding node in path - --------------------------------------------------------------------------------- -""" - - -def floy(A_and_n): - (A, n) = A_and_n - dist = list(A) - path = [[0] * n for i in xrange(n)] - for k in xrange(n): - for i in xrange(n): - for j in xrange(n): - if dist[i][j] > dist[i][k] + dist[k][j]: - dist[i][j] = dist[i][k] + dist[k][j] - path[i][k] = k - print(dist) - - -""" --------------------------------------------------------------------------------- - Prim's MST Algorithm - Args : G - Dictionary of edges - s - Starting Node - Vars : dist - Dictionary storing shortest distance from s to nearest node - known - Set of knows nodes - path - Preceding node in path --------------------------------------------------------------------------------- -""" - - -def prim(G, s): - dist, known, path = {s: 0}, set(), {s: 0} - while True: - if len(known) == len(G) - 1: - break - mini = 100000 - for i in dist: - if i not in known and dist[i] < mini: - mini = dist[i] - u = i - known.add(u) - for v in G[u]: - if v[0] not in known: - if v[1] < dist.get(v[0], 100000): - dist[v[0]] = v[1] - path[v[0]] = u - - -""" --------------------------------------------------------------------------------- - Accepting Edge list - Vars : n - Number of nodes - m - Number of edges - Returns : l - Edge list - n - Number of Nodes --------------------------------------------------------------------------------- -""" - - -def edglist(): - n, m = map(int, raw_input().split(" ")) - l = [] - for i in xrange(m): - l.append(map(int, raw_input().split(' '))) - return l, n - - -""" --------------------------------------------------------------------------------- - Kruskal's MST Algorithm - Args : E - Edge list - n - Number of Nodes - Vars : s - Set of all nodes as unique disjoint sets (initially) --------------------------------------------------------------------------------- -""" - - -def krusk(E_and_n): - # Sort edges on the basis of distance - (E, n) = E_and_n - E.sort(reverse=True, key=lambda x: x[2]) - s = [set([i]) for i in range(1, n + 1)] - while True: - if len(s) == 1: - break - print(s) - x = E.pop() - for i in xrange(len(s)): - if x[0] in s[i]: - break - for j in xrange(len(s)): - if x[1] in s[j]: - if i == j: - break - s[j].update(s[i]) - s.pop(i) - break diff --git a/Graphs/minimum_spanning_tree_kruskal.py b/Graphs/minimum_spanning_tree_kruskal.py deleted file mode 100644 index 81d64f421a31..000000000000 --- a/Graphs/minimum_spanning_tree_kruskal.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import print_function -num_nodes, num_edges = list(map(int,input().split())) - -edges = [] - -for i in range(num_edges): - node1, node2, cost = list(map(int,input().split())) - edges.append((i,node1,node2,cost)) - -edges = sorted(edges, key=lambda edge: edge[3]) - -parent = [i for i in range(num_nodes)] - -def find_parent(i): - if(i != parent[i]): - parent[i] = find_parent(parent[i]) - return parent[i] - -minimum_spanning_tree_cost = 0 -minimum_spanning_tree = [] - -for edge in edges: - parent_a = find_parent(edge[1]) - parent_b = find_parent(edge[2]) - if(parent_a != parent_b): - minimum_spanning_tree_cost += edge[3] - minimum_spanning_tree.append(edge) - parent[parent_a] = parent_b - -print(minimum_spanning_tree_cost) -for edge in minimum_spanning_tree: - print(edge) diff --git a/Graphs/scc_kosaraju.py b/Graphs/scc_kosaraju.py deleted file mode 100644 index 1f13ebaba36b..000000000000 --- a/Graphs/scc_kosaraju.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import print_function -# n - no of nodes, m - no of edges -n, m = list(map(int,input().split())) - -g = [[] for i in range(n)] #graph -r = [[] for i in range(n)] #reversed graph -# input graph data (edges) -for i in range(m): - u, v = list(map(int,input().split())) - g[u].append(v) - r[v].append(u) - -stack = [] -visit = [False]*n -scc = [] -component = [] - -def dfs(u): - global g, r, scc, component, visit, stack - if visit[u]: return - visit[u] = True - for v in g[u]: - dfs(v) - stack.append(u) - -def dfs2(u): - global g, r, scc, component, visit, stack - if visit[u]: return - visit[u] = True - component.append(u) - for v in r[u]: - dfs2(v) - -def kosaraju(): - global g, r, scc, component, visit, stack - for i in range(n): - dfs(i) - visit = [False]*n - for i in stack[::-1]: - if visit[i]: continue - component = [] - dfs2(i) - scc.append(component) - return scc - -print(kosaraju()) diff --git a/Graphs/tarjans_scc.py b/Graphs/tarjans_scc.py deleted file mode 100644 index 89754e593508..000000000000 --- a/Graphs/tarjans_scc.py +++ /dev/null @@ -1,78 +0,0 @@ -from collections import deque - - -def tarjan(g): - """ - Tarjan's algo for finding strongly connected components in a directed graph - - Uses two main attributes of each node to track reachability, the index of that node within a component(index), - and the lowest index reachable from that node(lowlink). - - We then perform a dfs of the each component making sure to update these parameters for each node and saving the - nodes we visit on the way. - - If ever we find that the lowest reachable node from a current node is equal to the index of the current node then it - must be the root of a strongly connected component and so we save it and it's equireachable vertices as a strongly - connected component. - - Complexity: strong_connect() is called at most once for each node and has a complexity of O(|E|) as it is DFS. - Therefore this has complexity O(|V| + |E|) for a graph G = (V, E) - - """ - - n = len(g) - stack = deque() - on_stack = [False for _ in range(n)] - index_of = [-1 for _ in range(n)] - lowlink_of = index_of[:] - - def strong_connect(v, index, components): - index_of[v] = index # the number when this node is seen - lowlink_of[v] = index # lowest rank node reachable from here - index += 1 - stack.append(v) - on_stack[v] = True - - for w in g[v]: - if index_of[w] == -1: - index = strong_connect(w, index, components) - lowlink_of[v] = lowlink_of[w] if lowlink_of[w] < lowlink_of[v] else lowlink_of[v] - elif on_stack[w]: - lowlink_of[v] = lowlink_of[w] if lowlink_of[w] < lowlink_of[v] else lowlink_of[v] - - if lowlink_of[v] == index_of[v]: - component = [] - w = stack.pop() - on_stack[w] = False - component.append(w) - while w != v: - w = stack.pop() - on_stack[w] = False - component.append(w) - components.append(component) - return index - - components = [] - for v in range(n): - if index_of[v] == -1: - strong_connect(v, 0, components) - - return components - - -def create_graph(n, edges): - g = [[] for _ in range(n)] - for u, v in edges: - g[u].append(v) - return g - - -if __name__ == '__main__': - # Test - n_vertices = 7 - source = [0, 0, 1, 2, 3, 3, 4, 4, 6] - target = [1, 3, 2, 0, 1, 4, 5, 6, 5] - edges = [(u, v) for u, v in zip(source, target)] - g = create_graph(n_vertices, edges) - - assert [[5], [6], [4], [3, 2, 1, 0]] == tarjan(g) diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000000..de631c3ef333 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +## MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/License b/License deleted file mode 100644 index c84ae570c084..000000000000 --- a/License +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2016 The Algorithms - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Maths/ModularExponential.py b/Maths/ModularExponential.py deleted file mode 100644 index b3f4c00bd5d8..000000000000 --- a/Maths/ModularExponential.py +++ /dev/null @@ -1,20 +0,0 @@ -def modularExponential(base, power, mod): - if power < 0: - return -1 - base %= mod - result = 1 - - while power > 0: - if power & 1: - result = (result * base) % mod - power = power >> 1 - base = (base * base) % mod - return result - - -def main(): - print(modularExponential(3, 200, 13)) - - -if __name__ == '__main__': - main() diff --git a/Maths/SimpsonRule.py b/Maths/SimpsonRule.py deleted file mode 100644 index 091c86c17f1b..000000000000 --- a/Maths/SimpsonRule.py +++ /dev/null @@ -1,49 +0,0 @@ - -''' -Numerical integration or quadrature for a smooth function f with known values at x_i - -This method is the classical approch of suming 'Equally Spaced Abscissas' - -method 2: -"Simpson Rule" - -''' -from __future__ import print_function - - -def method_2(boundary, steps): -# "Simpson Rule" -# int(f) = delta_x/2 * (b-a)/3*(f1 + 4f2 + 2f_3 + ... + fn) - h = (boundary[1] - boundary[0]) / steps - a = boundary[0] - b = boundary[1] - x_i = makePoints(a,b,h) - y = 0.0 - y += (h/3.0)*f(a) - cnt = 2 - for i in x_i: - y += (h/3)*(4-2*(cnt%2))*f(i) - cnt += 1 - y += (h/3.0)*f(b) - return y - -def makePoints(a,b,h): - x = a + h - while x < (b-h): - yield x - x = x + h - -def f(x): #enter your function here - y = (x-0)*(x-0) - return y - -def main(): - a = 0.0 #Lower bound of integration - b = 1.0 #Upper bound of integration - steps = 10.0 #define number of steps or resolution - boundary = [a, b] #define boundary of integration - y = method_2(boundary, steps) - print('y = {0}'.format(y)) - -if __name__ == '__main__': - main() diff --git a/Maths/TrapezoidalRule.py b/Maths/TrapezoidalRule.py deleted file mode 100644 index 52310c1ed3b0..000000000000 --- a/Maths/TrapezoidalRule.py +++ /dev/null @@ -1,46 +0,0 @@ -''' -Numerical integration or quadrature for a smooth function f with known values at x_i - -This method is the classical approch of suming 'Equally Spaced Abscissas' - -method 1: -"extended trapezoidal rule" - -''' -from __future__ import print_function - -def method_1(boundary, steps): -# "extended trapezoidal rule" -# int(f) = dx/2 * (f1 + 2f2 + ... + fn) - h = (boundary[1] - boundary[0]) / steps - a = boundary[0] - b = boundary[1] - x_i = makePoints(a,b,h) - y = 0.0 - y += (h/2.0)*f(a) - for i in x_i: - #print(i) - y += h*f(i) - y += (h/2.0)*f(b) - return y - -def makePoints(a,b,h): - x = a + h - while x < (b-h): - yield x - x = x + h - -def f(x): #enter your function here - y = (x-0)*(x-0) - return y - -def main(): - a = 0.0 #Lower bound of integration - b = 1.0 #Upper bound of integration - steps = 10.0 #define number of steps or resolution - boundary = [a, b] #define boundary of integration - y = method_1(boundary, steps) - print('y = {0}'.format(y)) - -if __name__ == '__main__': - main() diff --git a/Multi_Hueristic_Astar.py b/Multi_Hueristic_Astar.py deleted file mode 100644 index 7fbd2ff04542..000000000000 --- a/Multi_Hueristic_Astar.py +++ /dev/null @@ -1,268 +0,0 @@ -from __future__ import print_function -import heapq -import numpy as np -import math -import copy - -try: - xrange # Python 2 -except NameError: - xrange = range # Python 3 - - -class PriorityQueue: - def __init__(self): - self.elements = [] - self.set = set() - - def minkey(self): - if not self.empty(): - return self.elements[0][0] - else: - return float('inf') - - def empty(self): - return len(self.elements) == 0 - - def put(self, item, priority): - if item not in self.set: - heapq.heappush(self.elements, (priority, item)) - self.set.add(item) - else: - # update - # print("update", item) - temp = [] - (pri, x) = heapq.heappop(self.elements) - while x != item: - temp.append((pri, x)) - (pri, x) = heapq.heappop(self.elements) - temp.append((priority, item)) - for (pro, xxx) in temp: - heapq.heappush(self.elements, (pro, xxx)) - - def remove_element(self, item): - if item in self.set: - self.set.remove(item) - temp = [] - (pro, x) = heapq.heappop(self.elements) - while x != item: - temp.append((pro, x)) - (pro, x) = heapq.heappop(self.elements) - for (prito, yyy) in temp: - heapq.heappush(self.elements, (prito, yyy)) - - def top_show(self): - return self.elements[0][1] - - def get(self): - (priority, item) = heapq.heappop(self.elements) - self.set.remove(item) - return (priority, item) - -def consistent_hueristic(P, goal): - # euclidean distance - a = np.array(P) - b = np.array(goal) - return np.linalg.norm(a - b) - -def hueristic_2(P, goal): - # integer division by time variable - return consistent_hueristic(P, goal) // t - -def hueristic_1(P, goal): - # manhattan distance - return abs(P[0] - goal[0]) + abs(P[1] - goal[1]) - -def key(start, i, goal, g_function): - ans = g_function[start] + W1 * hueristics[i](start, goal) - return ans - -def do_something(back_pointer, goal, start): - grid = np.chararray((n, n)) - for i in range(n): - for j in range(n): - grid[i][j] = '*' - - for i in range(n): - for j in range(n): - if (j, (n-1)-i) in blocks: - grid[i][j] = "#" - - grid[0][(n-1)] = "-" - x = back_pointer[goal] - while x != start: - (x_c, y_c) = x - # print(x) - grid[(n-1)-y_c][x_c] = "-" - x = back_pointer[x] - grid[(n-1)][0] = "-" - - - for i in xrange(n): - for j in range(n): - if (i, j) == (0, n-1): - print(grid[i][j], end=' ') - print("<-- End position", end=' ') - else: - print(grid[i][j], end=' ') - print() - print("^") - print("Start position") - print() - print("# is an obstacle") - print("- is the path taken by algorithm") - print("PATH TAKEN BY THE ALGORITHM IS:-") - x = back_pointer[goal] - while x != start: - print(x, end=' ') - x = back_pointer[x] - print(x) - quit() - -def valid(p): - if p[0] < 0 or p[0] > n-1: - return False - if p[1] < 0 or p[1] > n-1: - return False - return True - -def expand_state(s, j, visited, g_function, close_list_anchor, close_list_inad, open_list, back_pointer): - for itera in range(n_hueristic): - open_list[itera].remove_element(s) - # print("s", s) - # print("j", j) - (x, y) = s - left = (x-1, y) - right = (x+1, y) - up = (x, y+1) - down = (x, y-1) - - for neighbours in [left, right, up, down]: - if neighbours not in blocks: - if valid(neighbours) and neighbours not in visited: - # print("neighbour", neighbours) - visited.add(neighbours) - back_pointer[neighbours] = -1 - g_function[neighbours] = float('inf') - - if valid(neighbours) and g_function[neighbours] > g_function[s] + 1: - g_function[neighbours] = g_function[s] + 1 - back_pointer[neighbours] = s - if neighbours not in close_list_anchor: - open_list[0].put(neighbours, key(neighbours, 0, goal, g_function)) - if neighbours not in close_list_inad: - for var in range(1,n_hueristic): - if key(neighbours, var, goal, g_function) <= W2 * key(neighbours, 0, goal, g_function): - # print("why not plssssssssss") - open_list[j].put(neighbours, key(neighbours, var, goal, g_function)) - - - # print - -def make_common_ground(): - some_list = [] - # block 1 - for x in range(1, 5): - for y in range(1, 6): - some_list.append((x, y)) - - # line - for x in range(15, 20): - some_list.append((x, 17)) - - # block 2 big - for x in range(10, 19): - for y in range(1, 15): - some_list.append((x, y)) - - # L block - for x in range(1, 4): - for y in range(12, 19): - some_list.append((x, y)) - for x in range(3, 13): - for y in range(16, 19): - some_list.append((x, y)) - return some_list - -hueristics = {0: consistent_hueristic, 1: hueristic_1, 2: hueristic_2} - -blocks_blk = [(0, 1),(1, 1),(2, 1),(3, 1),(4, 1),(5, 1),(6, 1),(7, 1),(8, 1),(9, 1),(10, 1),(11, 1),(12, 1),(13, 1),(14, 1),(15, 1),(16, 1),(17, 1),(18, 1), (19, 1)] -blocks_no = [] -blocks_all = make_common_ground() - - - - -blocks = blocks_blk -# hyper parameters -W1 = 1 -W2 = 1 -n = 20 -n_hueristic = 3 # one consistent and two other inconsistent - -# start and end destination -start = (0, 0) -goal = (n-1, n-1) - -t = 1 -def multi_a_star(start, goal, n_hueristic): - g_function = {start: 0, goal: float('inf')} - back_pointer = {start:-1, goal:-1} - open_list = [] - visited = set() - - for i in range(n_hueristic): - open_list.append(PriorityQueue()) - open_list[i].put(start, key(start, i, goal, g_function)) - - close_list_anchor = [] - close_list_inad = [] - while open_list[0].minkey() < float('inf'): - for i in range(1, n_hueristic): - # print("i", i) - # print(open_list[0].minkey(), open_list[i].minkey()) - if open_list[i].minkey() <= W2 * open_list[0].minkey(): - global t - t += 1 - # print("less prio") - if g_function[goal] <= open_list[i].minkey(): - if g_function[goal] < float('inf'): - do_something(back_pointer, goal, start) - else: - _, get_s = open_list[i].top_show() - visited.add(get_s) - expand_state(get_s, i, visited, g_function, close_list_anchor, close_list_inad, open_list, back_pointer) - close_list_inad.append(get_s) - else: - # print("more prio") - if g_function[goal] <= open_list[0].minkey(): - if g_function[goal] < float('inf'): - do_something(back_pointer, goal, start) - else: - # print("hoolla") - get_s = open_list[0].top_show() - visited.add(get_s) - expand_state(get_s, 0, visited, g_function, close_list_anchor, close_list_inad, open_list, back_pointer) - close_list_anchor.append(get_s) - print("No path found to goal") - print() - for i in range(n-1,-1, -1): - for j in range(n): - if (j, i) in blocks: - print('#', end=' ') - elif (j, i) in back_pointer: - if (j, i) == (n-1, n-1): - print('*', end=' ') - else: - print('-', end=' ') - else: - print('*', end=' ') - if (j, i) == (n-1, n-1): - print('<-- End position', end=' ') - print() - print("^") - print("Start position") - print() - print("# is an obstacle") - print("- is the path taken by algorithm") -multi_a_star(start, goal, n_hueristic) diff --git a/Neural_Network/bpnn.py b/Neural_Network/bpnn.py deleted file mode 100644 index ed5d4c8cbf79..000000000000 --- a/Neural_Network/bpnn.py +++ /dev/null @@ -1,190 +0,0 @@ -''' - -A Framework of Back Propagation Neural Network(BP) model - -Easy to use: - * add many layers as you want ��� - * clearly see how the loss decreasing -Easy to expand: - * more activation functions - * more loss functions - * more optimization method - -Author: Stephen Lee -Github : https://github.com/RiptideBo -Date: 2017.11.23 - -''' - -import numpy as np -import matplotlib.pyplot as plt - - -def sigmoid(x): - return 1 / (1 + np.exp(-1 * x)) - -class DenseLayer(): - ''' - Layers of BP neural network - ''' - def __init__(self,units,activation=None,learning_rate=None,is_input_layer=False): - ''' - common connected layer of bp network - :param units: numbers of neural units - :param activation: activation function - :param learning_rate: learning rate for paras - :param is_input_layer: whether it is input layer or not - ''' - self.units = units - self.weight = None - self.bias = None - self.activation = activation - if learning_rate is None: - learning_rate = 0.3 - self.learn_rate = learning_rate - self.is_input_layer = is_input_layer - - def initializer(self,back_units): - self.weight = np.asmatrix(np.random.normal(0,0.5,(self.units,back_units))) - self.bias = np.asmatrix(np.random.normal(0,0.5,self.units)).T - if self.activation is None: - self.activation = sigmoid - - def cal_gradient(self): - if self.activation == sigmoid: - gradient_mat = np.dot(self.output ,(1- self.output).T) - gradient_activation = np.diag(np.diag(gradient_mat)) - else: - gradient_activation = 1 - return gradient_activation - - def forward_propagation(self,xdata): - self.xdata = xdata - if self.is_input_layer: - # input layer - self.wx_plus_b = xdata - self.output = xdata - return xdata - else: - self.wx_plus_b = np.dot(self.weight,self.xdata) - self.bias - self.output = self.activation(self.wx_plus_b) - return self.output - - def back_propagation(self,gradient): - - gradient_activation = self.cal_gradient() # i * i 维 - gradient = np.asmatrix(np.dot(gradient.T,gradient_activation)) - - self._gradient_weight = np.asmatrix(self.xdata) - self._gradient_bias = -1 - self._gradient_x = self.weight - - self.gradient_weight = np.dot(gradient.T,self._gradient_weight.T) - self.gradient_bias = gradient * self._gradient_bias - self.gradient = np.dot(gradient,self._gradient_x).T - # ----------------------upgrade - # -----------the Negative gradient direction -------- - self.weight = self.weight - self.learn_rate * self.gradient_weight - self.bias = self.bias - self.learn_rate * self.gradient_bias.T - - return self.gradient - - -class BPNN(): - ''' - Back Propagation Neural Network model - ''' - def __init__(self): - self.layers = [] - self.train_mse = [] - self.fig_loss = plt.figure() - self.ax_loss = self.fig_loss.add_subplot(1,1,1) - - def add_layer(self,layer): - self.layers.append(layer) - - def build(self): - for i,layer in enumerate(self.layers[:]): - if i < 1: - layer.is_input_layer = True - else: - layer.initializer(self.layers[i-1].units) - - def summary(self): - for i,layer in enumerate(self.layers[:]): - print('------- layer %d -------'%i) - print('weight.shape ',np.shape(layer.weight)) - print('bias.shape ',np.shape(layer.bias)) - - def train(self,xdata,ydata,train_round,accuracy): - self.train_round = train_round - self.accuracy = accuracy - - self.ax_loss.hlines(self.accuracy, 0, self.train_round * 1.1) - - x_shape = np.shape(xdata) - for round_i in range(train_round): - all_loss = 0 - for row in range(x_shape[0]): - _xdata = np.asmatrix(xdata[row,:]).T - _ydata = np.asmatrix(ydata[row,:]).T - - # forward propagation - for layer in self.layers: - _xdata = layer.forward_propagation(_xdata) - - loss, gradient = self.cal_loss(_ydata, _xdata) - all_loss = all_loss + loss - - # back propagation - # the input_layer does not upgrade - for layer in self.layers[:0:-1]: - gradient = layer.back_propagation(gradient) - - mse = all_loss/x_shape[0] - self.train_mse.append(mse) - - self.plot_loss() - - if mse < self.accuracy: - print('----达到精度----') - return mse - - def cal_loss(self,ydata,ydata_): - self.loss = np.sum(np.power((ydata - ydata_),2)) - self.loss_gradient = 2 * (ydata_ - ydata) - # vector (shape is the same as _ydata.shape) - return self.loss,self.loss_gradient - - def plot_loss(self): - if self.ax_loss.lines: - self.ax_loss.lines.remove(self.ax_loss.lines[0]) - self.ax_loss.plot(self.train_mse, 'r-') - plt.ion() - plt.show() - plt.pause(0.1) - - - - -def example(): - - x = np.random.randn(10,10) - y = np.asarray([[0.8,0.4],[0.4,0.3],[0.34,0.45],[0.67,0.32], - [0.88,0.67],[0.78,0.77],[0.55,0.66],[0.55,0.43],[0.54,0.1], - [0.1,0.5]]) - - model = BPNN() - model.add_layer(DenseLayer(10)) - model.add_layer(DenseLayer(20)) - model.add_layer(DenseLayer(30)) - model.add_layer(DenseLayer(2)) - - model.build() - - model.summary() - - model.train(xdata=x,ydata=y,train_round=100,accuracy=0.01) - -if __name__ == '__main__': - example() diff --git a/Neural_Network/convolution_neural_network.py b/Neural_Network/convolution_neural_network.py deleted file mode 100644 index 0dca2bc485d1..000000000000 --- a/Neural_Network/convolution_neural_network.py +++ /dev/null @@ -1,306 +0,0 @@ -#-*- coding: utf-8 -*- - -''' - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - Name - - CNN - Convolution Neural Network For Photo Recognizing - Goal - - Recognize Handing Writting Word Photo - Detail:Total 5 layers neural network - * Convolution layer - * Pooling layer - * Input layer layer of BP - * Hiden layer of BP - * Output layer of BP - Author: Stephen Lee - Github: 245885195@qq.com - Date: 2017.9.20 - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - ''' -from __future__ import print_function - -import numpy as np -import matplotlib.pyplot as plt - -class CNN(): - - def __init__(self,conv1_get,size_p1,bp_num1,bp_num2,bp_num3,rate_w=0.2,rate_t=0.2): - ''' - :param conv1_get: [a,c,d],size, number, step of convolution kernel - :param size_p1: pooling size - :param bp_num1: units number of flatten layer - :param bp_num2: units number of hidden layer - :param bp_num3: units number of output layer - :param rate_w: rate of weight learning - :param rate_t: rate of threshold learning - ''' - self.num_bp1 = bp_num1 - self.num_bp2 = bp_num2 - self.num_bp3 = bp_num3 - self.conv1 = conv1_get[:2] - self.step_conv1 = conv1_get[2] - self.size_pooling1 = size_p1 - self.rate_weight = rate_w - self.rate_thre = rate_t - self.w_conv1 = [np.mat(-1*np.random.rand(self.conv1[0],self.conv1[0])+0.5) for i in range(self.conv1[1])] - self.wkj = np.mat(-1 * np.random.rand(self.num_bp3, self.num_bp2) + 0.5) - self.vji = np.mat(-1*np.random.rand(self.num_bp2, self.num_bp1)+0.5) - self.thre_conv1 = -2*np.random.rand(self.conv1[1])+1 - self.thre_bp2 = -2*np.random.rand(self.num_bp2)+1 - self.thre_bp3 = -2*np.random.rand(self.num_bp3)+1 - - - def save_model(self,save_path): - #save model dict with pickle - import pickle - model_dic = {'num_bp1':self.num_bp1, - 'num_bp2':self.num_bp2, - 'num_bp3':self.num_bp3, - 'conv1':self.conv1, - 'step_conv1':self.step_conv1, - 'size_pooling1':self.size_pooling1, - 'rate_weight':self.rate_weight, - 'rate_thre':self.rate_thre, - 'w_conv1':self.w_conv1, - 'wkj':self.wkj, - 'vji':self.vji, - 'thre_conv1':self.thre_conv1, - 'thre_bp2':self.thre_bp2, - 'thre_bp3':self.thre_bp3} - with open(save_path, 'wb') as f: - pickle.dump(model_dic, f) - - print('Model saved: %s'% save_path) - - @classmethod - def ReadModel(cls,model_path): - #read saved model - import pickle - with open(model_path, 'rb') as f: - model_dic = pickle.load(f) - - conv_get= model_dic.get('conv1') - conv_get.append(model_dic.get('step_conv1')) - size_p1 = model_dic.get('size_pooling1') - bp1 = model_dic.get('num_bp1') - bp2 = model_dic.get('num_bp2') - bp3 = model_dic.get('num_bp3') - r_w = model_dic.get('rate_weight') - r_t = model_dic.get('rate_thre') - #create model instance - conv_ins = CNN(conv_get,size_p1,bp1,bp2,bp3,r_w,r_t) - #modify model parameter - conv_ins.w_conv1 = model_dic.get('w_conv1') - conv_ins.wkj = model_dic.get('wkj') - conv_ins.vji = model_dic.get('vji') - conv_ins.thre_conv1 = model_dic.get('thre_conv1') - conv_ins.thre_bp2 = model_dic.get('thre_bp2') - conv_ins.thre_bp3 = model_dic.get('thre_bp3') - return conv_ins - - - def sig(self,x): - return 1 / (1 + np.exp(-1*x)) - - def do_round(self,x): - return round(x, 3) - - def convolute(self,data,convs,w_convs,thre_convs,conv_step): - #convolution process - size_conv = convs[0] - num_conv =convs[1] - size_data = np.shape(data)[0] - #get the data slice of original image data, data_focus - data_focus = [] - for i_focus in range(0, size_data - size_conv + 1, conv_step): - for j_focus in range(0, size_data - size_conv + 1, conv_step): - focus = data[i_focus:i_focus + size_conv, j_focus:j_focus + size_conv] - data_focus.append(focus) - #caculate the feature map of every single kernel, and saved as list of matrix - data_featuremap = [] - Size_FeatureMap = int((size_data - size_conv) / conv_step + 1) - for i_map in range(num_conv): - featuremap = [] - for i_focus in range(len(data_focus)): - net_focus = np.sum(np.multiply(data_focus[i_focus], w_convs[i_map])) - thre_convs[i_map] - featuremap.append(self.sig(net_focus)) - featuremap = np.asmatrix(featuremap).reshape(Size_FeatureMap, Size_FeatureMap) - data_featuremap.append(featuremap) - - #expanding the data slice to One dimenssion - focus1_list = [] - for each_focus in data_focus: - focus1_list.extend(self.Expand_Mat(each_focus)) - focus_list = np.asarray(focus1_list) - return focus_list,data_featuremap - - def pooling(self,featuremaps,size_pooling,type='average_pool'): - #pooling process - size_map = len(featuremaps[0]) - size_pooled = int(size_map/size_pooling) - featuremap_pooled = [] - for i_map in range(len(featuremaps)): - map = featuremaps[i_map] - map_pooled = [] - for i_focus in range(0,size_map,size_pooling): - for j_focus in range(0, size_map, size_pooling): - focus = map[i_focus:i_focus + size_pooling, j_focus:j_focus + size_pooling] - if type == 'average_pool': - #average pooling - map_pooled.append(np.average(focus)) - elif type == 'max_pooling': - #max pooling - map_pooled.append(np.max(focus)) - map_pooled = np.asmatrix(map_pooled).reshape(size_pooled,size_pooled) - featuremap_pooled.append(map_pooled) - return featuremap_pooled - - def _expand(self,datas): - #expanding three dimension data to one dimension list - data_expanded = [] - for i in range(len(datas)): - shapes = np.shape(datas[i]) - data_listed = datas[i].reshape(1,shapes[0]*shapes[1]) - data_listed = data_listed.getA().tolist()[0] - data_expanded.extend(data_listed) - data_expanded = np.asarray(data_expanded) - return data_expanded - - def _expand_mat(self,data_mat): - #expanding matrix to one dimension list - data_mat = np.asarray(data_mat) - shapes = np.shape(data_mat) - data_expanded = data_mat.reshape(1,shapes[0]*shapes[1]) - return data_expanded - - def _calculate_gradient_from_pool(self,out_map,pd_pool,num_map,size_map,size_pooling): - ''' - calcluate the gradient from the data slice of pool layer - pd_pool: list of matrix - out_map: the shape of data slice(size_map*size_map) - return: pd_all: list of matrix, [num, size_map, size_map] - ''' - pd_all = [] - i_pool = 0 - for i_map in range(num_map): - pd_conv1 = np.ones((size_map, size_map)) - for i in range(0, size_map, size_pooling): - for j in range(0, size_map, size_pooling): - pd_conv1[i:i + size_pooling, j:j + size_pooling] = pd_pool[i_pool] - i_pool = i_pool + 1 - pd_conv2 = np.multiply(pd_conv1,np.multiply(out_map[i_map],(1-out_map[i_map]))) - pd_all.append(pd_conv2) - return pd_all - - def trian(self,patterns,datas_train, datas_teach, n_repeat, error_accuracy,draw_e = bool): - #model traning - print('----------------------Start Training-------------------------') - print((' - - Shape: Train_Data ',np.shape(datas_train))) - print((' - - Shape: Teach_Data ',np.shape(datas_teach))) - rp = 0 - all_mse = [] - mse = 10000 - while rp < n_repeat and mse >= error_accuracy: - alle = 0 - print('-------------Learning Time %d--------------'%rp) - for p in range(len(datas_train)): - #print('------------Learning Image: %d--------------'%p) - data_train = np.asmatrix(datas_train[p]) - data_teach = np.asarray(datas_teach[p]) - data_focus1,data_conved1 = self.convolute(data_train,self.conv1,self.w_conv1, - self.thre_conv1,conv_step=self.step_conv1) - data_pooled1 = self.pooling(data_conved1,self.size_pooling1) - shape_featuremap1 = np.shape(data_conved1) - ''' - print(' -----original shape ', np.shape(data_train)) - print(' ---- after convolution ',np.shape(data_conv1)) - print(' -----after pooling ',np.shape(data_pooled1)) - ''' - data_bp_input = self._expand(data_pooled1) - bp_out1 = data_bp_input - - bp_net_j = np.dot(bp_out1,self.vji.T) - self.thre_bp2 - bp_out2 = self.sig(bp_net_j) - bp_net_k = np.dot(bp_out2 ,self.wkj.T) - self.thre_bp3 - bp_out3 = self.sig(bp_net_k) - - #--------------Model Leaning ------------------------ - # calcluate error and gradient--------------- - pd_k_all = np.multiply((data_teach - bp_out3), np.multiply(bp_out3, (1 - bp_out3))) - pd_j_all = np.multiply(np.dot(pd_k_all,self.wkj), np.multiply(bp_out2, (1 - bp_out2))) - pd_i_all = np.dot(pd_j_all,self.vji) - - pd_conv1_pooled = pd_i_all / (self.size_pooling1*self.size_pooling1) - pd_conv1_pooled = pd_conv1_pooled.T.getA().tolist() - pd_conv1_all = self._calculate_gradient_from_pool(data_conved1,pd_conv1_pooled,shape_featuremap1[0], - shape_featuremap1[1],self.size_pooling1) - #weight and threshold learning process--------- - #convolution layer - for k_conv in range(self.conv1[1]): - pd_conv_list = self._expand_mat(pd_conv1_all[k_conv]) - delta_w = self.rate_weight * np.dot(pd_conv_list,data_focus1) - - self.w_conv1[k_conv] = self.w_conv1[k_conv] + delta_w.reshape((self.conv1[0],self.conv1[0])) - - self.thre_conv1[k_conv] = self.thre_conv1[k_conv] - np.sum(pd_conv1_all[k_conv]) * self.rate_thre - #all connected layer - self.wkj = self.wkj + pd_k_all.T * bp_out2 * self.rate_weight - self.vji = self.vji + pd_j_all.T * bp_out1 * self.rate_weight - self.thre_bp3 = self.thre_bp3 - pd_k_all * self.rate_thre - self.thre_bp2 = self.thre_bp2 - pd_j_all * self.rate_thre - # calculate the sum error of all single image - errors = np.sum(abs((data_teach - bp_out3))) - alle = alle + errors - #print(' ----Teach ',data_teach) - #print(' ----BP_output ',bp_out3) - 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.5) - plt.show() - print('------------------Training Complished---------------------') - print((' - - Training epoch: ', rp, ' - - Mse: %.6f' % mse)) - if draw_e: - draw_error() - return mse - - def predict(self,datas_test): - #model predict - produce_out = [] - print('-------------------Start Testing-------------------------') - print((' - - Shape: Test_Data ',np.shape(datas_test))) - for p in range(len(datas_test)): - data_test = np.asmatrix(datas_test[p]) - data_focus1, data_conved1 = self.convolute(data_test, self.conv1, self.w_conv1, - self.thre_conv1, conv_step=self.step_conv1) - data_pooled1 = self.pooling(data_conved1, self.size_pooling1) - data_bp_input = self._expand(data_pooled1) - - bp_out1 = data_bp_input - bp_net_j = bp_out1 * self.vji.T - self.thre_bp2 - bp_out2 = self.sig(bp_net_j) - bp_net_k = bp_out2 * self.wkj.T - self.thre_bp3 - bp_out3 = self.sig(bp_net_k) - produce_out.extend(bp_out3.getA().tolist()) - res = [list(map(self.do_round,each)) for each in produce_out] - return np.asarray(res) - - def convolution(self,data): - #return the data of image after convoluting process so we can check it out - data_test = np.asmatrix(data) - data_focus1, data_conved1 = self.convolute(data_test, self.conv1, self.w_conv1, - self.thre_conv1, conv_step=self.step_conv1) - data_pooled1 = self.pooling(data_conved1, self.size_pooling1) - - return data_conved1,data_pooled1 - - -if __name__ == '__main__': - pass - ''' - I will put the example on other file - ''' \ No newline at end of file diff --git a/Neural_Network/perceptron.py b/Neural_Network/perceptron.py deleted file mode 100644 index 8ac3e8fc69e9..000000000000 --- a/Neural_Network/perceptron.py +++ /dev/null @@ -1,124 +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 - -''' -from __future__ import print_function - -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/Project Euler/Problem 01/sol1.py b/Project Euler/Problem 01/sol1.py deleted file mode 100644 index 27031c3cfa9a..000000000000 --- a/Project Euler/Problem 01/sol1.py +++ /dev/null @@ -1,17 +0,0 @@ -''' -Problem Statement: -If we list all the natural numbers below 10 that are multiples of 3 or 5, -we get 3,5,6 and 9. The sum of these multiples is 23. -Find the sum of all the multiples of 3 or 5 below N. -''' -from __future__ import print_function -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 -n = int(raw_input().strip()) -sum=0 -for a in range(3,n): - if(a%3==0 or a%5==0): - sum+=a -print(sum) diff --git a/Project Euler/Problem 01/sol2.py b/Project Euler/Problem 01/sol2.py deleted file mode 100644 index 2b7760e0bfff..000000000000 --- a/Project Euler/Problem 01/sol2.py +++ /dev/null @@ -1,20 +0,0 @@ -''' -Problem Statement: -If we list all the natural numbers below 10 that are multiples of 3 or 5, -we get 3,5,6 and 9. The sum of these multiples is 23. -Find the sum of all the multiples of 3 or 5 below N. -''' -from __future__ import print_function -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 -n = int(raw_input().strip()) -sum = 0 -terms = (n-1)//3 -sum+= ((terms)*(6+(terms-1)*3))//2 #sum of an A.P. -terms = (n-1)//5 -sum+= ((terms)*(10+(terms-1)*5))//2 -terms = (n-1)//15 -sum-= ((terms)*(30+(terms-1)*15))//2 -print(sum) diff --git a/Project Euler/Problem 01/sol3.py b/Project Euler/Problem 01/sol3.py deleted file mode 100644 index f4f3aefcc5de..000000000000 --- a/Project Euler/Problem 01/sol3.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import print_function - -''' -Problem Statement: -If we list all the natural numbers below 10 that are multiples of 3 or 5, -we get 3,5,6 and 9. The sum of these multiples is 23. -Find the sum of all the multiples of 3 or 5 below N. -''' -''' -This solution is based on the pattern that the successive numbers in the series follow: 0+3,+2,+1,+3,+1,+2,+3. -''' - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 -n = int(raw_input().strip()) -sum=0 -num=0 -while(1): - num+=3 - if(num>=n): - break - sum+=num - num+=2 - if(num>=n): - break - sum+=num - num+=1 - if(num>=n): - break - sum+=num - num+=3 - if(num>=n): - break - sum+=num - num+=1 - if(num>=n): - break - sum+=num - num+=2 - if(num>=n): - break - sum+=num - num+=3 - if(num>=n): - break - sum+=num - -print(sum); diff --git a/Project Euler/Problem 01/sol4.py b/Project Euler/Problem 01/sol4.py deleted file mode 100644 index 7941f5fcd3fe..000000000000 --- a/Project Euler/Problem 01/sol4.py +++ /dev/null @@ -1,30 +0,0 @@ -def mulitples(limit): - xmulti = [] - zmulti = [] - z = 3 - x = 5 - temp = 1 - while True: - result = z * temp - if (result < limit): - zmulti.append(result) - temp += 1 - else: - temp = 1 - break - while True: - result = x * temp - if (result < limit): - xmulti.append(result) - temp += 1 - else: - break - collection = list(set(xmulti+zmulti)) - return (sum(collection)) - - - - - - -print (mulitples(1000)) diff --git a/Project Euler/Problem 02/sol1.py b/Project Euler/Problem 02/sol1.py deleted file mode 100644 index f8257fb615fb..000000000000 --- a/Project Euler/Problem 02/sol1.py +++ /dev/null @@ -1,26 +0,0 @@ -''' -Problem: -Each new term in the Fibonacci sequence is generated by adding the previous two terms. By starting with 1 and 2, -the first 10 terms will be: - 1,2,3,5,8,13,21,34,55,89,.. -By considering the terms in the Fibonacci sequence whose values do not exceed n, find the sum of the even-valued terms. -e.g. for n=10, we have {2,8}, sum is 10. -''' -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 - -n = int(raw_input().strip()) -i=1 -j=2 -sum=0 -while(j<=n): - if((j&1)==0): #can also use (j%2==0) - sum+=j - temp=i - i=j - j=temp+i -print(sum) diff --git a/Project Euler/Problem 02/sol2.py b/Project Euler/Problem 02/sol2.py deleted file mode 100644 index 9bbd0c535d63..000000000000 --- a/Project Euler/Problem 02/sol2.py +++ /dev/null @@ -1,12 +0,0 @@ -def fib(n): - a, b, s = 0, 1, 0 - while b < n: - if b % 2 == 0 and b < n: s += b - a, b = b, a+b - ls.append(s) - -T = int(input().strip()) -ls = [] -for _ in range(T): - fib(int(input().strip())) -print(*ls, sep = '\n') diff --git a/Project Euler/Problem 02/sol3.py b/Project Euler/Problem 02/sol3.py deleted file mode 100644 index d36b741bb4f9..000000000000 --- a/Project Euler/Problem 02/sol3.py +++ /dev/null @@ -1,20 +0,0 @@ -''' -Problem: -Each new term in the Fibonacci sequence is generated by adding the previous two terms. - 0,1,1,2,3,5,8,13,21,34,55,89,.. -Every third term from 0 is even So using this I have written a simple code -By considering the terms in the Fibonacci sequence whose values do not exceed n, find the sum of the even-valued terms. -e.g. for n=10, we have {2,8}, sum is 10. -''' -"""Python 3""" -n = int(input()) -a=0 -b=2 -count=0 -while 4*b+a1): - prime=n -print(prime) diff --git a/Project Euler/Problem 04/sol1.py b/Project Euler/Problem 04/sol1.py deleted file mode 100644 index 77135ec7fc1a..000000000000 --- a/Project Euler/Problem 04/sol1.py +++ /dev/null @@ -1,29 +0,0 @@ -''' -Problem: -A palindromic number reads the same both ways. The largest palindrome made from the product of two 2-digit numbers is 9009 = 91 x 99. -Find the largest palindrome made from the product of two 3-digit numbers which is less than N. -''' -from __future__ import print_function -limit = int(input("limit? ")) - -# fetchs the next number -for number in range(limit-1,10000,-1): - - # converts number into string. - strNumber = str(number) - - # checks whether 'strNumber' is a palindrome. - if(strNumber == strNumber[::-1]): - - divisor = 999 - - # if 'number' is a product of two 3-digit numbers - # then number is the answer otherwise fetch next number. - while(divisor != 99): - - if((number % divisor == 0) and (len(str(number / divisor)) == 3)): - - print(number) - exit(0) - - divisor -=1 \ No newline at end of file diff --git a/Project Euler/Problem 04/sol2.py b/Project Euler/Problem 04/sol2.py deleted file mode 100644 index e27e7d30471e..000000000000 --- a/Project Euler/Problem 04/sol2.py +++ /dev/null @@ -1,19 +0,0 @@ -''' -Problem: -A palindromic number reads the same both ways. The largest palindrome made from the product of two 2-digit numbers is 9009 = 91 x 99. -Find the largest palindrome made from the product of two 3-digit numbers which is less than N. -''' -from __future__ import print_function -arr = [] -for i in range(999,100,-1): - for j in range(999,100,-1): - t = str(i*j) - if t == t[::-1]: - arr.append(i*j) -arr.sort() - -n=int(input()) -for i in arr[::-1]: - if(i LargestProduct: - LargestProduct = product - print(LargestProduct) - - -if __name__ == '__main__': - main() diff --git a/Project Euler/Problem 10/sol1.py b/Project Euler/Problem 10/sol1.py deleted file mode 100644 index 94e5b7362114..000000000000 --- a/Project Euler/Problem 10/sol1.py +++ /dev/null @@ -1,38 +0,0 @@ -from __future__ import print_function -from math import sqrt - -try: - xrange #Python 2 -except NameError: - xrange = range #Python 3 - -def is_prime(n): - for i in xrange(2, int(sqrt(n))+1): - if n%i == 0: - return False - - return True - -def sum_of_primes(n): - if n > 2: - sumOfPrimes = 2 - else: - return 0 - - for i in xrange(3, n, 2): - if is_prime(i): - sumOfPrimes += i - - return sumOfPrimes - -if __name__ == '__main__': - import sys - - if len(sys.argv) == 1: - print(sum_of_primes(2000000)) - else: - try: - n = int(sys.argv[1]) - print(sum_of_primes(n)) - except ValueError: - print('Invalid entry - please enter a number.') diff --git a/Project Euler/Problem 11/sol1.py b/Project Euler/Problem 11/sol1.py deleted file mode 100644 index b882dc449156..000000000000 --- a/Project Euler/Problem 11/sol1.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import print_function -''' -What is the greatest product of four adjacent numbers (horizontally, vertically, or diagonally) in this 20x20 array? - -08 02 22 97 38 15 00 40 00 75 04 05 07 78 52 12 50 77 91 08 -49 49 99 40 17 81 18 57 60 87 17 40 98 43 69 48 04 56 62 00 -81 49 31 73 55 79 14 29 93 71 40 67 53 88 30 03 49 13 36 65 -52 70 95 23 04 60 11 42 69 24 68 56 01 32 56 71 37 02 36 91 -22 31 16 71 51 67 63 89 41 92 36 54 22 40 40 28 66 33 13 80 -24 47 32 60 99 03 45 02 44 75 33 53 78 36 84 20 35 17 12 50 -32 98 81 28 64 23 67 10 26 38 40 67 59 54 70 66 18 38 64 70 -67 26 20 68 02 62 12 20 95 63 94 39 63 08 40 91 66 49 94 21 -24 55 58 05 66 73 99 26 97 17 78 78 96 83 14 88 34 89 63 72 -21 36 23 09 75 00 76 44 20 45 35 14 00 61 33 97 34 31 33 95 -78 17 53 28 22 75 31 67 15 94 03 80 04 62 16 14 09 53 56 92 -16 39 05 42 96 35 31 47 55 58 88 24 00 17 54 24 36 29 85 57 -86 56 00 48 35 71 89 07 05 44 44 37 44 60 21 58 51 54 17 58 -19 80 81 68 05 94 47 69 28 73 92 13 86 52 17 77 04 89 55 40 -04 52 08 83 97 35 99 16 07 97 57 32 16 26 26 79 33 27 98 66 -88 36 68 87 57 62 20 72 03 46 33 67 46 55 12 32 63 93 53 69 -04 42 16 73 38 25 39 11 24 94 72 18 08 46 29 32 40 62 76 36 -20 69 36 41 72 30 23 88 34 62 99 69 82 67 59 85 74 04 36 16 -20 73 35 29 78 31 90 01 74 31 49 71 48 86 81 16 23 57 05 54 -01 70 54 71 83 51 54 69 16 92 33 48 61 43 52 01 89 19 67 48 -''' - -try: - xrange #Python 2 -except NameError: - xrange = range #Python 2 - -def largest_product(grid): - nColumns = len(grid[0]) - nRows = len(grid) - - largest = 0 - lrDiagProduct = 0 - rlDiagProduct = 0 - - #Check vertically, horizontally, diagonally at the same time (only works for nxn grid) - for i in xrange(nColumns): - for j in xrange(nRows-3): - vertProduct = grid[j][i]*grid[j+1][i]*grid[j+2][i]*grid[j+3][i] - horzProduct = grid[i][j]*grid[i][j+1]*grid[i][j+2]*grid[i][j+3] - - #Left-to-right diagonal (\) product - if (i < nColumns-3): - lrDiagProduct = grid[i][j]*grid[i+1][j+1]*grid[i+2][j+2]*grid[i+3][j+3] - - #Right-to-left diagonal(/) product - if (i > 2): - rlDiagProduct = grid[i][j]*grid[i-1][j+1]*grid[i-2][j+2]*grid[i-3][j+3] - - maxProduct = max(vertProduct, horzProduct, lrDiagProduct, rlDiagProduct) - if maxProduct > largest: - largest = maxProduct - - return largest - -if __name__ == '__main__': - grid = [] - with open('grid.txt') as file: - for line in file: - grid.append(line.strip('\n').split(' ')) - - grid = [[int(i) for i in grid[j]] for j in xrange(len(grid))] - - print(largest_product(grid)) \ No newline at end of file diff --git a/Project Euler/Problem 11/sol2.py b/Project Euler/Problem 11/sol2.py deleted file mode 100644 index b03395f01697..000000000000 --- a/Project Euler/Problem 11/sol2.py +++ /dev/null @@ -1,39 +0,0 @@ -def main(): - with open ("grid.txt", "r") as f: - l = [] - for i in range(20): - l.append([int(x) for x in f.readline().split()]) - - maximum = 0 - - # right - for i in range(20): - for j in range(17): - temp = l[i][j] * l[i][j+1] * l[i][j+2] * l[i][j+3] - if temp > maximum: - maximum = temp - - # down - for i in range(17): - for j in range(20): - temp = l[i][j] * l[i+1][j] * l[i+2][j] * l[i+3][j] - if temp > maximum: - maximum = temp - - #diagonal 1 - for i in range(17): - for j in range(17): - temp = l[i][j] * l[i+1][j+1] * l[i+2][j+2] * l[i+3][j+3] - if temp > maximum: - maximum = temp - - #diagonal 2 - for i in range(17): - for j in range(3, 20): - temp = l[i][j] * l[i+1][j-1] * l[i+2][j-2] * l[i+3][j-3] - if temp > maximum: - maximum = temp - print(maximum) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/Project Euler/Problem 12/sol1.py b/Project Euler/Problem 12/sol1.py deleted file mode 100644 index 9c4483fd62e5..000000000000 --- a/Project Euler/Problem 12/sol1.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import print_function -from math import sqrt -''' -Highly divisible triangular numbers -Problem 12 -The sequence of triangle numbers is generated by adding the natural numbers. So the 7th triangle number would be 1 + 2 + 3 + 4 + 5 + 6 + 7 = 28. The first ten terms would be: - -1, 3, 6, 10, 15, 21, 28, 36, 45, 55, ... - -Let us list the factors of the first seven triangle numbers: - - 1: 1 - 3: 1,3 - 6: 1,2,3,6 -10: 1,2,5,10 -15: 1,3,5,15 -21: 1,3,7,21 -28: 1,2,4,7,14,28 -We can see that 28 is the first triangle number to have over five divisors. - -What is the value of the first triangle number to have over five hundred divisors? -''' -try: - xrange #Python 2 -except NameError: - xrange = range #Python 3 - -def count_divisors(n): - nDivisors = 0 - for i in xrange(1, int(sqrt(n))+1): - if n%i == 0: - nDivisors += 2 - - return nDivisors - -tNum = 1 -i = 1 - -while True: - i += 1 - tNum += i - - if count_divisors(tNum) > 500: - break - -print(tNum) \ No newline at end of file diff --git a/Project Euler/Problem 13/sol1.py b/Project Euler/Problem 13/sol1.py deleted file mode 100644 index faaaad5e88c1..000000000000 --- a/Project Euler/Problem 13/sol1.py +++ /dev/null @@ -1,14 +0,0 @@ -''' -Problem Statement: -Work out the first ten digits of the sum of the N 50-digit numbers. -''' -from __future__ import print_function - -n = int(input().strip()) - -array = [] -for i in range(n): - array.append(int(input().strip())) - -print(str(sum(array))[:10]) - diff --git a/Project Euler/Problem 14/sol1.py b/Project Euler/Problem 14/sol1.py deleted file mode 100644 index 9037f6eb8bd5..000000000000 --- a/Project Euler/Problem 14/sol1.py +++ /dev/null @@ -1,21 +0,0 @@ -from __future__ import print_function -largest_number = 0 -pre_counter = 0 - -for input1 in range(750000,1000000): - counter = 1 - number = input1 - - while number > 1: - if number % 2 == 0: - number /=2 - counter += 1 - else: - number = (3*number)+1 - counter += 1 - - if counter > pre_counter: - largest_number = input1 - pre_counter = counter - -print(('Largest Number:',largest_number,'->',pre_counter,'digits')) diff --git a/Project Euler/Problem 15/sol1.py b/Project Euler/Problem 15/sol1.py deleted file mode 100644 index 9b61b37d2081..000000000000 --- a/Project Euler/Problem 15/sol1.py +++ /dev/null @@ -1,20 +0,0 @@ -from __future__ import print_function -from math import factorial, ceil - -def lattice_paths(n): - n = 2*n #middle entry of odd rows starting at row 3 is the solution for n = 1, 2, 3,... - k = n/2 - - return factorial(n)/(factorial(k)*factorial(n-k)) - -if __name__ == '__main__': - import sys - - if len(sys.argv) == 1: - print(lattice_paths(20)) - else: - try: - n = int(sys.argv[1]) - print(lattice_paths(n)) - except ValueError: - print('Invalid entry - please enter a number.') diff --git a/Project Euler/Problem 16/sol1.py b/Project Euler/Problem 16/sol1.py deleted file mode 100644 index 05c7916bd10a..000000000000 --- a/Project Euler/Problem 16/sol1.py +++ /dev/null @@ -1,15 +0,0 @@ -power = int(input("Enter the power of 2: ")) -num = 2**power - -string_num = str(num) - -list_num = list(string_num) - -sum_of_num = 0 - -print("2 ^",power,"=",num) - -for i in list_num: - sum_of_num += int(i) - -print("Sum of the digits are:",sum_of_num) diff --git a/Project Euler/Problem 17/sol1.py b/Project Euler/Problem 17/sol1.py deleted file mode 100644 index 9de5d80b9b29..000000000000 --- a/Project Euler/Problem 17/sol1.py +++ /dev/null @@ -1,35 +0,0 @@ -from __future__ import print_function -''' -Number letter counts -Problem 17 - -If the numbers 1 to 5 are written out in words: one, two, three, four, five, then there are 3 + 3 + 5 + 4 + 4 = 19 letters used in total. - -If all the numbers from 1 to 1000 (one thousand) inclusive were written out in words, how many letters would be used? - - -NOTE: Do not count spaces or hyphens. For example, 342 (three hundred and forty-two) contains 23 letters and 115 (one hundred and fifteen) -contains 20 letters. The use of "and" when writing out numbers is in compliance with British usage. -''' - -ones_counts = [0, 3, 3, 5, 4, 4, 3, 5, 5, 4, 3, 6, 6, 8, 8, 7, 7, 9, 8, 8] #number of letters in zero, one, two, ..., nineteen (0 for zero since it's never said aloud) -tens_counts = [0, 0, 6, 6, 5, 5, 5, 7, 6, 6] #number of letters in twenty, thirty, ..., ninety (0 for numbers less than 20 due to inconsistency in teens) - -count = 0 - -for i in range(1, 1001): - if i < 1000: - if i >= 100: - count += ones_counts[i/100] + 7 #add number of letters for "n hundred" - - if i%100 is not 0: - count += 3 #add number of letters for "and" if number is not multiple of 100 - - if 0 < i%100 < 20: - count += ones_counts[i%100] #add number of letters for one, two, three, ..., nineteen (could be combined with below if not for inconsistency in teens) - else: - count += ones_counts[i%10] + tens_counts[(i%100-i%10)/10] #add number of letters for twenty, twenty one, ..., ninety nine - else: - count += ones_counts[i/1000] + 8 - -print(count) \ No newline at end of file diff --git a/Project Euler/Problem 19/sol1.py b/Project Euler/Problem 19/sol1.py deleted file mode 100644 index 94cf117026a4..000000000000 --- a/Project Euler/Problem 19/sol1.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import print_function -''' -Counting Sundays -Problem 19 - -You are given the following information, but you may prefer to do some research for yourself. - -1 Jan 1900 was a Monday. -Thirty days has September, -April, June and November. -All the rest have thirty-one, -Saving February alone, -Which has twenty-eight, rain or shine. -And on leap years, twenty-nine. - -A leap year occurs on any year evenly divisible by 4, but not on a century unless it is divisible by 400. - -How many Sundays fell on the first of the month during the twentieth century (1 Jan 1901 to 31 Dec 2000)? -''' - -days_per_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] - -day = 6 -month = 1 -year = 1901 - -sundays = 0 - -while year < 2001: - day += 7 - - if (year%4 == 0 and not year%100 == 0) or (year%400 == 0): - if day > days_per_month[month-1] and month is not 2: - month += 1 - day = day-days_per_month[month-2] - elif day > 29 and month is 2: - month += 1 - day = day-29 - else: - if day > days_per_month[month-1]: - month += 1 - day = day-days_per_month[month-2] - - if month > 12: - year += 1 - month = 1 - - if year < 2001 and day is 1: - sundays += 1 - -print(sundays) \ No newline at end of file diff --git a/Project Euler/Problem 20/sol1.py b/Project Euler/Problem 20/sol1.py deleted file mode 100644 index 73e41d5cc8fa..000000000000 --- a/Project Euler/Problem 20/sol1.py +++ /dev/null @@ -1,27 +0,0 @@ -# Finding the factorial. -def factorial(n): - fact = 1 - for i in range(1,n+1): - fact *= i - return fact - -# Spliting the digits and adding it. -def split_and_add(number): - sum_of_digits = 0 - while(number>0): - last_digit = number % 10 - sum_of_digits += last_digit - number = int(number/10) # Removing the last_digit from the given number. - return sum_of_digits - -# Taking the user input. -number = int(input("Enter the Number: ")) - -# Assigning the factorial from the factorial function. -factorial = factorial(number) - -# Spliting and adding the factorial into answer. -answer = split_and_add(factorial) - -# Printing the answer. -print(answer) diff --git a/Project Euler/Problem 20/sol2.py b/Project Euler/Problem 20/sol2.py deleted file mode 100644 index bca9af9cb9ef..000000000000 --- a/Project Euler/Problem 20/sol2.py +++ /dev/null @@ -1,5 +0,0 @@ -from math import factorial -def main(): - print(sum([int(x) for x in str(factorial(100))])) -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/Project Euler/Problem 21/sol1.py b/Project Euler/Problem 21/sol1.py deleted file mode 100644 index 6d137a7d4332..000000000000 --- a/Project Euler/Problem 21/sol1.py +++ /dev/null @@ -1,42 +0,0 @@ -#-.- coding: latin-1 -.- -from __future__ import print_function -from math import sqrt -''' -Amicable Numbers -Problem 21 - -Let d(n) be defined as the sum of proper divisors of n (numbers less than n which divide evenly into n). -If d(a) = b and d(b) = a, where a ≠ b, then a and b are an amicable pair and each of a and b are called amicable numbers. - -For example, the proper divisors of 220 are 1, 2, 4, 5, 10, 11, 20, 22, 44, 55 and 110; therefore d(220) = 284. The proper divisors of 284 are 1, 2, 4, 71 and 142; so d(284) = 220. - -Evaluate the sum of all the amicable numbers under 10000. -''' -try: - xrange #Python 2 -except NameError: - xrange = range #Python 3 - -def sum_of_divisors(n): - total = 0 - for i in xrange(1, int(sqrt(n)+1)): - if n%i == 0 and i != sqrt(n): - total += i + n//i - elif i == sqrt(n): - total += i - - return total-n - -sums = [] -total = 0 - -for i in xrange(1, 10000): - n = sum_of_divisors(i) - - if n < len(sums): - if sums[n-1] == i: - total += n + i - - sums.append(n) - -print(total) \ No newline at end of file diff --git a/Project Euler/Problem 22/sol1.py b/Project Euler/Problem 22/sol1.py deleted file mode 100644 index 7754306583dc..000000000000 --- a/Project Euler/Problem 22/sol1.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: latin-1 -*- -from __future__ import print_function -''' -Name scores -Problem 22 - -Using names.txt (right click and 'Save Link/Target As...'), a 46K text file containing over five-thousand first names, begin by sorting it -into alphabetical order. Then working out the alphabetical value for each name, multiply this value by its alphabetical position in the list -to obtain a name score. - -For example, when the list is sorted into alphabetical order, COLIN, which is worth 3 + 15 + 12 + 9 + 14 = 53, is the 938th name in the list. -So, COLIN would obtain a score of 938 × 53 = 49714. - -What is the total of all the name scores in the file? -''' -try: - xrange #Python 2 -except NameError: - xrange = range #Python 3 - -with open('p022_names.txt') as file: - names = str(file.readlines()[0]) - names = names.replace('"', '').split(',') - -names.sort() - -name_score = 0 -total_score = 0 - -for i, name in enumerate(names): - for letter in name: - name_score += ord(letter) - 64 - - total_score += (i+1)*name_score - name_score = 0 - -print(total_score) \ No newline at end of file diff --git a/Project Euler/Problem 22/sol2.py b/Project Euler/Problem 22/sol2.py deleted file mode 100644 index d7f9abf09d49..000000000000 --- a/Project Euler/Problem 22/sol2.py +++ /dev/null @@ -1,533 +0,0 @@ -def main(): - name = [ - "MARY", "PATRICIA", "LINDA", "BARBARA", "ELIZABETH", "JENNIFER", "MARIA", "SUSAN", "MARGARET", "DOROTHY", - "LISA", "NANCY", "KAREN", "BETTY", "HELEN", "SANDRA", "DONNA", "CAROL", "RUTH", "SHARON", - "MICHELLE", "LAURA", "SARAH", "KIMBERLY", "DEBORAH", "JESSICA", "SHIRLEY", "CYNTHIA", "ANGELA", "MELISSA", - "BRENDA", "AMY", "ANNA", "REBECCA", "VIRGINIA", "KATHLEEN", "PAMELA", "MARTHA", "DEBRA", "AMANDA", - "STEPHANIE", "CAROLYN", "CHRISTINE", "MARIE", "JANET", "CATHERINE", "FRANCES", "ANN", "JOYCE", "DIANE", - "ALICE", "JULIE", "HEATHER", "TERESA", "DORIS", "GLORIA", "EVELYN", "JEAN", "CHERYL", "MILDRED", - "KATHERINE", "JOAN", "ASHLEY", "JUDITH", "ROSE", "JANICE", "KELLY", "NICOLE", "JUDY", "CHRISTINA", - "KATHY", "THERESA", "BEVERLY", "DENISE", "TAMMY", "IRENE", "JANE", "LORI", "RACHEL", "MARILYN", - "ANDREA", "KATHRYN", "LOUISE", "SARA", "ANNE", "JACQUELINE", "WANDA", "BONNIE", "JULIA", "RUBY", - "LOIS", "TINA", "PHYLLIS", "NORMA", "PAULA", "DIANA", "ANNIE", "LILLIAN", "EMILY", "ROBIN", - "PEGGY", "CRYSTAL", "GLADYS", "RITA", "DAWN", "CONNIE", "FLORENCE", "TRACY", "EDNA", "TIFFANY", - "CARMEN", "ROSA", "CINDY", "GRACE", "WENDY", "VICTORIA", "EDITH", "KIM", "SHERRY", "SYLVIA", - "JOSEPHINE", "THELMA", "SHANNON", "SHEILA", "ETHEL", "ELLEN", "ELAINE", "MARJORIE", "CARRIE", "CHARLOTTE", - "MONICA", "ESTHER", "PAULINE", "EMMA", "JUANITA", "ANITA", "RHONDA", "HAZEL", "AMBER", "EVA", - "DEBBIE", "APRIL", "LESLIE", "CLARA", "LUCILLE", "JAMIE", "JOANNE", "ELEANOR", "VALERIE", "DANIELLE", - "MEGAN", "ALICIA", "SUZANNE", "MICHELE", "GAIL", "BERTHA", "DARLENE", "VERONICA", "JILL", "ERIN", - "GERALDINE", "LAUREN", "CATHY", "JOANN", "LORRAINE", "LYNN", "SALLY", "REGINA", "ERICA", "BEATRICE", - "DOLORES", "BERNICE", "AUDREY", "YVONNE", "ANNETTE", "JUNE", "SAMANTHA", "MARION", "DANA", "STACY", - "ANA", "RENEE", "IDA", "VIVIAN", "ROBERTA", "HOLLY", "BRITTANY", "MELANIE", "LORETTA", "YOLANDA", - "JEANETTE", "LAURIE", "KATIE", "KRISTEN", "VANESSA", "ALMA", "SUE", "ELSIE", "BETH", "JEANNE", - "VICKI", "CARLA", "TARA", "ROSEMARY", "EILEEN", "TERRI", "GERTRUDE", "LUCY", "TONYA", "ELLA", - "STACEY", "WILMA", "GINA", "KRISTIN", "JESSIE", "NATALIE", "AGNES", "VERA", "WILLIE", "CHARLENE", - "BESSIE", "DELORES", "MELINDA", "PEARL", "ARLENE", "MAUREEN", "COLLEEN", "ALLISON", "TAMARA", "JOY", - "GEORGIA", "CONSTANCE", "LILLIE", "CLAUDIA", "JACKIE", "MARCIA", "TANYA", "NELLIE", "MINNIE", "MARLENE", - "HEIDI", "GLENDA", "LYDIA", "VIOLA", "COURTNEY", "MARIAN", "STELLA", "CAROLINE", "DORA", "JO", - "VICKIE", "MATTIE", "TERRY", "MAXINE", "IRMA", "MABEL", "MARSHA", "MYRTLE", "LENA", "CHRISTY", - "DEANNA", "PATSY", "HILDA", "GWENDOLYN", "JENNIE", "NORA", "MARGIE", "NINA", "CASSANDRA", "LEAH", - "PENNY", "KAY", "PRISCILLA", "NAOMI", "CAROLE", "BRANDY", "OLGA", "BILLIE", "DIANNE", "TRACEY", - "LEONA", "JENNY", "FELICIA", "SONIA", "MIRIAM", "VELMA", "BECKY", "BOBBIE", "VIOLET", "KRISTINA", - "TONI", "MISTY", "MAE", "SHELLY", "DAISY", "RAMONA", "SHERRI", "ERIKA", "KATRINA", "CLAIRE", - "LINDSEY", "LINDSAY", "GENEVA", "GUADALUPE", "BELINDA", "MARGARITA", "SHERYL", "CORA", "FAYE", "ADA", - "NATASHA", "SABRINA", "ISABEL", "MARGUERITE", "HATTIE", "HARRIET", "MOLLY", "CECILIA", "KRISTI", "BRANDI", - "BLANCHE", "SANDY", "ROSIE", "JOANNA", "IRIS", "EUNICE", "ANGIE", "INEZ", "LYNDA", "MADELINE", - "AMELIA", "ALBERTA", "GENEVIEVE", "MONIQUE", "JODI", "JANIE", "MAGGIE", "KAYLA", "SONYA", "JAN", - "LEE", "KRISTINE", "CANDACE", "FANNIE", "MARYANN", "OPAL", "ALISON", "YVETTE", "MELODY", "LUZ", - "SUSIE", "OLIVIA", "FLORA", "SHELLEY", "KRISTY", "MAMIE", "LULA", "LOLA", "VERNA", "BEULAH", - "ANTOINETTE", "CANDICE", "JUANA", "JEANNETTE", "PAM", "KELLI", "HANNAH", "WHITNEY", "BRIDGET", "KARLA", - "CELIA", "LATOYA", "PATTY", "SHELIA", "GAYLE", "DELLA", "VICKY", "LYNNE", "SHERI", "MARIANNE", - "KARA", "JACQUELYN", "ERMA", "BLANCA", "MYRA", "LETICIA", "PAT", "KRISTA", "ROXANNE", "ANGELICA", - "JOHNNIE", "ROBYN", "FRANCIS", "ADRIENNE", "ROSALIE", "ALEXANDRA", "BROOKE", "BETHANY", "SADIE", "BERNADETTE", - "TRACI", "JODY", "KENDRA", "JASMINE", "NICHOLE", "RACHAEL", "CHELSEA", "MABLE", "ERNESTINE", "MURIEL", - "MARCELLA", "ELENA", "KRYSTAL", "ANGELINA", "NADINE", "KARI", "ESTELLE", "DIANNA", "PAULETTE", "LORA", - "MONA", "DOREEN", "ROSEMARIE", "ANGEL", "DESIREE", "ANTONIA", "HOPE", "GINGER", "JANIS", "BETSY", - "CHRISTIE", "FREDA", "MERCEDES", "MEREDITH", "LYNETTE", "TERI", "CRISTINA", "EULA", "LEIGH", "MEGHAN", - "SOPHIA", "ELOISE", "ROCHELLE", "GRETCHEN", "CECELIA", "RAQUEL", "HENRIETTA", "ALYSSA", "JANA", "KELLEY", - "GWEN", "KERRY", "JENNA", "TRICIA", "LAVERNE", "OLIVE", "ALEXIS", "TASHA", "SILVIA", "ELVIRA", - "CASEY", "DELIA", "SOPHIE", "KATE", "PATTI", "LORENA", "KELLIE", "SONJA", "LILA", "LANA", - "DARLA", "MAY", "MINDY", "ESSIE", "MANDY", "LORENE", "ELSA", "JOSEFINA", "JEANNIE", "MIRANDA", - "DIXIE", "LUCIA", "MARTA", "FAITH", "LELA", "JOHANNA", "SHARI", "CAMILLE", "TAMI", "SHAWNA", - "ELISA", "EBONY", "MELBA", "ORA", "NETTIE", "TABITHA", "OLLIE", "JAIME", "WINIFRED", "KRISTIE", - "MARINA", "ALISHA", "AIMEE", "RENA", "MYRNA", "MARLA", "TAMMIE", "LATASHA", "BONITA", "PATRICE", - "RONDA", "SHERRIE", "ADDIE", "FRANCINE", "DELORIS", "STACIE", "ADRIANA", "CHERI", "SHELBY", "ABIGAIL", - "CELESTE", "JEWEL", "CARA", "ADELE", "REBEKAH", "LUCINDA", "DORTHY", "CHRIS", "EFFIE", "TRINA", - "REBA", "SHAWN", "SALLIE", "AURORA", "LENORA", "ETTA", "LOTTIE", "KERRI", "TRISHA", "NIKKI", - "ESTELLA", "FRANCISCA", "JOSIE", "TRACIE", "MARISSA", "KARIN", "BRITTNEY", "JANELLE", "LOURDES", "LAUREL", - "HELENE", "FERN", "ELVA", "CORINNE", "KELSEY", "INA", "BETTIE", "ELISABETH", "AIDA", "CAITLIN", - "INGRID", "IVA", "EUGENIA", "CHRISTA", "GOLDIE", "CASSIE", "MAUDE", "JENIFER", "THERESE", "FRANKIE", - "DENA", "LORNA", "JANETTE", "LATONYA", "CANDY", "MORGAN", "CONSUELO", "TAMIKA", "ROSETTA", "DEBORA", - "CHERIE", "POLLY", "DINA", "JEWELL", "FAY", "JILLIAN", "DOROTHEA", "NELL", "TRUDY", "ESPERANZA", - "PATRICA", "KIMBERLEY", "SHANNA", "HELENA", "CAROLINA", "CLEO", "STEFANIE", "ROSARIO", "OLA", "JANINE", - "MOLLIE", "LUPE", "ALISA", "LOU", "MARIBEL", "SUSANNE", "BETTE", "SUSANA", "ELISE", "CECILE", - "ISABELLE", "LESLEY", "JOCELYN", "PAIGE", "JONI", "RACHELLE", "LEOLA", "DAPHNE", "ALTA", "ESTER", - "PETRA", "GRACIELA", "IMOGENE", "JOLENE", "KEISHA", "LACEY", "GLENNA", "GABRIELA", "KERI", "URSULA", - "LIZZIE", "KIRSTEN", "SHANA", "ADELINE", "MAYRA", "JAYNE", "JACLYN", "GRACIE", "SONDRA", "CARMELA", - "MARISA", "ROSALIND", "CHARITY", "TONIA", "BEATRIZ", "MARISOL", "CLARICE", "JEANINE", "SHEENA", "ANGELINE", - "FRIEDA", "LILY", "ROBBIE", "SHAUNA", "MILLIE", "CLAUDETTE", "CATHLEEN", "ANGELIA", "GABRIELLE", "AUTUMN", - "KATHARINE", "SUMMER", "JODIE", "STACI", "LEA", "CHRISTI", "JIMMIE", "JUSTINE", "ELMA", "LUELLA", - "MARGRET", "DOMINIQUE", "SOCORRO", "RENE", "MARTINA", "MARGO", "MAVIS", "CALLIE", "BOBBI", "MARITZA", - "LUCILE", "LEANNE", "JEANNINE", "DEANA", "AILEEN", "LORIE", "LADONNA", "WILLA", "MANUELA", "GALE", - "SELMA", "DOLLY", "SYBIL", "ABBY", "LARA", "DALE", "IVY", "DEE", "WINNIE", "MARCY", - "LUISA", "JERI", "MAGDALENA", "OFELIA", "MEAGAN", "AUDRA", "MATILDA", "LEILA", "CORNELIA", "BIANCA", - "SIMONE", "BETTYE", "RANDI", "VIRGIE", "LATISHA", "BARBRA", "GEORGINA", "ELIZA", "LEANN", "BRIDGETTE", - "RHODA", "HALEY", "ADELA", "NOLA", "BERNADINE", "FLOSSIE", "ILA", "GRETA", "RUTHIE", "NELDA", - "MINERVA", "LILLY", "TERRIE", "LETHA", "HILARY", "ESTELA", "VALARIE", "BRIANNA", "ROSALYN", "EARLINE", - "CATALINA", "AVA", "MIA", "CLARISSA", "LIDIA", "CORRINE", "ALEXANDRIA", "CONCEPCION", "TIA", "SHARRON", - "RAE", "DONA", "ERICKA", "JAMI", "ELNORA", "CHANDRA", "LENORE", "NEVA", "MARYLOU", "MELISA", - "TABATHA", "SERENA", "AVIS", "ALLIE", "SOFIA", "JEANIE", "ODESSA", "NANNIE", "HARRIETT", "LORAINE", - "PENELOPE", "MILAGROS", "EMILIA", "BENITA", "ALLYSON", "ASHLEE", "TANIA", "TOMMIE", "ESMERALDA", "KARINA", - "EVE", "PEARLIE", "ZELMA", "MALINDA", "NOREEN", "TAMEKA", "SAUNDRA", "HILLARY", "AMIE", "ALTHEA", - "ROSALINDA", "JORDAN", "LILIA", "ALANA", "GAY", "CLARE", "ALEJANDRA", "ELINOR", "MICHAEL", "LORRIE", - "JERRI", "DARCY", "EARNESTINE", "CARMELLA", "TAYLOR", "NOEMI", "MARCIE", "LIZA", "ANNABELLE", "LOUISA", - "EARLENE", "MALLORY", "CARLENE", "NITA", "SELENA", "TANISHA", "KATY", "JULIANNE", "JOHN", "LAKISHA", - "EDWINA", "MARICELA", "MARGERY", "KENYA", "DOLLIE", "ROXIE", "ROSLYN", "KATHRINE", "NANETTE", "CHARMAINE", - "LAVONNE", "ILENE", "KRIS", "TAMMI", "SUZETTE", "CORINE", "KAYE", "JERRY", "MERLE", "CHRYSTAL", - "LINA", "DEANNE", "LILIAN", "JULIANA", "ALINE", "LUANN", "KASEY", "MARYANNE", "EVANGELINE", "COLETTE", - "MELVA", "LAWANDA", "YESENIA", "NADIA", "MADGE", "KATHIE", "EDDIE", "OPHELIA", "VALERIA", "NONA", - "MITZI", "MARI", "GEORGETTE", "CLAUDINE", "FRAN", "ALISSA", "ROSEANN", "LAKEISHA", "SUSANNA", "REVA", - "DEIDRE", "CHASITY", "SHEREE", "CARLY", "JAMES", "ELVIA", "ALYCE", "DEIRDRE", "GENA", "BRIANA", - "ARACELI", "KATELYN", "ROSANNE", "WENDI", "TESSA", "BERTA", "MARVA", "IMELDA", "MARIETTA", "MARCI", - "LEONOR", "ARLINE", "SASHA", "MADELYN", "JANNA", "JULIETTE", "DEENA", "AURELIA", "JOSEFA", "AUGUSTA", - "LILIANA", "YOUNG", "CHRISTIAN", "LESSIE", "AMALIA", "SAVANNAH", "ANASTASIA", "VILMA", "NATALIA", "ROSELLA", - "LYNNETTE", "CORINA", "ALFREDA", "LEANNA", "CAREY", "AMPARO", "COLEEN", "TAMRA", "AISHA", "WILDA", - "KARYN", "CHERRY", "QUEEN", "MAURA", "MAI", "EVANGELINA", "ROSANNA", "HALLIE", "ERNA", "ENID", - "MARIANA", "LACY", "JULIET", "JACKLYN", "FREIDA", "MADELEINE", "MARA", "HESTER", "CATHRYN", "LELIA", - "CASANDRA", "BRIDGETT", "ANGELITA", "JANNIE", "DIONNE", "ANNMARIE", "KATINA", "BERYL", "PHOEBE", "MILLICENT", - "KATHERYN", "DIANN", "CARISSA", "MARYELLEN", "LIZ", "LAURI", "HELGA", "GILDA", "ADRIAN", "RHEA", - "MARQUITA", "HOLLIE", "TISHA", "TAMERA", "ANGELIQUE", "FRANCESCA", "BRITNEY", "KAITLIN", "LOLITA", "FLORINE", - "ROWENA", "REYNA", "TWILA", "FANNY", "JANELL", "INES", "CONCETTA", "BERTIE", "ALBA", "BRIGITTE", - "ALYSON", "VONDA", "PANSY", "ELBA", "NOELLE", "LETITIA", "KITTY", "DEANN", "BRANDIE", "LOUELLA", - "LETA", "FELECIA", "SHARLENE", "LESA", "BEVERLEY", "ROBERT", "ISABELLA", "HERMINIA", "TERRA", "CELINA", - "TORI", "OCTAVIA", "JADE", "DENICE", "GERMAINE", "SIERRA", "MICHELL", "CORTNEY", "NELLY", "DORETHA", - "SYDNEY", "DEIDRA", "MONIKA", "LASHONDA", "JUDI", "CHELSEY", "ANTIONETTE", "MARGOT", "BOBBY", "ADELAIDE", - "NAN", "LEEANN", "ELISHA", "DESSIE", "LIBBY", "KATHI", "GAYLA", "LATANYA", "MINA", "MELLISA", - "KIMBERLEE", "JASMIN", "RENAE", "ZELDA", "ELDA", "MA", "JUSTINA", "GUSSIE", "EMILIE", "CAMILLA", - "ABBIE", "ROCIO", "KAITLYN", "JESSE", "EDYTHE", "ASHLEIGH", "SELINA", "LAKESHA", "GERI", "ALLENE", - "PAMALA", "MICHAELA", "DAYNA", "CARYN", "ROSALIA", "SUN", "JACQULINE", "REBECA", "MARYBETH", "KRYSTLE", - "IOLA", "DOTTIE", "BENNIE", "BELLE", "AUBREY", "GRISELDA", "ERNESTINA", "ELIDA", "ADRIANNE", "DEMETRIA", - "DELMA", "CHONG", "JAQUELINE", "DESTINY", "ARLEEN", "VIRGINA", "RETHA", "FATIMA", "TILLIE", "ELEANORE", - "CARI", "TREVA", "BIRDIE", "WILHELMINA", "ROSALEE", "MAURINE", "LATRICE", "YONG", "JENA", "TARYN", - "ELIA", "DEBBY", "MAUDIE", "JEANNA", "DELILAH", "CATRINA", "SHONDA", "HORTENCIA", "THEODORA", "TERESITA", - "ROBBIN", "DANETTE", "MARYJANE", "FREDDIE", "DELPHINE", "BRIANNE", "NILDA", "DANNA", "CINDI", "BESS", - "IONA", "HANNA", "ARIEL", "WINONA", "VIDA", "ROSITA", "MARIANNA", "WILLIAM", "RACHEAL", "GUILLERMINA", - "ELOISA", "CELESTINE", "CAREN", "MALISSA", "LONA", "CHANTEL", "SHELLIE", "MARISELA", "LEORA", "AGATHA", - "SOLEDAD", "MIGDALIA", "IVETTE", "CHRISTEN", "ATHENA", "JANEL", "CHLOE", "VEDA", "PATTIE", "TESSIE", - "TERA", "MARILYNN", "LUCRETIA", "KARRIE", "DINAH", "DANIELA", "ALECIA", "ADELINA", "VERNICE", "SHIELA", - "PORTIA", "MERRY", "LASHAWN", "DEVON", "DARA", "TAWANA", "OMA", "VERDA", "CHRISTIN", "ALENE", - "ZELLA", "SANDI", "RAFAELA", "MAYA", "KIRA", "CANDIDA", "ALVINA", "SUZAN", "SHAYLA", "LYN", - "LETTIE", "ALVA", "SAMATHA", "ORALIA", "MATILDE", "MADONNA", "LARISSA", "VESTA", "RENITA", "INDIA", - "DELOIS", "SHANDA", "PHILLIS", "LORRI", "ERLINDA", "CRUZ", "CATHRINE", "BARB", "ZOE", "ISABELL", - "IONE", "GISELA", "CHARLIE", "VALENCIA", "ROXANNA", "MAYME", "KISHA", "ELLIE", "MELLISSA", "DORRIS", - "DALIA", "BELLA", "ANNETTA", "ZOILA", "RETA", "REINA", "LAURETTA", "KYLIE", "CHRISTAL", "PILAR", - "CHARLA", "ELISSA", "TIFFANI", "TANA", "PAULINA", "LEOTA", "BREANNA", "JAYME", "CARMEL", "VERNELL", - "TOMASA", "MANDI", "DOMINGA", "SANTA", "MELODIE", "LURA", "ALEXA", "TAMELA", "RYAN", "MIRNA", - "KERRIE", "VENUS", "NOEL", "FELICITA", "CRISTY", "CARMELITA", "BERNIECE", "ANNEMARIE", "TIARA", "ROSEANNE", - "MISSY", "CORI", "ROXANA", "PRICILLA", "KRISTAL", "JUNG", "ELYSE", "HAYDEE", "ALETHA", "BETTINA", - "MARGE", "GILLIAN", "FILOMENA", "CHARLES", "ZENAIDA", "HARRIETTE", "CARIDAD", "VADA", "UNA", "ARETHA", - "PEARLINE", "MARJORY", "MARCELA", "FLOR", "EVETTE", "ELOUISE", "ALINA", "TRINIDAD", "DAVID", "DAMARIS", - "CATHARINE", "CARROLL", "BELVA", "NAKIA", "MARLENA", "LUANNE", "LORINE", "KARON", "DORENE", "DANITA", - "BRENNA", "TATIANA", "SAMMIE", "LOUANN", "LOREN", "JULIANNA", "ANDRIA", "PHILOMENA", "LUCILA", "LEONORA", - "DOVIE", "ROMONA", "MIMI", "JACQUELIN", "GAYE", "TONJA", "MISTI", "JOE", "GENE", "CHASTITY", - "STACIA", "ROXANN", "MICAELA", "NIKITA", "MEI", "VELDA", "MARLYS", "JOHNNA", "AURA", "LAVERN", - "IVONNE", "HAYLEY", "NICKI", "MAJORIE", "HERLINDA", "GEORGE", "ALPHA", "YADIRA", "PERLA", "GREGORIA", - "DANIEL", "ANTONETTE", "SHELLI", "MOZELLE", "MARIAH", "JOELLE", "CORDELIA", "JOSETTE", "CHIQUITA", "TRISTA", - "LOUIS", "LAQUITA", "GEORGIANA", "CANDI", "SHANON", "LONNIE", "HILDEGARD", "CECIL", "VALENTINA", "STEPHANY", - "MAGDA", "KAROL", "GERRY", "GABRIELLA", "TIANA", "ROMA", "RICHELLE", "RAY", "PRINCESS", "OLETA", - "JACQUE", "IDELLA", "ALAINA", "SUZANNA", "JOVITA", "BLAIR", "TOSHA", "RAVEN", "NEREIDA", "MARLYN", - "KYLA", "JOSEPH", "DELFINA", "TENA", "STEPHENIE", "SABINA", "NATHALIE", "MARCELLE", "GERTIE", "DARLEEN", - "THEA", "SHARONDA", "SHANTEL", "BELEN", "VENESSA", "ROSALINA", "ONA", "GENOVEVA", "COREY", "CLEMENTINE", - "ROSALBA", "RENATE", "RENATA", "MI", "IVORY", "GEORGIANNA", "FLOY", "DORCAS", "ARIANA", "TYRA", - "THEDA", "MARIAM", "JULI", "JESICA", "DONNIE", "VIKKI", "VERLA", "ROSELYN", "MELVINA", "JANNETTE", - "GINNY", "DEBRAH", "CORRIE", "ASIA", "VIOLETA", "MYRTIS", "LATRICIA", "COLLETTE", "CHARLEEN", "ANISSA", - "VIVIANA", "TWYLA", "PRECIOUS", "NEDRA", "LATONIA", "LAN", "HELLEN", "FABIOLA", "ANNAMARIE", "ADELL", - "SHARYN", "CHANTAL", "NIKI", "MAUD", "LIZETTE", "LINDY", "KIA", "KESHA", "JEANA", "DANELLE", - "CHARLINE", "CHANEL", "CARROL", "VALORIE", "LIA", "DORTHA", "CRISTAL", "SUNNY", "LEONE", "LEILANI", - "GERRI", "DEBI", "ANDRA", "KESHIA", "IMA", "EULALIA", "EASTER", "DULCE", "NATIVIDAD", "LINNIE", - "KAMI", "GEORGIE", "CATINA", "BROOK", "ALDA", "WINNIFRED", "SHARLA", "RUTHANN", "MEAGHAN", "MAGDALENE", - "LISSETTE", "ADELAIDA", "VENITA", "TRENA", "SHIRLENE", "SHAMEKA", "ELIZEBETH", "DIAN", "SHANTA", "MICKEY", - "LATOSHA", "CARLOTTA", "WINDY", "SOON", "ROSINA", "MARIANN", "LEISA", "JONNIE", "DAWNA", "CATHIE", - "BILLY", "ASTRID", "SIDNEY", "LAUREEN", "JANEEN", "HOLLI", "FAWN", "VICKEY", "TERESSA", "SHANTE", - "RUBYE", "MARCELINA", "CHANDA", "CARY", "TERESE", "SCARLETT", "MARTY", "MARNIE", "LULU", "LISETTE", - "JENIFFER", "ELENOR", "DORINDA", "DONITA", "CARMAN", "BERNITA", "ALTAGRACIA", "ALETA", "ADRIANNA", "ZORAIDA", - "RONNIE", "NICOLA", "LYNDSEY", "KENDALL", "JANINA", "CHRISSY", "AMI", "STARLA", "PHYLIS", "PHUONG", - "KYRA", "CHARISSE", "BLANCH", "SANJUANITA", "RONA", "NANCI", "MARILEE", "MARANDA", "CORY", "BRIGETTE", - "SANJUANA", "MARITA", "KASSANDRA", "JOYCELYN", "IRA", "FELIPA", "CHELSIE", "BONNY", "MIREYA", "LORENZA", - "KYONG", "ILEANA", "CANDELARIA", "TONY", "TOBY", "SHERIE", "OK", "MARK", "LUCIE", "LEATRICE", - "LAKESHIA", "GERDA", "EDIE", "BAMBI", "MARYLIN", "LAVON", "HORTENSE", "GARNET", "EVIE", "TRESSA", - "SHAYNA", "LAVINA", "KYUNG", "JEANETTA", "SHERRILL", "SHARA", "PHYLISS", "MITTIE", "ANABEL", "ALESIA", - "THUY", "TAWANDA", "RICHARD", "JOANIE", "TIFFANIE", "LASHANDA", "KARISSA", "ENRIQUETA", "DARIA", "DANIELLA", - "CORINNA", "ALANNA", "ABBEY", "ROXANE", "ROSEANNA", "MAGNOLIA", "LIDA", "KYLE", "JOELLEN", "ERA", - "CORAL", "CARLEEN", "TRESA", "PEGGIE", "NOVELLA", "NILA", "MAYBELLE", "JENELLE", "CARINA", "NOVA", - "MELINA", "MARQUERITE", "MARGARETTE", "JOSEPHINA", "EVONNE", "DEVIN", "CINTHIA", "ALBINA", "TOYA", "TAWNYA", - "SHERITA", "SANTOS", "MYRIAM", "LIZABETH", "LISE", "KEELY", "JENNI", "GISELLE", "CHERYLE", "ARDITH", - "ARDIS", "ALESHA", "ADRIANE", "SHAINA", "LINNEA", "KAROLYN", "HONG", "FLORIDA", "FELISHA", "DORI", - "DARCI", "ARTIE", "ARMIDA", "ZOLA", "XIOMARA", "VERGIE", "SHAMIKA", "NENA", "NANNETTE", "MAXIE", - "LOVIE", "JEANE", "JAIMIE", "INGE", "FARRAH", "ELAINA", "CAITLYN", "STARR", "FELICITAS", "CHERLY", - "CARYL", "YOLONDA", "YASMIN", "TEENA", "PRUDENCE", "PENNIE", "NYDIA", "MACKENZIE", "ORPHA", "MARVEL", - "LIZBETH", "LAURETTE", "JERRIE", "HERMELINDA", "CAROLEE", "TIERRA", "MIRIAN", "META", "MELONY", "KORI", - "JENNETTE", "JAMILA", "ENA", "ANH", "YOSHIKO", "SUSANNAH", "SALINA", "RHIANNON", "JOLEEN", "CRISTINE", - "ASHTON", "ARACELY", "TOMEKA", "SHALONDA", "MARTI", "LACIE", "KALA", "JADA", "ILSE", "HAILEY", - "BRITTANI", "ZONA", "SYBLE", "SHERRYL", "RANDY", "NIDIA", "MARLO", "KANDICE", "KANDI", "DEB", - "DEAN", "AMERICA", "ALYCIA", "TOMMY", "RONNA", "NORENE", "MERCY", "JOSE", "INGEBORG", "GIOVANNA", - "GEMMA", "CHRISTEL", "AUDRY", "ZORA", "VITA", "VAN", "TRISH", "STEPHAINE", "SHIRLEE", "SHANIKA", - "MELONIE", "MAZIE", "JAZMIN", "INGA", "HOA", "HETTIE", "GERALYN", "FONDA", "ESTRELLA", "ADELLA", - "SU", "SARITA", "RINA", "MILISSA", "MARIBETH", "GOLDA", "EVON", "ETHELYN", "ENEDINA", "CHERISE", - "CHANA", "VELVA", "TAWANNA", "SADE", "MIRTA", "LI", "KARIE", "JACINTA", "ELNA", "DAVINA", - "CIERRA", "ASHLIE", "ALBERTHA", "TANESHA", "STEPHANI", "NELLE", "MINDI", "LU", "LORINDA", "LARUE", - "FLORENE", "DEMETRA", "DEDRA", "CIARA", "CHANTELLE", "ASHLY", "SUZY", "ROSALVA", "NOELIA", "LYDA", - "LEATHA", "KRYSTYNA", "KRISTAN", "KARRI", "DARLINE", "DARCIE", "CINDA", "CHEYENNE", "CHERRIE", "AWILDA", - "ALMEDA", "ROLANDA", "LANETTE", "JERILYN", "GISELE", "EVALYN", "CYNDI", "CLETA", "CARIN", "ZINA", - "ZENA", "VELIA", "TANIKA", "PAUL", "CHARISSA", "THOMAS", "TALIA", "MARGARETE", "LAVONDA", "KAYLEE", - "KATHLENE", "JONNA", "IRENA", "ILONA", "IDALIA", "CANDIS", "CANDANCE", "BRANDEE", "ANITRA", "ALIDA", - "SIGRID", "NICOLETTE", "MARYJO", "LINETTE", "HEDWIG", "CHRISTIANA", "CASSIDY", "ALEXIA", "TRESSIE", "MODESTA", - "LUPITA", "LITA", "GLADIS", "EVELIA", "DAVIDA", "CHERRI", "CECILY", "ASHELY", "ANNABEL", "AGUSTINA", - "WANITA", "SHIRLY", "ROSAURA", "HULDA", "EUN", "BAILEY", "YETTA", "VERONA", "THOMASINA", "SIBYL", - "SHANNAN", "MECHELLE", "LUE", "LEANDRA", "LANI", "KYLEE", "KANDY", "JOLYNN", "FERNE", "EBONI", - "CORENE", "ALYSIA", "ZULA", "NADA", "MOIRA", "LYNDSAY", "LORRETTA", "JUAN", "JAMMIE", "HORTENSIA", - "GAYNELL", "CAMERON", "ADRIA", "VINA", "VICENTA", "TANGELA", "STEPHINE", "NORINE", "NELLA", "LIANA", - "LESLEE", "KIMBERELY", "ILIANA", "GLORY", "FELICA", "EMOGENE", "ELFRIEDE", "EDEN", "EARTHA", "CARMA", - "BEA", "OCIE", "MARRY", "LENNIE", "KIARA", "JACALYN", "CARLOTA", "ARIELLE", "YU", "STAR", - "OTILIA", "KIRSTIN", "KACEY", "JOHNETTA", "JOEY", "JOETTA", "JERALDINE", "JAUNITA", "ELANA", "DORTHEA", - "CAMI", "AMADA", "ADELIA", "VERNITA", "TAMAR", "SIOBHAN", "RENEA", "RASHIDA", "OUIDA", "ODELL", - "NILSA", "MERYL", "KRISTYN", "JULIETA", "DANICA", "BREANNE", "AUREA", "ANGLEA", "SHERRON", "ODETTE", - "MALIA", "LORELEI", "LIN", "LEESA", "KENNA", "KATHLYN", "FIONA", "CHARLETTE", "SUZIE", "SHANTELL", - "SABRA", "RACQUEL", "MYONG", "MIRA", "MARTINE", "LUCIENNE", "LAVADA", "JULIANN", "JOHNIE", "ELVERA", - "DELPHIA", "CLAIR", "CHRISTIANE", "CHAROLETTE", "CARRI", "AUGUSTINE", "ASHA", "ANGELLA", "PAOLA", "NINFA", - "LEDA", "LAI", "EDA", "SUNSHINE", "STEFANI", "SHANELL", "PALMA", "MACHELLE", "LISSA", "KECIA", - "KATHRYNE", "KARLENE", "JULISSA", "JETTIE", "JENNIFFER", "HUI", "CORRINA", "CHRISTOPHER", "CAROLANN", "ALENA", - "TESS", "ROSARIA", "MYRTICE", "MARYLEE", "LIANE", "KENYATTA", "JUDIE", "JANEY", "IN", "ELMIRA", - "ELDORA", "DENNA", "CRISTI", "CATHI", "ZAIDA", "VONNIE", "VIVA", "VERNIE", "ROSALINE", "MARIELA", - "LUCIANA", "LESLI", "KARAN", "FELICE", "DENEEN", "ADINA", "WYNONA", "TARSHA", "SHERON", "SHASTA", - "SHANITA", "SHANI", "SHANDRA", "RANDA", "PINKIE", "PARIS", "NELIDA", "MARILOU", "LYLA", "LAURENE", - "LACI", "JOI", "JANENE", "DOROTHA", "DANIELE", "DANI", "CAROLYNN", "CARLYN", "BERENICE", "AYESHA", - "ANNELIESE", "ALETHEA", "THERSA", "TAMIKO", "RUFINA", "OLIVA", "MOZELL", "MARYLYN", "MADISON", "KRISTIAN", - "KATHYRN", "KASANDRA", "KANDACE", "JANAE", "GABRIEL", "DOMENICA", "DEBBRA", "DANNIELLE", "CHUN", "BUFFY", - "BARBIE", "ARCELIA", "AJA", "ZENOBIA", "SHAREN", "SHAREE", "PATRICK", "PAGE", "MY", "LAVINIA", - "KUM", "KACIE", "JACKELINE", "HUONG", "FELISA", "EMELIA", "ELEANORA", "CYTHIA", "CRISTIN", "CLYDE", - "CLARIBEL", "CARON", "ANASTACIA", "ZULMA", "ZANDRA", "YOKO", "TENISHA", "SUSANN", "SHERILYN", "SHAY", - "SHAWANDA", "SABINE", "ROMANA", "MATHILDA", "LINSEY", "KEIKO", "JOANA", "ISELA", "GRETTA", "GEORGETTA", - "EUGENIE", "DUSTY", "DESIRAE", "DELORA", "CORAZON", "ANTONINA", "ANIKA", "WILLENE", "TRACEE", "TAMATHA", - "REGAN", "NICHELLE", "MICKIE", "MAEGAN", "LUANA", "LANITA", "KELSIE", "EDELMIRA", "BREE", "AFTON", - "TEODORA", "TAMIE", "SHENA", "MEG", "LINH", "KELI", "KACI", "DANYELLE", "BRITT", "ARLETTE", - "ALBERTINE", "ADELLE", "TIFFINY", "STORMY", "SIMONA", "NUMBERS", "NICOLASA", "NICHOL", "NIA", "NAKISHA", - "MEE", "MAIRA", "LOREEN", "KIZZY", "JOHNNY", "JAY", "FALLON", "CHRISTENE", "BOBBYE", "ANTHONY", - "YING", "VINCENZA", "TANJA", "RUBIE", "RONI", "QUEENIE", "MARGARETT", "KIMBERLI", "IRMGARD", "IDELL", - "HILMA", "EVELINA", "ESTA", "EMILEE", "DENNISE", "DANIA", "CARL", "CARIE", "ANTONIO", "WAI", - "SANG", "RISA", "RIKKI", "PARTICIA", "MUI", "MASAKO", "MARIO", "LUVENIA", "LOREE", "LONI", - "LIEN", "KEVIN", "GIGI", "FLORENCIA", "DORIAN", "DENITA", "DALLAS", "CHI", "BILLYE", "ALEXANDER", - "TOMIKA", "SHARITA", "RANA", "NIKOLE", "NEOMA", "MARGARITE", "MADALYN", "LUCINA", "LAILA", "KALI", - "JENETTE", "GABRIELE", "EVELYNE", "ELENORA", "CLEMENTINA", "ALEJANDRINA", "ZULEMA", "VIOLETTE", "VANNESSA", "THRESA", - "RETTA", "PIA", "PATIENCE", "NOELLA", "NICKIE", "JONELL", "DELTA", "CHUNG", "CHAYA", "CAMELIA", - "BETHEL", "ANYA", "ANDREW", "THANH", "SUZANN", "SPRING", "SHU", "MILA", "LILLA", "LAVERNA", - "KEESHA", "KATTIE", "GIA", "GEORGENE", "EVELINE", "ESTELL", "ELIZBETH", "VIVIENNE", "VALLIE", "TRUDIE", - "STEPHANE", "MICHEL", "MAGALY", "MADIE", "KENYETTA", "KARREN", "JANETTA", "HERMINE", "HARMONY", "DRUCILLA", - "DEBBI", "CELESTINA", "CANDIE", "BRITNI", "BECKIE", "AMINA", "ZITA", "YUN", "YOLANDE", "VIVIEN", - "VERNETTA", "TRUDI", "SOMMER", "PEARLE", "PATRINA", "OSSIE", "NICOLLE", "LOYCE", "LETTY", "LARISA", - "KATHARINA", "JOSELYN", "JONELLE", "JENELL", "IESHA", "HEIDE", "FLORINDA", "FLORENTINA", "FLO", "ELODIA", - "DORINE", "BRUNILDA", "BRIGID", "ASHLI", "ARDELLA", "TWANA", "THU", "TARAH", "SUNG", "SHEA", - "SHAVON", "SHANE", "SERINA", "RAYNA", "RAMONITA", "NGA", "MARGURITE", "LUCRECIA", "KOURTNEY", "KATI", - "JESUS", "JESENIA", "DIAMOND", "CRISTA", "AYANA", "ALICA", "ALIA", "VINNIE", "SUELLEN", "ROMELIA", - "RACHELL", "PIPER", "OLYMPIA", "MICHIKO", "KATHALEEN", "JOLIE", "JESSI", "JANESSA", "HANA", "HA", - "ELEASE", "CARLETTA", "BRITANY", "SHONA", "SALOME", "ROSAMOND", "REGENA", "RAINA", "NGOC", "NELIA", - "LOUVENIA", "LESIA", "LATRINA", "LATICIA", "LARHONDA", "JINA", "JACKI", "HOLLIS", "HOLLEY", "EMMY", - "DEEANN", "CORETTA", "ARNETTA", "VELVET", "THALIA", "SHANICE", "NETA", "MIKKI", "MICKI", "LONNA", - "LEANA", "LASHUNDA", "KILEY", "JOYE", "JACQULYN", "IGNACIA", "HYUN", "HIROKO", "HENRY", "HENRIETTE", - "ELAYNE", "DELINDA", "DARNELL", "DAHLIA", "COREEN", "CONSUELA", "CONCHITA", "CELINE", "BABETTE", "AYANNA", - "ANETTE", "ALBERTINA", "SKYE", "SHAWNEE", "SHANEKA", "QUIANA", "PAMELIA", "MIN", "MERRI", "MERLENE", - "MARGIT", "KIESHA", "KIERA", "KAYLENE", "JODEE", "JENISE", "ERLENE", "EMMIE", "ELSE", "DARYL", - "DALILA", "DAISEY", "CODY", "CASIE", "BELIA", "BABARA", "VERSIE", "VANESA", "SHELBA", "SHAWNDA", - "SAM", "NORMAN", "NIKIA", "NAOMA", "MARNA", "MARGERET", "MADALINE", "LAWANA", "KINDRA", "JUTTA", - "JAZMINE", "JANETT", "HANNELORE", "GLENDORA", "GERTRUD", "GARNETT", "FREEDA", "FREDERICA", "FLORANCE", "FLAVIA", - "DENNIS", "CARLINE", "BEVERLEE", "ANJANETTE", "VALDA", "TRINITY", "TAMALA", "STEVIE", "SHONNA", "SHA", - "SARINA", "ONEIDA", "MICAH", "MERILYN", "MARLEEN", "LURLINE", "LENNA", "KATHERIN", "JIN", "JENI", - "HAE", "GRACIA", "GLADY", "FARAH", "ERIC", "ENOLA", "EMA", "DOMINQUE", "DEVONA", "DELANA", - "CECILA", "CAPRICE", "ALYSHA", "ALI", "ALETHIA", "VENA", "THERESIA", "TAWNY", "SONG", "SHAKIRA", - "SAMARA", "SACHIKO", "RACHELE", "PAMELLA", "NICKY", "MARNI", "MARIEL", "MAREN", "MALISA", "LIGIA", - "LERA", "LATORIA", "LARAE", "KIMBER", "KATHERN", "KAREY", "JENNEFER", "JANETH", "HALINA", "FREDIA", - "DELISA", "DEBROAH", "CIERA", "CHIN", "ANGELIKA", "ANDREE", "ALTHA", "YEN", "VIVAN", "TERRESA", - "TANNA", "SUK", "SUDIE", "SOO", "SIGNE", "SALENA", "RONNI", "REBBECCA", "MYRTIE", "MCKENZIE", - "MALIKA", "MAIDA", "LOAN", "LEONARDA", "KAYLEIGH", "FRANCE", "ETHYL", "ELLYN", "DAYLE", "CAMMIE", - "BRITTNI", "BIRGIT", "AVELINA", "ASUNCION", "ARIANNA", "AKIKO", "VENICE", "TYESHA", "TONIE", "TIESHA", - "TAKISHA", "STEFFANIE", "SINDY", "SANTANA", "MEGHANN", "MANDA", "MACIE", "LADY", "KELLYE", "KELLEE", - "JOSLYN", "JASON", "INGER", "INDIRA", "GLINDA", "GLENNIS", "FERNANDA", "FAUSTINA", "ENEIDA", "ELICIA", - "DOT", "DIGNA", "DELL", "ARLETTA", "ANDRE", "WILLIA", "TAMMARA", "TABETHA", "SHERRELL", "SARI", - "REFUGIO", "REBBECA", "PAULETTA", "NIEVES", "NATOSHA", "NAKITA", "MAMMIE", "KENISHA", "KAZUKO", "KASSIE", - "GARY", "EARLEAN", "DAPHINE", "CORLISS", "CLOTILDE", "CAROLYNE", "BERNETTA", "AUGUSTINA", "AUDREA", "ANNIS", - "ANNABELL", "YAN", "TENNILLE", "TAMICA", "SELENE", "SEAN", "ROSANA", "REGENIA", "QIANA", "MARKITA", - "MACY", "LEEANNE", "LAURINE", "KYM", "JESSENIA", "JANITA", "GEORGINE", "GENIE", "EMIKO", "ELVIE", - "DEANDRA", "DAGMAR", "CORIE", "COLLEN", "CHERISH", "ROMAINE", "PORSHA", "PEARLENE", "MICHELINE", "MERNA", - "MARGORIE", "MARGARETTA", "LORE", "KENNETH", "JENINE", "HERMINA", "FREDERICKA", "ELKE", "DRUSILLA", "DORATHY", - "DIONE", "DESIRE", "CELENA", "BRIGIDA", "ANGELES", "ALLEGRA", "THEO", "TAMEKIA", "SYNTHIA", "STEPHEN", - "SOOK", "SLYVIA", "ROSANN", "REATHA", "RAYE", "MARQUETTA", "MARGART", "LING", "LAYLA", "KYMBERLY", - "KIANA", "KAYLEEN", "KATLYN", "KARMEN", "JOELLA", "IRINA", "EMELDA", "ELENI", "DETRA", "CLEMMIE", - "CHERYLL", "CHANTELL", "CATHEY", "ARNITA", "ARLA", "ANGLE", "ANGELIC", "ALYSE", "ZOFIA", "THOMASINE", - "TENNIE", "SON", "SHERLY", "SHERLEY", "SHARYL", "REMEDIOS", "PETRINA", "NICKOLE", "MYUNG", "MYRLE", - "MOZELLA", "LOUANNE", "LISHA", "LATIA", "LANE", "KRYSTA", "JULIENNE", "JOEL", "JEANENE", "JACQUALINE", - "ISAURA", "GWENDA", "EARLEEN", "DONALD", "CLEOPATRA", "CARLIE", "AUDIE", "ANTONIETTA", "ALISE", "ALEX", - "VERDELL", "VAL", "TYLER", "TOMOKO", "THAO", "TALISHA", "STEVEN", "SO", "SHEMIKA", "SHAUN", - "SCARLET", "SAVANNA", "SANTINA", "ROSIA", "RAEANN", "ODILIA", "NANA", "MINNA", "MAGAN", "LYNELLE", - "LE", "KARMA", "JOEANN", "IVANA", "INELL", "ILANA", "HYE", "HONEY", "HEE", "GUDRUN", - "FRANK", "DREAMA", "CRISSY", "CHANTE", "CARMELINA", "ARVILLA", "ARTHUR", "ANNAMAE", "ALVERA", "ALEIDA", - "AARON", "YEE", "YANIRA", "VANDA", "TIANNA", "TAM", "STEFANIA", "SHIRA", "PERRY", "NICOL", - "NANCIE", "MONSERRATE", "MINH", "MELYNDA", "MELANY", "MATTHEW", "LOVELLA", "LAURE", "KIRBY", "KACY", - "JACQUELYNN", "HYON", "GERTHA", "FRANCISCO", "ELIANA", "CHRISTENA", "CHRISTEEN", "CHARISE", "CATERINA", "CARLEY", - "CANDYCE", "ARLENA", "AMMIE", "YANG", "WILLETTE", "VANITA", "TUYET", "TINY", "SYREETA", "SILVA", - "SCOTT", "RONALD", "PENNEY", "NYLA", "MICHAL", "MAURICE", "MARYAM", "MARYA", "MAGEN", "LUDIE", - "LOMA", "LIVIA", "LANELL", "KIMBERLIE", "JULEE", "DONETTA", "DIEDRA", "DENISHA", "DEANE", "DAWNE", - "CLARINE", "CHERRYL", "BRONWYN", "BRANDON", "ALLA", "VALERY", "TONDA", "SUEANN", "SORAYA", "SHOSHANA", - "SHELA", "SHARLEEN", "SHANELLE", "NERISSA", "MICHEAL", "MERIDITH", "MELLIE", "MAYE", "MAPLE", "MAGARET", - "LUIS", "LILI", "LEONILA", "LEONIE", "LEEANNA", "LAVONIA", "LAVERA", "KRISTEL", "KATHEY", "KATHE", - "JUSTIN", "JULIAN", "JIMMY", "JANN", "ILDA", "HILDRED", "HILDEGARDE", "GENIA", "FUMIKO", "EVELIN", - "ERMELINDA", "ELLY", "DUNG", "DOLORIS", "DIONNA", "DANAE", "BERNEICE", "ANNICE", "ALIX", "VERENA", - "VERDIE", "TRISTAN", "SHAWNNA", "SHAWANA", "SHAUNNA", "ROZELLA", "RANDEE", "RANAE", "MILAGRO", "LYNELL", - "LUISE", "LOUIE", "LOIDA", "LISBETH", "KARLEEN", "JUNITA", "JONA", "ISIS", "HYACINTH", "HEDY", - "GWENN", "ETHELENE", "ERLINE", "EDWARD", "DONYA", "DOMONIQUE", "DELICIA", "DANNETTE", "CICELY", "BRANDA", - "BLYTHE", "BETHANN", "ASHLYN", "ANNALEE", "ALLINE", "YUKO", "VELLA", "TRANG", "TOWANDA", "TESHA", - "SHERLYN", "NARCISA", "MIGUELINA", "MERI", "MAYBELL", "MARLANA", "MARGUERITA", "MADLYN", "LUNA", "LORY", - "LORIANN", "LIBERTY", "LEONORE", "LEIGHANN", "LAURICE", "LATESHA", "LARONDA", "KATRICE", "KASIE", "KARL", - "KALEY", "JADWIGA", "GLENNIE", "GEARLDINE", "FRANCINA", "EPIFANIA", "DYAN", "DORIE", "DIEDRE", "DENESE", - "DEMETRICE", "DELENA", "DARBY", "CRISTIE", "CLEORA", "CATARINA", "CARISA", "BERNIE", "BARBERA", "ALMETA", - "TRULA", "TEREASA", "SOLANGE", "SHEILAH", "SHAVONNE", "SANORA", "ROCHELL", "MATHILDE", "MARGARETA", "MAIA", - "LYNSEY", "LAWANNA", "LAUNA", "KENA", "KEENA", "KATIA", "JAMEY", "GLYNDA", "GAYLENE", "ELVINA", - "ELANOR", "DANUTA", "DANIKA", "CRISTEN", "CORDIE", "COLETTA", "CLARITA", "CARMON", "BRYNN", "AZUCENA", - "AUNDREA", "ANGELE", "YI", "WALTER", "VERLIE", "VERLENE", "TAMESHA", "SILVANA", "SEBRINA", "SAMIRA", - "REDA", "RAYLENE", "PENNI", "PANDORA", "NORAH", "NOMA", "MIREILLE", "MELISSIA", "MARYALICE", "LARAINE", - "KIMBERY", "KARYL", "KARINE", "KAM", "JOLANDA", "JOHANA", "JESUSA", "JALEESA", "JAE", "JACQUELYNE", - "IRISH", "ILUMINADA", "HILARIA", "HANH", "GENNIE", "FRANCIE", "FLORETTA", "EXIE", "EDDA", "DREMA", - "DELPHA", "BEV", "BARBAR", "ASSUNTA", "ARDELL", "ANNALISA", "ALISIA", "YUKIKO", "YOLANDO", "WONDA", - "WEI", "WALTRAUD", "VETA", "TEQUILA", "TEMEKA", "TAMEIKA", "SHIRLEEN", "SHENITA", "PIEDAD", "OZELLA", - "MIRTHA", "MARILU", "KIMIKO", "JULIANE", "JENICE", "JEN", "JANAY", "JACQUILINE", "HILDE", "FE", - "FAE", "EVAN", "EUGENE", "ELOIS", "ECHO", "DEVORAH", "CHAU", "BRINDA", "BETSEY", "ARMINDA", - "ARACELIS", "APRYL", "ANNETT", "ALISHIA", "VEOLA", "USHA", "TOSHIKO", "THEOLA", "TASHIA", "TALITHA", - "SHERY", "RUDY", "RENETTA", "REIKO", "RASHEEDA", "OMEGA", "OBDULIA", "MIKA", "MELAINE", "MEGGAN", - "MARTIN", "MARLEN", "MARGET", "MARCELINE", "MANA", "MAGDALEN", "LIBRADA", "LEZLIE", "LEXIE", "LATASHIA", - "LASANDRA", "KELLE", "ISIDRA", "ISA", "INOCENCIA", "GWYN", "FRANCOISE", "ERMINIA", "ERINN", "DIMPLE", - "DEVORA", "CRISELDA", "ARMANDA", "ARIE", "ARIANE", "ANGELO", "ANGELENA", "ALLEN", "ALIZA", "ADRIENE", - "ADALINE", "XOCHITL", "TWANNA", "TRAN", "TOMIKO", "TAMISHA", "TAISHA", "SUSY", "SIU", "RUTHA", - "ROXY", "RHONA", "RAYMOND", "OTHA", "NORIKO", "NATASHIA", "MERRIE", "MELVIN", "MARINDA", "MARIKO", - "MARGERT", "LORIS", "LIZZETTE", "LEISHA", "KAILA", "KA", "JOANNIE", "JERRICA", "JENE", "JANNET", - "JANEE", "JACINDA", "HERTA", "ELENORE", "DORETTA", "DELAINE", "DANIELL", "CLAUDIE", "CHINA", "BRITTA", - "APOLONIA", "AMBERLY", "ALEASE", "YURI", "YUK", "WEN", "WANETA", "UTE", "TOMI", "SHARRI", - "SANDIE", "ROSELLE", "REYNALDA", "RAGUEL", "PHYLICIA", "PATRIA", "OLIMPIA", "ODELIA", "MITZIE", "MITCHELL", - "MISS", "MINDA", "MIGNON", "MICA", "MENDY", "MARIVEL", "MAILE", "LYNETTA", "LAVETTE", "LAURYN", - "LATRISHA", "LAKIESHA", "KIERSTEN", "KARY", "JOSPHINE", "JOLYN", "JETTA", "JANISE", "JACQUIE", "IVELISSE", - "GLYNIS", "GIANNA", "GAYNELLE", "EMERALD", "DEMETRIUS", "DANYELL", "DANILLE", "DACIA", "CORALEE", "CHER", - "CEOLA", "BRETT", "BELL", "ARIANNE", "ALESHIA", "YUNG", "WILLIEMAE", "TROY", "TRINH", "THORA", - "TAI", "SVETLANA", "SHERIKA", "SHEMEKA", "SHAUNDA", "ROSELINE", "RICKI", "MELDA", "MALLIE", "LAVONNA", - "LATINA", "LARRY", "LAQUANDA", "LALA", "LACHELLE", "KLARA", "KANDIS", "JOHNA", "JEANMARIE", "JAYE", - "HANG", "GRAYCE", "GERTUDE", "EMERITA", "EBONIE", "CLORINDA", "CHING", "CHERY", "CAROLA", "BREANN", - "BLOSSOM", "BERNARDINE", "BECKI", "ARLETHA", "ARGELIA", "ARA", "ALITA", "YULANDA", "YON", "YESSENIA", - "TOBI", "TASIA", "SYLVIE", "SHIRL", "SHIRELY", "SHERIDAN", "SHELLA", "SHANTELLE", "SACHA", "ROYCE", - "REBECKA", "REAGAN", "PROVIDENCIA", "PAULENE", "MISHA", "MIKI", "MARLINE", "MARICA", "LORITA", "LATOYIA", - "LASONYA", "KERSTIN", "KENDA", "KEITHA", "KATHRIN", "JAYMIE", "JACK", "GRICELDA", "GINETTE", "ERYN", - "ELINA", "ELFRIEDA", "DANYEL", "CHEREE", "CHANELLE", "BARRIE", "AVERY", "AURORE", "ANNAMARIA", "ALLEEN", - "AILENE", "AIDE", "YASMINE", "VASHTI", "VALENTINE", "TREASA", "TORY", "TIFFANEY", "SHERYLL", "SHARIE", - "SHANAE", "SAU", "RAISA", "PA", "NEDA", "MITSUKO", "MIRELLA", "MILDA", "MARYANNA", "MARAGRET", - "MABELLE", "LUETTA", "LORINA", "LETISHA", "LATARSHA", "LANELLE", "LAJUANA", "KRISSY", "KARLY", "KARENA", - "JON", "JESSIKA", "JERICA", "JEANELLE", "JANUARY", "JALISA", "JACELYN", "IZOLA", "IVEY", "GREGORY", - "EUNA", "ETHA", "DREW", "DOMITILA", "DOMINICA", "DAINA", "CREOLA", "CARLI", "CAMIE", "BUNNY", - "BRITTNY", "ASHANTI", "ANISHA", "ALEEN", "ADAH", "YASUKO", "WINTER", "VIKI", "VALRIE", "TONA", - "TINISHA", "THI", "TERISA", "TATUM", "TANEKA", "SIMONNE", "SHALANDA", "SERITA", "RESSIE", "REFUGIA", - "PAZ", "OLENE", "NA", "MERRILL", "MARGHERITA", "MANDIE", "MAN", "MAIRE", "LYNDIA", "LUCI", - "LORRIANE", "LORETA", "LEONIA", "LAVONA", "LASHAWNDA", "LAKIA", "KYOKO", "KRYSTINA", "KRYSTEN", "KENIA", - "KELSI", "JUDE", "JEANICE", "ISOBEL", "GEORGIANN", "GENNY", "FELICIDAD", "EILENE", "DEON", "DELOISE", - "DEEDEE", "DANNIE", "CONCEPTION", "CLORA", "CHERILYN", "CHANG", "CALANDRA", "BERRY", "ARMANDINA", "ANISA", - "ULA", "TIMOTHY", "TIERA", "THERESSA", "STEPHANIA", "SIMA", "SHYLA", "SHONTA", "SHERA", "SHAQUITA", - "SHALA", "SAMMY", "ROSSANA", "NOHEMI", "NERY", "MORIAH", "MELITA", "MELIDA", "MELANI", "MARYLYNN", - "MARISHA", "MARIETTE", "MALORIE", "MADELENE", "LUDIVINA", "LORIA", "LORETTE", "LORALEE", "LIANNE", "LEON", - "LAVENIA", "LAURINDA", "LASHON", "KIT", "KIMI", "KEILA", "KATELYNN", "KAI", "JONE", "JOANE", - "JI", "JAYNA", "JANELLA", "JA", "HUE", "HERTHA", "FRANCENE", "ELINORE", "DESPINA", "DELSIE", - "DEEDRA", "CLEMENCIA", "CARRY", "CAROLIN", "CARLOS", "BULAH", "BRITTANIE", "BOK", "BLONDELL", "BIBI", - "BEAULAH", "BEATA", "ANNITA", "AGRIPINA", "VIRGEN", "VALENE", "UN", "TWANDA", "TOMMYE", "TOI", - "TARRA", "TARI", "TAMMERA", "SHAKIA", "SADYE", "RUTHANNE", "ROCHEL", "RIVKA", "PURA", "NENITA", - "NATISHA", "MING", "MERRILEE", "MELODEE", "MARVIS", "LUCILLA", "LEENA", "LAVETA", "LARITA", "LANIE", - "KEREN", "ILEEN", "GEORGEANN", "GENNA", "GENESIS", "FRIDA", "EWA", "EUFEMIA", "EMELY", "ELA", - "EDYTH", "DEONNA", "DEADRA", "DARLENA", "CHANELL", "CHAN", "CATHERN", "CASSONDRA", "CASSAUNDRA", "BERNARDA", - "BERNA", "ARLINDA", "ANAMARIA", "ALBERT", "WESLEY", "VERTIE", "VALERI", "TORRI", "TATYANA", "STASIA", - "SHERISE", "SHERILL", "SEASON", "SCOTTIE", "SANDA", "RUTHE", "ROSY", "ROBERTO", "ROBBI", "RANEE", - "QUYEN", "PEARLY", "PALMIRA", "ONITA", "NISHA", "NIESHA", "NIDA", "NEVADA", "NAM", "MERLYN", - "MAYOLA", "MARYLOUISE", "MARYLAND", "MARX", "MARTH", "MARGENE", "MADELAINE", "LONDA", "LEONTINE", "LEOMA", - "LEIA", "LAWRENCE", "LAURALEE", "LANORA", "LAKITA", "KIYOKO", "KETURAH", "KATELIN", "KAREEN", "JONIE", - "JOHNETTE", "JENEE", "JEANETT", "IZETTA", "HIEDI", "HEIKE", "HASSIE", "HAROLD", "GIUSEPPINA", "GEORGANN", - "FIDELA", "FERNANDE", "ELWANDA", "ELLAMAE", "ELIZ", "DUSTI", "DOTTY", "CYNDY", "CORALIE", "CELESTA", - "ARGENTINA", "ALVERTA", "XENIA", "WAVA", "VANETTA", "TORRIE", "TASHINA", "TANDY", "TAMBRA", "TAMA", - "STEPANIE", "SHILA", "SHAUNTA", "SHARAN", "SHANIQUA", "SHAE", "SETSUKO", "SERAFINA", "SANDEE", "ROSAMARIA", - "PRISCILA", "OLINDA", "NADENE", "MUOI", "MICHELINA", "MERCEDEZ", "MARYROSE", "MARIN", "MARCENE", "MAO", - "MAGALI", "MAFALDA", "LOGAN", "LINN", "LANNIE", "KAYCE", "KAROLINE", "KAMILAH", "KAMALA", "JUSTA", - "JOLINE", "JENNINE", "JACQUETTA", "IRAIDA", "GERALD", "GEORGEANNA", "FRANCHESCA", "FAIRY", "EMELINE", "ELANE", - "EHTEL", "EARLIE", "DULCIE", "DALENE", "CRIS", "CLASSIE", "CHERE", "CHARIS", "CAROYLN", "CARMINA", - "CARITA", "BRIAN", "BETHANIE", "AYAKO", "ARICA", "AN", "ALYSA", "ALESSANDRA", "AKILAH", "ADRIEN", - "ZETTA", "YOULANDA", "YELENA", "YAHAIRA", "XUAN", "WENDOLYN", "VICTOR", "TIJUANA", "TERRELL", "TERINA", - "TERESIA", "SUZI", "SUNDAY", "SHERELL", "SHAVONDA", "SHAUNTE", "SHARDA", "SHAKITA", "SENA", "RYANN", - "RUBI", "RIVA", "REGINIA", "REA", "RACHAL", "PARTHENIA", "PAMULA", "MONNIE", "MONET", "MICHAELE", - "MELIA", "MARINE", "MALKA", "MAISHA", "LISANDRA", "LEO", "LEKISHA", "LEAN", "LAURENCE", "LAKENDRA", - "KRYSTIN", "KORTNEY", "KIZZIE", "KITTIE", "KERA", "KENDAL", "KEMBERLY", "KANISHA", "JULENE", "JULE", - "JOSHUA", "JOHANNE", "JEFFREY", "JAMEE", "HAN", "HALLEY", "GIDGET", "GALINA", "FREDRICKA", "FLETA", - "FATIMAH", "EUSEBIA", "ELZA", "ELEONORE", "DORTHEY", "DORIA", "DONELLA", "DINORAH", "DELORSE", "CLARETHA", - "CHRISTINIA", "CHARLYN", "BONG", "BELKIS", "AZZIE", "ANDERA", "AIKO", "ADENA", "YER", "YAJAIRA", - "WAN", "VANIA", "ULRIKE", "TOSHIA", "TIFANY", "STEFANY", "SHIZUE", "SHENIKA", "SHAWANNA", "SHAROLYN", - "SHARILYN", "SHAQUANA", "SHANTAY", "SEE", "ROZANNE", "ROSELEE", "RICKIE", "REMONA", "REANNA", "RAELENE", - "QUINN", "PHUNG", "PETRONILA", "NATACHA", "NANCEY", "MYRL", "MIYOKO", "MIESHA", "MERIDETH", "MARVELLA", - "MARQUITTA", "MARHTA", "MARCHELLE", "LIZETH", "LIBBIE", "LAHOMA", "LADAWN", "KINA", "KATHELEEN", "KATHARYN", - "KARISA", "KALEIGH", "JUNIE", "JULIEANN", "JOHNSIE", "JANEAN", "JAIMEE", "JACKQUELINE", "HISAKO", "HERMA", - "HELAINE", "GWYNETH", "GLENN", "GITA", "EUSTOLIA", "EMELINA", "ELIN", "EDRIS", "DONNETTE", "DONNETTA", - "DIERDRE", "DENAE", "DARCEL", "CLAUDE", "CLARISA", "CINDERELLA", "CHIA", "CHARLESETTA", "CHARITA", "CELSA", - "CASSY", "CASSI", "CARLEE", "BRUNA", "BRITTANEY", "BRANDE", "BILLI", "BAO", "ANTONETTA", "ANGLA", - "ANGELYN", "ANALISA", "ALANE", "WENONA", "WENDIE", "VERONIQUE", "VANNESA", "TOBIE", "TEMPIE", "SUMIKO", - "SULEMA", "SPARKLE", "SOMER", "SHEBA", "SHAYNE", "SHARICE", "SHANEL", "SHALON", "SAGE", "ROY", - "ROSIO", "ROSELIA", "RENAY", "REMA", "REENA", "PORSCHE", "PING", "PEG", "OZIE", "ORETHA", - "ORALEE", "ODA", "NU", "NGAN", "NAKESHA", "MILLY", "MARYBELLE", "MARLIN", "MARIS", "MARGRETT", - "MARAGARET", "MANIE", "LURLENE", "LILLIA", "LIESELOTTE", "LAVELLE", "LASHAUNDA", "LAKEESHA", "KEITH", "KAYCEE", - "KALYN", "JOYA", "JOETTE", "JENAE", "JANIECE", "ILLA", "GRISEL", "GLAYDS", "GENEVIE", "GALA", - "FREDDA", "FRED", "ELMER", "ELEONOR", "DEBERA", "DEANDREA", "DAN", "CORRINNE", "CORDIA", "CONTESSA", - "COLENE", "CLEOTILDE", "CHARLOTT", "CHANTAY", "CECILLE", "BEATRIS", "AZALEE", "ARLEAN", "ARDATH", "ANJELICA", - "ANJA", "ALFREDIA", "ALEISHA", "ADAM", "ZADA", "YUONNE", "XIAO", "WILLODEAN", "WHITLEY", "VENNIE", - "VANNA", "TYISHA", "TOVA", "TORIE", "TONISHA", "TILDA", "TIEN", "TEMPLE", "SIRENA", "SHERRIL", - "SHANTI", "SHAN", "SENAIDA", "SAMELLA", "ROBBYN", "RENDA", "REITA", "PHEBE", "PAULITA", "NOBUKO", - "NGUYET", "NEOMI", "MOON", "MIKAELA", "MELANIA", "MAXIMINA", "MARG", "MAISIE", "LYNNA", "LILLI", - "LAYNE", "LASHAUN", "LAKENYA", "LAEL", "KIRSTIE", "KATHLINE", "KASHA", "KARLYN", "KARIMA", "JOVAN", - "JOSEFINE", "JENNELL", "JACQUI", "JACKELYN", "HYO", "HIEN", "GRAZYNA", "FLORRIE", "FLORIA", "ELEONORA", - "DWANA", "DORLA", "DONG", "DELMY", "DEJA", "DEDE", "DANN", "CRYSTA", "CLELIA", "CLARIS", - "CLARENCE", "CHIEKO", "CHERLYN", "CHERELLE", "CHARMAIN", "CHARA", "CAMMY", "BEE", "ARNETTE", "ARDELLE", - "ANNIKA", "AMIEE", "AMEE", "ALLENA", "YVONE", "YUKI", "YOSHIE", "YEVETTE", "YAEL", "WILLETTA", - "VONCILE", "VENETTA", "TULA", "TONETTE", "TIMIKA", "TEMIKA", "TELMA", "TEISHA", "TAREN", "TA", - "STACEE", "SHIN", "SHAWNTA", "SATURNINA", "RICARDA", "POK", "PASTY", "ONIE", "NUBIA", "MORA", - "MIKE", "MARIELLE", "MARIELLA", "MARIANELA", "MARDELL", "MANY", "LUANNA", "LOISE", "LISABETH", "LINDSY", - "LILLIANA", "LILLIAM", "LELAH", "LEIGHA", "LEANORA", "LANG", "KRISTEEN", "KHALILAH", "KEELEY", "KANDRA", - "JUNKO", "JOAQUINA", "JERLENE", "JANI", "JAMIKA", "JAME", "HSIU", "HERMILA", "GOLDEN", "GENEVIVE", - "EVIA", "EUGENA", "EMMALINE", "ELFREDA", "ELENE", "DONETTE", "DELCIE", "DEEANNA", "DARCEY", "CUC", - "CLARINDA", "CIRA", "CHAE", "CELINDA", "CATHERYN", "CATHERIN", "CASIMIRA", "CARMELIA", "CAMELLIA", "BREANA", - "BOBETTE", "BERNARDINA", "BEBE", "BASILIA", "ARLYNE", "AMAL", "ALAYNA", "ZONIA", "ZENIA", "YURIKO", - "YAEKO", "WYNELL", "WILLOW", "WILLENA", "VERNIA", "TU", "TRAVIS", "TORA", "TERRILYN", "TERICA", - "TENESHA", "TAWNA", "TAJUANA", "TAINA", "STEPHNIE", "SONA", "SOL", "SINA", "SHONDRA", "SHIZUKO", - "SHERLENE", "SHERICE", "SHARIKA", "ROSSIE", "ROSENA", "RORY", "RIMA", "RIA", "RHEBA", "RENNA", - "PETER", "NATALYA", "NANCEE", "MELODI", "MEDA", "MAXIMA", "MATHA", "MARKETTA", "MARICRUZ", "MARCELENE", - "MALVINA", "LUBA", "LOUETTA", "LEIDA", "LECIA", "LAURAN", "LASHAWNA", "LAINE", "KHADIJAH", "KATERINE", - "KASI", "KALLIE", "JULIETTA", "JESUSITA", "JESTINE", "JESSIA", "JEREMY", "JEFFIE", "JANYCE", "ISADORA", - "GEORGIANNE", "FIDELIA", "EVITA", "EURA", "EULAH", "ESTEFANA", "ELSY", "ELIZABET", "ELADIA", "DODIE", - "DION", "DIA", "DENISSE", "DELORAS", "DELILA", "DAYSI", "DAKOTA", "CURTIS", "CRYSTLE", "CONCHA", - "COLBY", "CLARETTA", "CHU", "CHRISTIA", "CHARLSIE", "CHARLENA", "CARYLON", "BETTYANN", "ASLEY", "ASHLEA", - "AMIRA", "AI", "AGUEDA", "AGNUS", "YUETTE", "VINITA", "VICTORINA", "TYNISHA", "TREENA", "TOCCARA", - "TISH", "THOMASENA", "TEGAN", "SOILA", "SHILOH", "SHENNA", "SHARMAINE", "SHANTAE", "SHANDI", "SEPTEMBER", - "SARAN", "SARAI", "SANA", "SAMUEL", "SALLEY", "ROSETTE", "ROLANDE", "REGINE", "OTELIA", "OSCAR", - "OLEVIA", "NICHOLLE", "NECOLE", "NAIDA", "MYRTA", "MYESHA", "MITSUE", "MINTA", "MERTIE", "MARGY", - "MAHALIA", "MADALENE", "LOVE", "LOURA", "LOREAN", "LEWIS", "LESHA", "LEONIDA", "LENITA", "LAVONE", - "LASHELL", "LASHANDRA", "LAMONICA", "KIMBRA", "KATHERINA", "KARRY", "KANESHA", "JULIO", "JONG", "JENEVA", - "JAQUELYN", "HWA", "GILMA", "GHISLAINE", "GERTRUDIS", "FRANSISCA", "FERMINA", "ETTIE", "ETSUKO", "ELLIS", - "ELLAN", "ELIDIA", "EDRA", "DORETHEA", "DOREATHA", "DENYSE", "DENNY", "DEETTA", "DAINE", "CYRSTAL", - "CORRIN", "CAYLA", "CARLITA", "CAMILA", "BURMA", "BULA", "BUENA", "BLAKE", "BARABARA", "AVRIL", - "AUSTIN", "ALAINE", "ZANA", "WILHEMINA", "WANETTA", "VIRGIL", "VI", "VERONIKA", "VERNON", "VERLINE", - "VASILIKI", "TONITA", "TISA", "TEOFILA", "TAYNA", "TAUNYA", "TANDRA", "TAKAKO", "SUNNI", "SUANNE", - "SIXTA", "SHARELL", "SEEMA", "RUSSELL", "ROSENDA", "ROBENA", "RAYMONDE", "PEI", "PAMILA", "OZELL", - "NEIDA", "NEELY", "MISTIE", "MICHA", "MERISSA", "MAURITA", "MARYLN", "MARYETTA", "MARSHALL", "MARCELL", - "MALENA", "MAKEDA", "MADDIE", "LOVETTA", "LOURIE", "LORRINE", "LORILEE", "LESTER", "LAURENA", "LASHAY", - "LARRAINE", "LAREE", "LACRESHA", "KRISTLE", "KRISHNA", "KEVA", "KEIRA", "KAROLE", "JOIE", "JINNY", - "JEANNETTA", "JAMA", "HEIDY", "GILBERTE", "GEMA", "FAVIOLA", "EVELYNN", "ENDA", "ELLI", "ELLENA", - "DIVINA", "DAGNY", "COLLENE", "CODI", "CINDIE", "CHASSIDY", "CHASIDY", "CATRICE", "CATHERINA", "CASSEY", - "CAROLL", "CARLENA", "CANDRA", "CALISTA", "BRYANNA", "BRITTENY", "BEULA", "BARI", "AUDRIE", "AUDRIA", - "ARDELIA", "ANNELLE", "ANGILA", "ALONA", "ALLYN", "DOUGLAS", "ROGER", "JONATHAN", "RALPH", "NICHOLAS", - "BENJAMIN", "BRUCE", "HARRY", "WAYNE", "STEVE", "HOWARD", "ERNEST", "PHILLIP", "TODD", "CRAIG", - "ALAN", "PHILIP", "EARL", "DANNY", "BRYAN", "STANLEY", "LEONARD", "NATHAN", "MANUEL", "RODNEY", - "MARVIN", "VINCENT", "JEFFERY", "JEFF", "CHAD", "JACOB", "ALFRED", "BRADLEY", "HERBERT", "FREDERICK", - "EDWIN", "DON", "RICKY", "RANDALL", "BARRY", "BERNARD", "LEROY", "MARCUS", "THEODORE", "CLIFFORD", - "MIGUEL", "JIM", "TOM", "CALVIN", "BILL", "LLOYD", "DEREK", "WARREN", "DARRELL", "JEROME", - "FLOYD", "ALVIN", "TIM", "GORDON", "GREG", "JORGE", "DUSTIN", "PEDRO", "DERRICK", "ZACHARY", - "HERMAN", "GLEN", "HECTOR", "RICARDO", "RICK", "BRENT", "RAMON", "GILBERT", "MARC", "REGINALD", - "RUBEN", "NATHANIEL", "RAFAEL", "EDGAR", "MILTON", "RAUL", "BEN", "CHESTER", "DUANE", "FRANKLIN", - "BRAD", "RON", "ROLAND", "ARNOLD", "HARVEY", "JARED", "ERIK", "DARRYL", "NEIL", "JAVIER", - "FERNANDO", "CLINTON", "TED", "MATHEW", "TYRONE", "DARREN", "LANCE", "KURT", "ALLAN", "NELSON", - "GUY", "CLAYTON", "HUGH", "MAX", "DWAYNE", "DWIGHT", "ARMANDO", "FELIX", "EVERETT", "IAN", - "WALLACE", "KEN", "BOB", "ALFREDO", "ALBERTO", "DAVE", "IVAN", "BYRON", "ISAAC", "MORRIS", - "CLIFTON", "WILLARD", "ROSS", "ANDY", "SALVADOR", "KIRK", "SERGIO", "SETH", "KENT", "TERRANCE", - "EDUARDO", "TERRENCE", "ENRIQUE", "WADE", "STUART", "FREDRICK", "ARTURO", "ALEJANDRO", "NICK", "LUTHER", - "WENDELL", "JEREMIAH", "JULIUS", "OTIS", "TREVOR", "OLIVER", "LUKE", "HOMER", "GERARD", "DOUG", - "KENNY", "HUBERT", "LYLE", "MATT", "ALFONSO", "ORLANDO", "REX", "CARLTON", "ERNESTO", "NEAL", - "PABLO", "LORENZO", "OMAR", "WILBUR", "GRANT", "HORACE", "RODERICK", "ABRAHAM", "WILLIS", "RICKEY", - "ANDRES", "CESAR", "JOHNATHAN", "MALCOLM", "RUDOLPH", "DAMON", "KELVIN", "PRESTON", "ALTON", "ARCHIE", - "MARCO", "WM", "PETE", "RANDOLPH", "GARRY", "GEOFFREY", "JONATHON", "FELIPE", "GERARDO", "ED", - "DOMINIC", "DELBERT", "COLIN", "GUILLERMO", "EARNEST", "LUCAS", "BENNY", "SPENCER", "RODOLFO", "MYRON", - "EDMUND", "GARRETT", "SALVATORE", "CEDRIC", "LOWELL", "GREGG", "SHERMAN", "WILSON", "SYLVESTER", "ROOSEVELT", - "ISRAEL", "JERMAINE", "FORREST", "WILBERT", "LELAND", "SIMON", "CLARK", "IRVING", "BRYANT", "OWEN", - "RUFUS", "WOODROW", "KRISTOPHER", "MACK", "LEVI", "MARCOS", "GUSTAVO", "JAKE", "LIONEL", "GILBERTO", - "CLINT", "NICOLAS", "ISMAEL", "ORVILLE", "ERVIN", "DEWEY", "AL", "WILFRED", "JOSH", "HUGO", - "IGNACIO", "CALEB", "TOMAS", "SHELDON", "ERICK", "STEWART", "DOYLE", "DARREL", "ROGELIO", "TERENCE", - "SANTIAGO", "ALONZO", "ELIAS", "BERT", "ELBERT", "RAMIRO", "CONRAD", "NOAH", "GRADY", "PHIL", - "CORNELIUS", "LAMAR", "ROLANDO", "CLAY", "PERCY", "DEXTER", "BRADFORD", "DARIN", "AMOS", "MOSES", - "IRVIN", "SAUL", "ROMAN", "RANDAL", "TIMMY", "DARRIN", "WINSTON", "BRENDAN", "ABEL", "DOMINICK", - "BOYD", "EMILIO", "ELIJAH", "DOMINGO", "EMMETT", "MARLON", "EMANUEL", "JERALD", "EDMOND", "EMIL", - "DEWAYNE", "WILL", "OTTO", "TEDDY", "REYNALDO", "BRET", "JESS", "TRENT", "HUMBERTO", "EMMANUEL", - "STEPHAN", "VICENTE", "LAMONT", "GARLAND", "MILES", "EFRAIN", "HEATH", "RODGER", "HARLEY", "ETHAN", - "ELDON", "ROCKY", "PIERRE", "JUNIOR", "FREDDY", "ELI", "BRYCE", "ANTOINE", "STERLING", "CHASE", - "GROVER", "ELTON", "CLEVELAND", "DYLAN", "CHUCK", "DAMIAN", "REUBEN", "STAN", "AUGUST", "LEONARDO", - "JASPER", "RUSSEL", "ERWIN", "BENITO", "HANS", "MONTE", "BLAINE", "ERNIE", "CURT", "QUENTIN", - "AGUSTIN", "MURRAY", "JAMAL", "ADOLFO", "HARRISON", "TYSON", "BURTON", "BRADY", "ELLIOTT", "WILFREDO", - "BART", "JARROD", "VANCE", "DENIS", "DAMIEN", "JOAQUIN", "HARLAN", "DESMOND", "ELLIOT", "DARWIN", - "GREGORIO", "BUDDY", "XAVIER", "KERMIT", "ROSCOE", "ESTEBAN", "ANTON", "SOLOMON", "SCOTTY", "NORBERT", - "ELVIN", "WILLIAMS", "NOLAN", "ROD", "QUINTON", "HAL", "BRAIN", "ROB", "ELWOOD", "KENDRICK", - "DARIUS", "MOISES", "FIDEL", "THADDEUS", "CLIFF", "MARCEL", "JACKSON", "RAPHAEL", "BRYON", "ARMAND", - "ALVARO", "JEFFRY", "DANE", "JOESPH", "THURMAN", "NED", "RUSTY", "MONTY", "FABIAN", "REGGIE", - "MASON", "GRAHAM", "ISAIAH", "VAUGHN", "GUS", "LOYD", "DIEGO", "ADOLPH", "NORRIS", "MILLARD", - "ROCCO", "GONZALO", "DERICK", "RODRIGO", "WILEY", "RIGOBERTO", "ALPHONSO", "TY", "NOE", "VERN", - "REED", "JEFFERSON", "ELVIS", "BERNARDO", "MAURICIO", "HIRAM", "DONOVAN", "BASIL", "RILEY", "NICKOLAS", - "MAYNARD", "SCOT", "VINCE", "QUINCY", "EDDY", "SEBASTIAN", "FEDERICO", "ULYSSES", "HERIBERTO", "DONNELL", - "COLE", "DAVIS", "GAVIN", "EMERY", "WARD", "ROMEO", "JAYSON", "DANTE", "CLEMENT", "COY", - "MAXWELL", "JARVIS", "BRUNO", "ISSAC", "DUDLEY", "BROCK", "SANFORD", "CARMELO", "BARNEY", "NESTOR", - "STEFAN", "DONNY", "ART", "LINWOOD", "BEAU", "WELDON", "GALEN", "ISIDRO", "TRUMAN", "DELMAR", - "JOHNATHON", "SILAS", "FREDERIC", "DICK", "IRWIN", "MERLIN", "CHARLEY", "MARCELINO", "HARRIS", "CARLO", - "TRENTON", "KURTIS", "HUNTER", "AURELIO", "WINFRED", "VITO", "COLLIN", "DENVER", "CARTER", "LEONEL", - "EMORY", "PASQUALE", "MOHAMMAD", "MARIANO", "DANIAL", "LANDON", "DIRK", "BRANDEN", "ADAN", "BUFORD", - "GERMAN", "WILMER", "EMERSON", "ZACHERY", "FLETCHER", "JACQUES", "ERROL", "DALTON", "MONROE", "JOSUE", - "EDWARDO", "BOOKER", "WILFORD", "SONNY", "SHELTON", "CARSON", "THERON", "RAYMUNDO", "DAREN", "HOUSTON", - "ROBBY", "LINCOLN", "GENARO", "BENNETT", "OCTAVIO", "CORNELL", "HUNG", "ARRON", "ANTONY", "HERSCHEL", - "GIOVANNI", "GARTH", "CYRUS", "CYRIL", "RONNY", "LON", "FREEMAN", "DUNCAN", "KENNITH", "CARMINE", - "ERICH", "CHADWICK", "WILBURN", "RUSS", "REID", "MYLES", "ANDERSON", "MORTON", "JONAS", "FOREST", - "MITCHEL", "MERVIN", "ZANE", "RICH", "JAMEL", "LAZARO", "ALPHONSE", "RANDELL", "MAJOR", "JARRETT", - "BROOKS", "ABDUL", "LUCIANO", "SEYMOUR", "EUGENIO", "MOHAMMED", "VALENTIN", "CHANCE", "ARNULFO", "LUCIEN", - "FERDINAND", "THAD", "EZRA", "ALDO", "RUBIN", "ROYAL", "MITCH", "EARLE", "ABE", "WYATT", - "MARQUIS", "LANNY", "KAREEM", "JAMAR", "BORIS", "ISIAH", "EMILE", "ELMO", "ARON", "LEOPOLDO", - "EVERETTE", "JOSEF", "ELOY", "RODRICK", "REINALDO", "LUCIO", "JERROD", "WESTON", "HERSHEL", "BARTON", - "PARKER", "LEMUEL", "BURT", "JULES", "GIL", "ELISEO", "AHMAD", "NIGEL", "EFREN", "ANTWAN", - "ALDEN", "MARGARITO", "COLEMAN", "DINO", "OSVALDO", "LES", "DEANDRE", "NORMAND", "KIETH", "TREY", - "NORBERTO", "NAPOLEON", "JEROLD", "FRITZ", "ROSENDO", "MILFORD", "CHRISTOPER", "ALFONZO", "LYMAN", "JOSIAH", - "BRANT", "WILTON", "RICO", "JAMAAL", "DEWITT", "BRENTON", "OLIN", "FOSTER", "FAUSTINO", "CLAUDIO", - "JUDSON", "GINO", "EDGARDO", "ALEC", "TANNER", "JARRED", "DONN", "TAD", "PRINCE", "PORFIRIO", - "ODIS", "LENARD", "CHAUNCEY", "TOD", "MEL", "MARCELO", "KORY", "AUGUSTUS", "KEVEN", "HILARIO", - "BUD", "SAL", "ORVAL", "MAURO", "ZACHARIAH", "OLEN", "ANIBAL", "MILO", "JED", "DILLON", - "AMADO", "NEWTON", "LENNY", "RICHIE", "HORACIO", "BRICE", "MOHAMED", "DELMER", "DARIO", "REYES", - "MAC", "JONAH", "JERROLD", "ROBT", "HANK", "RUPERT", "ROLLAND", "KENTON", "DAMION", "ANTONE", - "WALDO", "FREDRIC", "BRADLY", "KIP", "BURL", "WALKER", "TYREE", "JEFFEREY", "AHMED", "WILLY", - "STANFORD", "OREN", "NOBLE", "MOSHE", "MIKEL", "ENOCH", "BRENDON", "QUINTIN", "JAMISON", "FLORENCIO", - "DARRICK", "TOBIAS", "HASSAN", "GIUSEPPE", "DEMARCUS", "CLETUS", "TYRELL", "LYNDON", "KEENAN", "WERNER", - "GERALDO", "COLUMBUS", "CHET", "BERTRAM", "MARKUS", "HUEY", "HILTON", "DWAIN", "DONTE", "TYRON", - "OMER", "ISAIAS", "HIPOLITO", "FERMIN", "ADALBERTO", "BO", "BARRETT", "TEODORO", "MCKINLEY", "MAXIMO", - "GARFIELD", "RALEIGH", "LAWERENCE", "ABRAM", "RASHAD", "KING", "EMMITT", "DARON", "SAMUAL", "MIQUEL", - "EUSEBIO", "DOMENIC", "DARRON", "BUSTER", "WILBER", "RENATO", "JC", "HOYT", "HAYWOOD", "EZEKIEL", - "CHAS", "FLORENTINO", "ELROY", "CLEMENTE", "ARDEN", "NEVILLE", "EDISON", "DESHAWN", "NATHANIAL", "JORDON", - "DANILO", "CLAUD", "SHERWOOD", "RAYMON", "RAYFORD", "CRISTOBAL", "AMBROSE", "TITUS", "HYMAN", "FELTON", - "EZEQUIEL", "ERASMO", "STANTON", "LONNY", "LEN", "IKE", "MILAN", "LINO", "JAROD", "HERB", - "ANDREAS", "WALTON", "RHETT", "PALMER", "DOUGLASS", "CORDELL", "OSWALDO", "ELLSWORTH", "VIRGILIO", "TONEY", - "NATHANAEL", "DEL", "BENEDICT", "MOSE", "JOHNSON", "ISREAL", "GARRET", "FAUSTO", "ASA", "ARLEN", - "ZACK", "WARNER", "MODESTO", "FRANCESCO", "MANUAL", "GAYLORD", "GASTON", "FILIBERTO", "DEANGELO", "MICHALE", - "GRANVILLE", "WES", "MALIK", "ZACKARY", "TUAN", "ELDRIDGE", "CRISTOPHER", "CORTEZ", "ANTIONE", "MALCOM", - "LONG", "KOREY", "JOSPEH", "COLTON", "WAYLON", "VON", "HOSEA", "SHAD", "SANTO", "RUDOLF", - "ROLF", "REY", "RENALDO", "MARCELLUS", "LUCIUS", "KRISTOFER", "BOYCE", "BENTON", "HAYDEN", "HARLAND", - "ARNOLDO", "RUEBEN", "LEANDRO", "KRAIG", "JERRELL", "JEROMY", "HOBERT", "CEDRICK", "ARLIE", "WINFORD", - "WALLY", "LUIGI", "KENETH", "JACINTO", "GRAIG", "FRANKLYN", "EDMUNDO", "SID", "PORTER", "LEIF", - "JERAMY", "BUCK", "WILLIAN", "VINCENZO", "SHON", "LYNWOOD", "JERE", "HAI", "ELDEN", "DORSEY", - "DARELL", "BRODERICK", "ALONSO" - ] - total_sum = 0 - temp_sum = 0 - name.sort() - for i in range(len(name)): - for j in name[i]: - temp_sum += ord(j) - ord('A') + 1 - total_sum += (i + 1) * temp_sum - temp_sum = 0 - print(total_sum) - - -if __name__ == '__main__': - main() diff --git a/Project Euler/Problem 24/sol1.py b/Project Euler/Problem 24/sol1.py deleted file mode 100644 index b20493cb03af..000000000000 --- a/Project Euler/Problem 24/sol1.py +++ /dev/null @@ -1,7 +0,0 @@ -from itertools import permutations -def main(): - result=list(map("".join, permutations('0123456789'))) - print(result[999999]) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/Project Euler/Problem 25/sol1.py b/Project Euler/Problem 25/sol1.py deleted file mode 100644 index f8cea3093dcf..000000000000 --- a/Project Euler/Problem 25/sol1.py +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import print_function - -try: - xrange #Python 2 -except NameError: - xrange = range #Python 3 - -def fibonacci(n): - if n == 1 or type(n) is not int: - return 0 - elif n == 2: - return 1 - else: - sequence = [0, 1] - for i in xrange(2, n+1): - sequence.append(sequence[i-1] + sequence[i-2]) - - return sequence[n] - -def fibonacci_digits_index(n): - digits = 0 - index = 2 - - while digits < n: - index += 1 - digits = len(str(fibonacci(index))) - - return index - -if __name__ == '__main__': - print(fibonacci_digits_index(1000)) \ No newline at end of file diff --git a/Project Euler/Problem 28/sol1.py b/Project Euler/Problem 28/sol1.py deleted file mode 100644 index 4942115ce537..000000000000 --- a/Project Euler/Problem 28/sol1.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import print_function -from math import ceil - -try: - xrange #Python 2 -except NameError: - xrange = range #Python 3 - -def diagonal_sum(n): - total = 1 - - for i in xrange(1, int(ceil(n/2.0))): - odd = 2*i+1 - even = 2*i - total = total + 4*odd**2 - 6*even - - return total - -if __name__ == '__main__': - import sys - - if len(sys.argv) == 1: - print(diagonal_sum(1001)) - else: - try: - n = int(sys.argv[1]) - diagonal_sum(n) - except ValueError: - print('Invalid entry - please enter a number') \ No newline at end of file diff --git a/Project Euler/Problem 29/solution.py b/Project Euler/Problem 29/solution.py deleted file mode 100644 index 64d35c84d9ca..000000000000 --- a/Project Euler/Problem 29/solution.py +++ /dev/null @@ -1,33 +0,0 @@ -def main(): - """ - Consider all integer combinations of ab for 2 <= a <= 5 and 2 <= b <= 5: - - 22=4, 23=8, 24=16, 25=32 - 32=9, 33=27, 34=81, 35=243 - 42=16, 43=64, 44=256, 45=1024 - 52=25, 53=125, 54=625, 55=3125 - If they are then placed in numerical order, with any repeats removed, - we get the following sequence of 15 distinct terms: - - 4, 8, 9, 16, 25, 27, 32, 64, 81, 125, 243, 256, 625, 1024, 3125 - - How many distinct terms are in the sequence generated by ab - for 2 <= a <= 100 and 2 <= b <= 100? - """ - - collectPowers = set() - - currentPow = 0 - - N = 101 # maximum limit - - for a in range(2, N): - for b in range(2, N): - currentPow = a**b # calculates the current power - collectPowers.add(currentPow) # adds the result to the set - - print("Number of terms ", len(collectPowers)) - - -if __name__ == '__main__': - main() diff --git a/Project Euler/Problem 36/sol1.py b/Project Euler/Problem 36/sol1.py deleted file mode 100644 index d78e7e59f210..000000000000 --- a/Project Euler/Problem 36/sol1.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import print_function -''' -Double-base palindromes -Problem 36 -The decimal number, 585 = 10010010012 (binary), is palindromic in both bases. - -Find the sum of all numbers, less than one million, which are palindromic in base 10 and base 2. - -(Please note that the palindromic number, in either base, may not include leading zeros.) -''' -try: - xrange #Python 2 -except NameError: - xrange = range #Python 3 - -def is_palindrome(n): - n = str(n) - - if n == n[::-1]: - return True - else: - return False - -total = 0 - -for i in xrange(1, 1000000): - if is_palindrome(i) and is_palindrome(bin(i).split('b')[1]): - total += i - -print(total) \ No newline at end of file diff --git a/Project Euler/Problem 40/sol1.py b/Project Euler/Problem 40/sol1.py deleted file mode 100644 index ab4017512a1a..000000000000 --- a/Project Euler/Problem 40/sol1.py +++ /dev/null @@ -1,26 +0,0 @@ -#-.- coding: latin-1 -.- -from __future__ import print_function -''' -Champernowne's constant -Problem 40 -An irrational decimal fraction is created by concatenating the positive integers: - -0.123456789101112131415161718192021... - -It can be seen that the 12th digit of the fractional part is 1. - -If dn represents the nth digit of the fractional part, find the value of the following expression. - -d1 × d10 × d100 × d1000 × d10000 × d100000 × d1000000 -''' - -constant = [] -i = 1 - -while len(constant) < 1e6: - constant.append(str(i)) - i += 1 - -constant = ''.join(constant) - -print(int(constant[0])*int(constant[9])*int(constant[99])*int(constant[999])*int(constant[9999])*int(constant[99999])*int(constant[999999])) \ No newline at end of file diff --git a/Project Euler/Problem 48/sol1.py b/Project Euler/Problem 48/sol1.py deleted file mode 100644 index 5c4bdb0f6384..000000000000 --- a/Project Euler/Problem 48/sol1.py +++ /dev/null @@ -1,21 +0,0 @@ -from __future__ import print_function -''' -Self Powers -Problem 48 - -The series, 11 + 22 + 33 + ... + 1010 = 10405071317. - -Find the last ten digits of the series, 11 + 22 + 33 + ... + 10001000. -''' - -try: - xrange -except NameError: - xrange = range - -total = 0 -for i in xrange(1, 1001): - total += i**i - - -print(str(total)[-10:]) \ No newline at end of file diff --git a/Project Euler/Problem 52/sol1.py b/Project Euler/Problem 52/sol1.py deleted file mode 100644 index 376b4cfa1d63..000000000000 --- a/Project Euler/Problem 52/sol1.py +++ /dev/null @@ -1,23 +0,0 @@ -from __future__ import print_function -''' -Permuted multiples -Problem 52 - -It can be seen that the number, 125874, and its double, 251748, contain exactly the same digits, but in a different order. - -Find the smallest positive integer, x, such that 2x, 3x, 4x, 5x, and 6x, contain the same digits. -''' -i = 1 - -while True: - if sorted(list(str(i))) == \ - sorted(list(str(2*i))) == \ - sorted(list(str(3*i))) == \ - sorted(list(str(4*i))) == \ - sorted(list(str(5*i))) == \ - sorted(list(str(6*i))): - break - - i += 1 - -print(i) \ No newline at end of file diff --git a/Project Euler/Problem 53/sol1.py b/Project Euler/Problem 53/sol1.py deleted file mode 100644 index ed6d5329eb4e..000000000000 --- a/Project Euler/Problem 53/sol1.py +++ /dev/null @@ -1,36 +0,0 @@ -#-.- coding: latin-1 -.- -from __future__ import print_function -from math import factorial -''' -Combinatoric selections -Problem 53 - -There are exactly ten ways of selecting three from five, 12345: - -123, 124, 125, 134, 135, 145, 234, 235, 245, and 345 - -In combinatorics, we use the notation, 5C3 = 10. - -In general, - -nCr = n!/(r!(n−r)!),where r ≤ n, n! = n×(n−1)×...×3×2×1, and 0! = 1. -It is not until n = 23, that a value exceeds one-million: 23C10 = 1144066. - -How many, not necessarily distinct, values of nCr, for 1 ≤ n ≤ 100, are greater than one-million? -''' -try: - xrange #Python 2 -except NameError: - xrange = range #Python 3 - -def combinations(n, r): - return factorial(n)/(factorial(r)*factorial(n-r)) - -total = 0 - -for i in xrange(1, 101): - for j in xrange(1, i+1): - if combinations(i, j) > 1e6: - total += 1 - -print(total) \ No newline at end of file diff --git a/Project Euler/Problem 76/sol1.py b/Project Euler/Problem 76/sol1.py deleted file mode 100644 index 2832f6d7afb6..000000000000 --- a/Project Euler/Problem 76/sol1.py +++ /dev/null @@ -1,35 +0,0 @@ -from __future__ import print_function -''' -Counting Summations -Problem 76 - -It is possible to write five as a sum in exactly six different ways: - -4 + 1 -3 + 2 -3 + 1 + 1 -2 + 2 + 1 -2 + 1 + 1 + 1 -1 + 1 + 1 + 1 + 1 - -How many different ways can one hundred be written as a sum of at least two positive integers? -''' -try: - xrange #Python 2 -except NameError: - xrange = range #Python 3 - -def partition(m): - memo = [[0 for _ in xrange(m)] for _ in xrange(m+1)] - for i in xrange(m+1): - memo[i][0] = 1 - - for n in xrange(m+1): - for k in xrange(1, m): - memo[n][k] += memo[n][k-1] - if n > k: - memo[n][k] += memo[n-k-1][k] - - return (memo[m][m-1] - 1) - -print(partition(100)) \ No newline at end of file diff --git a/Project Euler/Problem 9/sol1.py b/Project Euler/Problem 9/sol1.py deleted file mode 100644 index e54c543b4721..000000000000 --- a/Project Euler/Problem 9/sol1.py +++ /dev/null @@ -1,15 +0,0 @@ -from __future__ import print_function -# Program to find the product of a,b,c which are Pythagorean Triplet that satisfice the following: -# 1. a < b < c -# 2. a**2 + b**2 = c**2 -# 3. a + b + c = 1000 - -print("Please Wait...") -for a in range(300): - for b in range(400): - for c in range(500): - if(a < b < c): - if((a**2) + (b**2) == (c**2)): - if((a+b+c) == 1000): - print(("Product of",a,"*",b,"*",c,"=",(a*b*c))) - break diff --git a/Project Euler/Problem 9/sol2.py b/Project Euler/Problem 9/sol2.py deleted file mode 100644 index 13674d25875e..000000000000 --- a/Project Euler/Problem 9/sol2.py +++ /dev/null @@ -1,19 +0,0 @@ -"""A Pythagorean triplet is a set of three natural numbers, for which, -a^2+b^2=c^2 -Given N, Check if there exists any Pythagorean triplet for which a+b+c=N -Find maximum possible value of product of a,b,c among all such Pythagorean triplets, If there is no such Pythagorean triplet print -1.""" -#!/bin/python3 -import sys - -product=-1 -d=0 -N = int(input()) -for a in range(1,N//3): - """Solving the two equations a**2+b**2=c**2 and a+b+c=N eliminating c """ - b=(N*N-2*a*N)//(2*N-2*a) - c=N-a-b - if c*c==(a*a+b*b): - d=(a*b*c) - if d>=product: - product=d -print(product) diff --git a/Project Euler/README.md b/Project Euler/README.md deleted file mode 100644 index 9f77f719f0f1..000000000000 --- a/Project Euler/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# ProjectEuler - -Problems are taken from https://projecteuler.net/. - -Project Euler is a series of challenging mathematical/computer programming problems that will require more than just mathematical -insights to solve. Project Euler is ideal for mathematicians who are learning to code. - -Here the efficiency of your code is also checked. -I've tried to provide all the best possible solutions. - -PROBLEMS: - -1. If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3,5,6 and 9. The sum of these multiples is 23. - Find the sum of all the multiples of 3 or 5 below N. - -2. Each new term in the Fibonacci sequence is generated by adding the previous two terms. By starting with 1 and 2, - the first 10 terms will be: - 1,2,3,5,8,13,21,34,55,89,.. - By considering the terms in the Fibonacci sequence whose values do not exceed n, find the sum of the even-valued terms. - e.g. for n=10, we have {2,8}, sum is 10. - -3. The prime factors of 13195 are 5,7,13 and 29. What is the largest prime factor of a given number N? - e.g. for 10, largest prime factor = 5. For 17, largest prime factor = 17. - -4. A palindromic number reads the same both ways. The largest palindrome made from the product of two 2-digit numbers is 9009 = 91 × 99. - Find the largest palindrome made from the product of two 3-digit numbers which is less than N. - -5. 2520 is the smallest number that can be divided by each of the numbers from 1 to 10 without any remainder. - What is the smallest positive number that is evenly divisible(divisible with no remainder) by all of the numbers from 1 to N? - -6. The sum of the squares of the first ten natural numbers is, - 1^2 + 2^2 + ... + 10^2 = 385 - The square of the sum of the first ten natural numbers is, - (1 + 2 + ... + 10)^2 = 552 = 3025 - Hence the difference between the sum of the squares of the first ten natural numbers and the square of the sum is 3025 − 385 = 2640. - Find the difference between the sum of the squares of the first N natural numbers and the square of the sum. - -7. By listing the first six prime numbers: 2, 3, 5, 7, 11, and 13, we can see that the 6th prime is 13. - What is the Nth prime number? - -9. A Pythagorean triplet is a set of three natural numbers, a < b < c, for which, - a^2 + b^2 = c^2 - There exists exactly one Pythagorean triplet for which a + b + c = 1000. - Find the product abc. - -14. The following iterative sequence is defined for the set of positive integers: - n → n/2 (n is even) - n → 3n + 1 (n is odd) - Using the rule above and starting with 13, we generate the following sequence: - 13 → 40 → 20 → 10 → 5 → 16 → 8 → 4 → 2 → 1 - Which starting number, under one million, produces the longest chain? - -16. 2^15 = 32768 and the sum of its digits is 3 + 2 + 7 + 6 + 8 = 26. - What is the sum of the digits of the number 2^1000? -20. n! means n × (n − 1) × ... × 3 × 2 × 1 - For example, 10! = 10 × 9 × ... × 3 × 2 × 1 = 3628800, - and the sum of the digits in the number 10! is 3 + 6 + 2 + 8 + 8 + 0 + 0 = 27. - Find the sum of the digits in the number 100! diff --git a/README.md b/README.md index 9b394e001d66..e9a19fa15499 100644 --- a/README.md +++ b/README.md @@ -1,174 +1,49 @@ -# The Algorithms - 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 log n) - -###### 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-Complexity 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) - -### Vigenère -The **Vigenère cipher** is a method of encrypting alphabetic text by using a series of **interwoven Caesar ciphers** based on the letters of a keyword. It is **a form of polyalphabetic substitution**.
-The Vigenère cipher has been reinvented many times. The method was originally described by Giovan Battista Bellaso in his 1553 book La cifra del. Sig. Giovan Battista Bellaso; however, the scheme was later misattributed to Blaise de Vigenère in the 19th century, and is now widely known as the "Vigenère cipher".
-Though the cipher is easy to understand and implement, for three centuries it resisted all attempts to break it; this earned it the description **le chiffre indéchiffrable**(French for 'the indecipherable cipher'). -Many people have tried to implement encryption schemes that are essentially Vigenère ciphers. Friedrich Kasiski was the first to publish a general method of deciphering a Vigenère cipher in 1863. -###### Source: [Wikipedia](https://en.wikipedia.org/wiki/Vigen%C3%A8re_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/data_structures/UnionFind/__init__.py b/backtracking/__init__.py similarity index 100% rename from data_structures/UnionFind/__init__.py rename to backtracking/__init__.py 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/Quine_McCluskey/QuineMcCluskey.py b/boolean_algebra/Quine_McCluskey/QuineMcCluskey.py deleted file mode 100644 index eb3a11f7ee31..000000000000 --- a/boolean_algebra/Quine_McCluskey/QuineMcCluskey.py +++ /dev/null @@ -1,116 +0,0 @@ -def compare_string(string1, string2): - l1 = list(string1); l2 = list(string2) - count = 0 - for i in range(len(l1)): - if l1[i] != l2[i]: - count += 1 - l1[i] = '_' - if count > 1: - return -1 - else: - return("".join(l1)) - -def check(binary): - pi = [] - while 1: - 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 != -1: - check1[i] = '*' - check1[j] = '*' - temp.append(k) - 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, minterms): - temp = [] - s = '' - for m in minterms: - for i in range(no_of_variable): - s = str(m%2) + s - m //= 2 - temp.append(s) - s = '' - return temp - -def is_for_table(string1, string2, count): - l1 = list(string1);l2=list(string2) - count_n = 0 - for i in range(len(l1)): - if l1[i] != l2[i]: - count_n += 1 - if count_n == count: - return True - else: - return False - -def selection(chart, prime_implicants): - temp = [] - select = [0]*len(chart) - for i in range(len(chart[0])): - count = 0 - rem = -1 - for j in range(len(chart)): - if chart[j][i] == 1: - count += 1 - rem = j - if count == 1: - select[rem] = 1 - for i in range(len(select)): - if select[i] == 1: - for j in range(len(chart[0])): - if chart[i][j] == 1: - for k in range(len(chart)): - chart[k][j] = 0 - temp.append(prime_implicants[i]) - while 1: - max_n = 0; rem = -1; count_n = 0 - for i in range(len(chart)): - count_n = chart[i].count(1) - if count_n > max_n: - max_n = count_n - rem = i - - if max_n == 0: - return temp - - temp.append(prime_implicants[rem]) - - for i in range(len(chart[0])): - if chart[rem][i] == 1: - for j in range(len(chart)): - chart[j][i] = 0 - -def prime_implicant_chart(prime_implicants, binary): - 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(): - no_of_variable = int(input("Enter the no. of variables\n")) - minterms = [int(x) for x in input("Enter the decimal representation of Minterms 'Spaces Seprated'\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__': - main() \ No newline at end of file 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/Onepad_Cipher.py b/ciphers/Onepad_Cipher.py deleted file mode 100644 index 5536285efc17..000000000000 --- a/ciphers/Onepad_Cipher.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import print_function - - -class Onepad: - def encrypt(self, text): - '''Function to encrypt text using psedo-random numbers''' - 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 - - def decrypt(self, cipher, key): - '''Function to decrypt text using psedo-random numbers.''' - plain = [] - for i in range(len(key)): - p = (cipher[i]-(key[i])**2)/key[i] - plain.append(chr(p)) - plain = ''.join([i for i in plain]) - return plain - - -if __name__ == '__main__': - c, k = Onepad().encrypt('Hello') - print(c, k) - print(Onepad().decrypt(c, k)) diff --git a/ciphers/Prehistoric Men.txt b/ciphers/Prehistoric Men.txt deleted file mode 100644 index 86c4de821bfc..000000000000 --- a/ciphers/Prehistoric Men.txt +++ /dev/null @@ -1,7193 +0,0 @@ -The Project Gutenberg eBook, Prehistoric Men, by Robert J. (Robert John) -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 -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 -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. - - -Title: Prehistoric Men -Author: Robert J. (Robert John) Braidwood -Release Date: July 28, 2016 [eBook #52664] -Language: English -Character set encoding: UTF-8 - - -***START OF THE PROJECT GUTENBERG EBOOK PREHISTORIC MEN*** - - -E-text prepared by Stephen Hutcheson, Dave Morgan, Charlie Howard, and the -Online Distributed Proofreading Team (http://www.pgdp.net) - - - -Note: Project Gutenberg also has an HTML version of this - file which includes the original illustrations. - See 52664-h.htm or 52664-h.zip: - (http://www.gutenberg.org/files/52664/52664-h/52664-h.htm) - or - (http://www.gutenberg.org/files/52664/52664-h.zip) - - -Transcriber's note: - - Some characters might not display in this UTF-8 text - version. If so, the reader should consult the HTML - 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”. - Devices that cannot display that symbol may substitute - a question mark, a square, or other symbol. - - -PREHISTORIC MEN - -by - -ROBERT J. BRAIDWOOD - -Research Associate, Old World Prehistory - -Professor -Oriental Institute and Department of Anthropology -University of Chicago - -Drawings by Susan T. Richert - - -[Illustration] - -Chicago Natural History Museum -Popular Series -Anthropology, Number 37 - -Third Edition Issued in Co-operation with -The Oriental Institute, The University of Chicago - -Edited by Lillian A. Ross - -Printed in the United States of America -by Chicago Natural History Museum Press - -Copyright 1948, 1951, and 1957 by Chicago Natural History Museum - -First edition 1948 -Second edition 1951 -Third edition 1957 -Fourth edition 1959 - - -Preface - -[Illustration] - - -Like the writing of most professional archeologists, mine has been -confined to so-called learned papers. Good, bad, or indifferent, these -papers were in a jargon that only my colleagues and a few advanced -students could understand. Hence, when I was asked to do this little -book, I soon found it extremely difficult to say what I meant in simple -fashion. The style is new to me, but I hope the reader will not find it -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 -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 -of Pennsylvania), and also Mrs. Linda Braidwood, Associate of the -Oriental Institute, and Professors Fay-Cooper Cole and Sol Tax, of -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 -the story. I am deeply indebted to all these friends. - -While I was preparing the second edition, I had the great fortune -to be able to rework the third chapter with Professor Sherwood L. -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 -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 -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 -and better dating systems appear. - -I wish to thank Dr. Clifford C. Gregg, Director of Chicago Natural -History Museum, for the opportunity to publish this book. My old -friend, Dr. Paul S. Martin, Chief Curator in the Department of -Anthropology, asked me to undertake the job and inspired me to complete -it. I am also indebted to Miss Lillian A. Ross, Associate Editor of -Scientific Publications, and to Mr. George I. Quimby, Curator of -Exhibits in Anthropology, for all the time they have given me in -getting the manuscript into proper shape. - - ROBERT J. BRAIDWOOD - _June 15, 1950_ - - - - -Preface to the Third Edition - - -In preparing the enlarged third edition, many of the above mentioned -friends have again helped me. I have picked the brains of Professor F. -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 -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. - - ROBERT J. BRAIDWOOD - _March 1, 1959_ - - - - -Contents - - - PAGE - How We Learn about Prehistoric Men 7 - - The Changing World in Which Prehistoric Men Lived 17 - - Prehistoric Men Themselves 22 - - Cultural Beginnings 38 - - More Evidence of Culture 56 - - Early Moderns 70 - - End and Prelude 92 - - The First Revolution 121 - - The Conquest of Civilization 144 - - End of Prehistory 162 - - Summary 176 - - List of Books 180 - - Index 184 - - - - -HOW WE LEARN about Prehistoric Men - -[Illustration] - - -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 -million years old, but he did not begin to write history (or to write -anything) until about 5,000 years ago. - -The men who lived in prehistoric times left us no history books, but -they did unintentionally leave a record of their presence and their way -of life. This record is studied and interpreted by different kinds of -scientists. - - -SCIENTISTS WHO FIND OUT ABOUT PREHISTORIC MEN - -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 -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 -prehistoric men work with fossils, so they are sometimes called _human -paleontologists_. - - -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 -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 -history of human activity, provided always that the actions have -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 - ancient man, and - - 2. Only a few objects may be found, for most of these were probably - 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 - graves that have escaped destruction. - -What I mean is this: Let us say you are walking through a dump yard, -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 -sixty years old. - -When you think about the old spark plug in this way you have -just been making the beginnings of what we call an archeological -_interpretation_; you have been making the spark plug tell a story. -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. - - -SOME OTHER SCIENTISTS - -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 -_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 -the kinds of animals and plants that were living in prehistoric times -and so could be used for food by ancient man; what the weather was -like; and whether there were glaciers. Also, when I tell you that -prehistoric men did not appear until long after the great dinosaurs had -disappeared, I go on the say-so of the paleontologists. They know that -fossils of men and of dinosaurs are not found in the same geological -period. The dinosaur fossils come in early periods, the fossils of men -much later. - -Since World War II even the atomic scientists have been helping the -archeologists. By testing the amount of radioactivity left in charcoal, -wood, or other vegetable matter obtained from archeological sites, they -have been able to date the sites. Shell has been used also, and even -the hair of Egyptian mummies. The dates of geological and climatic -events have also been discovered. Some of this work has been done from -drillings taken from the bottom of the sea. - -This dating by radioactivity has considerably shortened the dates which -the archeologists used to give. If you find that some of the dates -I give here are more recent than the dates you see in other books -on prehistory, it is because I am using one of the new lower dating -systems. - -[Illustration: RADIOCARBON CHART - -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 - 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 - 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 - 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.” - - -HOW THE SCIENTISTS FIND OUT - -So far, this chapter has been mainly about the people who find out -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 -the strange stones came to earth as bolts of lightning. We know now -that these strange stones were prehistoric stone tools. - -Many important finds still come to us by accident. In 1935, a British -dentist, A. T. Marston, found the first of two fragments of a very -important fossil human skull, in a gravel pit at Swanscombe, on the -River Thames, England. He had to wait nine months, until the face of -the gravel pit had been dug eight yards farther back, before the second -fragment appeared. They fitted! Then, twenty years later, still another -piece appeared. In 1928 workmen who were blasting out rock for the -breakwater in the port of Haifa began to notice flint tools. Thus the -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, -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 -an old idea of his. He believed that the heroes of ancient Troy and -Mycenae were once real Trojans and Greeks. He proved it by going to -Turkey and Greece and digging up the remains of both cities. - -Schliemann had the great good fortune to find rich and spectacular -treasures, and he also had the common sense to keep notes and make -descriptions of what he found. He proved beyond doubt that many ancient -city mounds can be _stratified_. This means that there may be the -remains of many towns in a mound, one above another, like layers in a -cake. - -You might like to have an idea of how mounds come to be in layers. -The original settlers may have chosen the spot because it had a good -spring and there were good fertile lands nearby, or perhaps because -it was close to some road or river or harbor. These settlers probably -built their town of stone and mud-brick. Finally, something would have -happened to the town--a flood, or a burning, or a raid by enemies--and -the walls of the houses would have fallen in or would have melted down -as mud in the rain. Nothing would have remained but the mud and debris -of a low mound of _one_ layer. - -The second settlers would have wanted the spot for the same reasons -the first settlers did--good water, land, and roads. Also, the second -settlers would have found a nice low mound to build their houses on, -a protection from floods. But again, something would finally have -happened to the second town, and the walls of _its_ houses would have -come tumbling down. This makes the _second_ layer. And so on.... - -In Syria I once had the good fortune to dig on a large mound that had -no less than fifteen layers. Also, most of the layers were thick, and -there were signs of rebuilding and repairs within each layer. The mound -was more than a hundred feet high. In each layer, the building material -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 -that their lowest layer of rock was oldest or earliest, and that the -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 -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 -soon set out to dig on ancient sites, from Egypt to Central America. - - -ARCHEOLOGICAL INFORMATION - -As time went on, the study of archeological materials--found either -by accident or by digging on purpose--began to show certain things. -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 -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. - -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 -our 1900 mail-order catalogue is from the one of today. - -The archeologists who came after Schliemann began to notice other -things and to compare them with occurrences in modern times. The -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 -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 -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 -has to date at about A.D. 1936-56. - -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. - - -EVERYTHING BEGINS TO FIT TOGETHER - -Now we need to pull these ideas all together and see the complicated -structure the archeologists can build with their materials. - -Even the earliest archeologists soon found that there was a very long -range of prehistoric time which would yield only very simple things. -For this very long early part of prehistory, there was little to be -found but the flint tools which wandering, hunting and gathering -people made, and the bones of the wild animals they ate. Toward the -end of prehistoric time there was a general settling down with the -coming of agriculture, and all sorts of new things began to be made. -Archeologists soon got a general notion of what ought to appear with -what. Thus, it would upset a French prehistorian digging at the bottom -of a very early cave if he found a fine bronze sword, just as much as -it would upset him if he found a beer bottle. The people of his very -early cave layer simply could not have made bronze swords, which came -later, just as do beer bottles. Some accidental disturbance of the -layers of his cave must have happened. - -With any luck, archeologists do their digging in a layered, stratified -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 -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 -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 -facts begin to stand out. We begin to be more sure of which people -lived at the same time, which earlier and which later. We begin to -know who traded with whom, and which peoples seemed to live off by -themselves. We begin to find enough skeletons in burials so that the -physical anthropologists can tell us what the people looked like. We -get animal bones, and a paleontologist may tell us they are all bones -of wild animals; or he may tell us that some or most of the bones are -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 -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? -How long did they last? We watch the different tools and the traces -of the way things were done--how the burials were arranged, what -the living-places were like, and so on. We wonder about the people -themselves, for the traces of habits and traditions are useful to us -only as clues to the men who once had them. So we ask the physical -anthropologists about the skeletons that we found in the burials. The -physical anthropologists tell us about the anatomy and the similarities -and differences which the skeletons show when compared with other -skeletons. The physical anthropologists are even working on a -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. - -We become particularly interested in any signs of change--when new -materials and tool types and ways of doing things replace old ones. We -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 -happened before history began. - - - - -THE CHANGING WORLD in which Prehistoric Men Lived - -[Illustration] - - -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 -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 -prehistoric time. - -Or maybe we can grasp the length of time better in terms of -generations. As you know, primitive peoples tend to marry and have -children rather early in life. So suppose we say that twenty years -will make an average generation. At this rate there would be 25,000 -generations in a half-million years. But our United States is much less -than ten generations old, twenty-five generations take us back before -the time of Columbus, Julius Caesar was alive just 100 generations ago, -David was king of Israel less than 150 generations ago, 250 generations -take us back to the beginning of written history. And there were 24,750 -generations of men before written history began! - -I should probably tell you that there is a new method of prehistoric -dating which would cut the earliest dates in my reckoning almost -in half. Dr. Cesare Emiliani, combining radioactive (C14) and -chemical (oxygen isotope) methods in the study of deep-sea borings, -has developed a system which would lower the total range of human -prehistory to about 300,000 years. The system is still too new to have -had general examination and testing. Hence, I have not used it in this -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 -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. - -In earlier times the earth looked very different. Geologists call the -last great geological period the _Pleistocene_. It began somewhere -between a half million and a million years ago, and was a time of great -changes. Sometimes we call it the Ice Age, for in the Pleistocene -there were at least three or four times when large areas of earth -were covered with glaciers. The reason for my uncertainty is that -while there seem to have been four major mountain or alpine phases of -glaciation, there may only have been three general continental phases -in the Old World.[2] - - [2] This is a complicated affair and I do not want to bother you - with its details. Both the alpine and the continental ice sheets - seem to have had minor fluctuations during their _main_ phases, - and the advances of the later phases destroyed many of the - traces of the earlier phases. The general textbooks have tended - to follow the names and numbers established for the Alps early - in this century by two German geologists. I will not bother you - with the names, but there were _four_ major phases. It is the - second of these alpine phases which seems to fit the traces of - the earliest of the great continental glaciations. In this book, - I will use the four-part system, since it is the most familiar, - but will add the word _alpine_ so you may remember to make the - transition to the continental system if you wish to do so. - -Glaciers are great sheets of ice, sometimes over a thousand feet -thick, which are now known only in Greenland and Antarctica and in -high mountains. During several of the glacial periods in the Ice Age, -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. - -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 -ice. Because so much water was frozen and contained in the glaciers, -the water level of lakes and oceans was lowered. Flooded areas were -drained and appeared as dry land. There were times in the Ice Age when -there was no English Channel, so that England was not an island, and a -land bridge at the Dardanelles probably divided the Mediterranean from -the Black Sea. - -A very important thing for people living during the time of a -glaciation was the region adjacent to the glacier. They could not, of -course, live on the ice itself. The questions would be how close could -they live to it, and how would they have had to change their way of -life to do so. - - -GLACIERS CHANGE THE WEATHER - -Great sheets of ice change the weather. When the front of a glacier -stood at Milwaukee, the weather must have been bitterly cold in -Chicago. The climate of the whole world would have been different, and -you can see how animals and men would have been forced to move from one -place to another in search of food and warmth. - -On the other hand, it looks as if only a minor proportion of the whole -Ice Age was really taken up by times of glaciation. In between came -the _interglacial_ periods. During these times the climate around -Chicago was as warm as it is now, and sometimes even warmer. It may -interest you to know that the last great glacier melted away less than -10,000 years ago. Professor Ernst Antevs thinks we may be living in an -interglacial period and that the Ice Age may not be over yet. So if you -want to make a killing in real estate for your several hundred times -great-grandchildren, you might buy some land in the Arizona desert or -the Sahara. - -We do not yet know just why the glaciers appeared and disappeared, as -they did. It surely had something to do with an increase in rainfall -and a fall in temperature. It probably also had to do with a general -tendency for the land to rise at the beginning of the Pleistocene. We -know there was some mountain-building at that time. Hence, rain-bearing -winds nourished the rising and cooler uplands with snow. An increase -in all three of these factors--if they came together--would only have -needed to be slight. But exactly why this happened we do not know. - -The reason I tell you about the glaciers is simply to remind you of the -changing world in which prehistoric men lived. Their surroundings--the -animals and plants they used for food, and the weather they had to -protect themselves from--were always changing. On the other hand, this -change happened over so long a period of time and was so slow that -individual people could not have noticed it. Glaciers, about which they -probably knew nothing, moved in hundreds of miles to the north of them. -The people must simply have wandered ever more southward in search -of the plants and animals on which they lived. Or some men may have -stayed where they were and learned to hunt different animals and eat -different foods. Prehistoric men had to keep adapting themselves to new -environments and those who were most adaptive were most successful. - - -OTHER CHANGES - -Changes took place in the men themselves as well as in the ways they -lived. As time went on, they made better tools and weapons. Then, too, -we begin to find signs of how they started thinking of other things -than food and the tools to get it with. We find that they painted on -the walls of caves, and decorated their tools; we find that they buried -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.” -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 -impossible for men to go beyond that point if they only hunted and -fished and gathered wild foods. Once the basic change was made--once -the food-producing revolution became effective--technology leaped ahead -and civilization and written history soon began. - - - - -Prehistoric Men THEMSELVES - -[Illustration] - - -DO WE KNOW WHERE MAN ORIGINATED? - -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 -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 -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 -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.” - -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, -we have not yet found the tools which we suppose these earliest men in -the Far East must have made. - -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” -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 -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. - -Very soon after the earliest Javanese come the bones of slightly more -developed people in Java, and the jawbone of a man who once lived in -what is now Germany. The same general glacial beds which yielded the -later Javanese bones and the German jawbone also include tools. These -finds come from the time of the second alpine glaciation. - -So this is the situation. By the time of the end of the second alpine -or first continental glaciation (say 400,000 years ago) we have traces -of men from the extremes of the more southerly portions of the Old -World--South Africa, eastern Asia, and western Europe. There are also -some traces of men in the middle ground. In fact, Professor Franz -Weidenreich believed that creatures who were the immediate ancestors -of men had already spread over Europe, Africa, and Asia by the time -the Ice Age began. We certainly have no reason to disbelieve this, but -fortunate accidents of discovery have not yet given us the evidence to -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 -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 -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, -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 -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 -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. - - -BRAINS, HANDS, AND TOOLS - -Whether the australopithecines were our ancestors or not, the proper -ancestors of men must have been able to stand erect and to walk on -their two feet. Three further important things probably were involved, -next, before they could become men proper. These are: - - 1. The increasing size and development of the brain. - - 2. The increasing usefulness (specialization) of the thumb and hand. - - 3. The use of tools. - -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 -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 -_co-ordination_ of these things that counts. - -Many other things must have been happening to the bodies of the -creatures who were the ancestors of men. Our ancestors had to develop -organs of speech. More than that, they had to get the idea of letting -_certain sounds_ made with these speech organs have _certain meanings_. - -All this must have gone very slowly. Probably everything was developing -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 -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 -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 -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 -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 -with the australopithecine fossils. But there are three varieties -of the australopithecines and they last on until a time equal to -that of the second alpine glaciation. They are the best suggestion -we have yet as to what the ancestors of men _may_ have looked like. -They were certainly closer to men than to apes. Although their brain -size was no larger than the brains of modern apes their body size and -stature were quite small; hence, relative to their small size, their -brains were large. We have not been able to prove without doubt that -the australopithecines were _tool-making_ creatures, even though the -recent news has it that tools have been found with australopithecine -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.” - - -THE EARLIEST MEN WE KNOW - -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 -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 -australopithecines and _Pithecanthropus_? It is still too early to say. -We shall need more finds before we can be definite one way or the other. - -Java man, _Pithecanthropus_, comes from geological beds equal in age -to the latter part of the second alpine glaciation; the _Meganthropus_ -finds refer to beds of the beginning of this glaciation. The first -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 -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. - -No tools were found in the geological deposits where bones of Java man -appeared. There are some tools in the same general area, but they come -a bit later in time. One reason we accept the Java man as man--aside -from his general anatomical appearance--is that these tools probably -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 -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 -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,” -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 -these bones are now. The Red Chinese accuse us of having stolen them. -They were last seen on a dock-side at a Chinese port. But should you -catch a Marine with a sack of old bones, perhaps we could achieve peace -in Asia by returning them! Fortunately, there is a complete set of -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 -in their drug stores and grind them into powder for medicine. The -“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 -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. - -Peking man dates from early in the interglacial period following the -second alpine glaciation. He probably lived close to 350,000 years -ago. There are several finds to account for in Europe by about this -time, and one from northwest Africa. The very large jawbone found -near Heidelberg in Germany is doubtless even earlier than Peking man. -The beds where it was found are of second alpine glacial times, and -recently some tools have been said to have come from the same beds. -There is not much I need tell you about the Heidelberg jaw save that it -seems certainly to have belonged to an early man, and that it is very -big. - -Another find in Germany was made at Steinheim. It consists of the -fragmentary skull of a man. It is very important because of its -relative completeness, but it has not yet been fully studied. The bone -is thick, but the back of the head is neither very low nor primitive, -and the face is also not primitive. The forehead does, however, have -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 -necessarily so. This is important because it bears on the question of -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 -found with them. Since no jaws have yet been found at Steinheim or -Swanscombe, but the time is the same, one wonders if these people had -jaws like those of Ternafine. - - -WHAT HAPPENED TO JAVA AND PEKING MEN - -Professor Weidenreich thought that there were at least a dozen ways in -which the Peking man resembled the modern Mongoloids. This would seem -to indicate that Peking man was really just a very early Chinese. - -Several later fossil men have been found in the Java-Australian area. -The best known of these is the so-called Solo man. There are some finds -from Australia itself which we now know to be quite late. But it looks -as if we may assume a line of evolution from Java man down to the -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 - -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 -him were part of an elaborate fake! - - -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 -human fossil to be recognized as such. - -[Illustration: PRINCIPAL KNOWN TYPES OF FOSSIL MEN - - CRO-MAGNON - NEANDERTHAL - MODERN SKULL - COMBE-CAPELLE - SINANTHROPUS - 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 -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 -Howell sees it this way. In Europe, the earliest trace of men we -now know is the Heidelberg jaw. Evolution continued in Europe, from -Heidelberg through the Swanscombe and Steinheim types to a group of -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 -the neanderthaloids proper--about half way between Steinheim and the -classic Neanderthal people. - -Professor Howell believes that the pre-neanderthaloids who happened to -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” -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 -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 -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. - -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 -something about how they lived, and about some of the tools they made. - - -OTHER MEN CONTEMPORARY WITH THE NEANDERTHALOIDS - -We have seen that the neanderthaloids seem to be a specialization -in a corner of Europe. What was going on elsewhere? We think that -the pre-neanderthaloid type was a generally widespread form of men. -From this type evolved other more or less extreme although generally -related men. The Solo finds in Java form one such case. Another was the -Rhodesian man of Africa, and the more recent Hopefield finds show more -of the general Rhodesian type. It is more confusing than it needs to be -if these cases outside western Europe are called neanderthaloids. They -lived during the same approximate time range but they were all somewhat -different-looking people. - - -EARLY MODERN MEN - -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 -early modern men. Now that Piltdown has been liquidated and Galley Hill -is known to be very late, what is left of the idea? - -The backs of the skulls of the Swanscombe and Steinheim finds look -rather modern. Unless you pay attention to the face and forehead of the -Steinheim find--which not many people have--and perhaps also consider -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 -interglacial, as did the pre-neanderthaloids. The crowns 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 -fossils, as modern type men, is little stronger than that for -Swanscombe, although Professor Vallois believes it a good case. - -It seems to add up to the fact that there were people living in -Europe--before the classic neanderthaloids--who looked more modern, -in some features, than the classic western neanderthaloids did. Our -best suggestion of what men looked like--just before they became fully -modern--comes from a cave on Mount Carmel in Palestine. - - -THE FIRST MODERNS - -Professor T. D. McCown and the late Sir Arthur Keith, who studied the -Mount Carmel bones, figured out that one of the two groups involved -was as much as 70 per cent modern. There were, in fact, two groups or -varieties of men in the Mount Carmel caves and in at least two other -Palestinian caves of about the same time. The time would be about that -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 -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 -group and the pre-neanderthaloid group came from exactly the same time. -The conclusion was quite natural: here was a population of men in the -act of evolving in two different directions. But the time may not be -exactly the same. It is very difficult to be precise, within say 10,000 -years, for a time some 75,000 years ago. If the Skhul men are in fact -later than the pre-neanderthaloid group of Palestine, as some of us -think, then they show how relatively modern some men were--men who -lived at the same time as the classic Neanderthalers of the European -pocket. - -Soon after the first extremely cold phase of the last glaciation, we -begin to get a number of bones of completely modern men in Europe. -We also get great numbers of the tools they made, and their living -places in caves. Completely modern skeletons begin turning up in caves -dating back to toward 40,000 years ago. The time is about that of the -beginning of the second phase of the last glaciation. These skeletons -belonged to people no different from many people we see today. Like -people today, not everybody looked alike. (The positions of the more -important fossil men of later Europe are shown in the chart on page -72.) - - -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 -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”). - -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 -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 -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 -not proper here. It is also not clear whether the features of the bones -claimed to be “Negroid” are really so at all. - -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 -the Bushmen seem to have once been very widespread south of the Sahara -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.” - -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. - - -WHAT THE DIFFERENCES MEAN - -What does all this difference mean? It means that, at one moment in -time, within each different area, men tended to look somewhat alike. -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 -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”). -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 -self-righteous nonsense when we try to kid ourselves into thinking that -they do. - -I am not going to struggle with the whole business of modern stocks and -races. This is a book about prehistoric men, not recent historic or -modern men. My physical anthropologist friends have been very patient -in helping me to write and rewrite this chapter--I am not going to -break their patience completely. Races are their business, not mine, -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 - population which differs significantly from other human populations - in regard to the frequency of one or more of the genes it - possesses.” - - Professor Sherwood L. Washburn, professor of Physical Anthropology, - 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.” - -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 -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 -race proper, which is a matter of the biology of groups. - -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 -it. Some unfortunate genetic trait soon turns up, so this has just as -carefully to be bred out again, and so on. - - -SUMMARY OF PRESENT KNOWLEDGE OF FOSSIL MEN - -The earliest bones of men we now have--upon which all the experts -would probably agree--are those of _Meganthropus_, from Java, of about -450,000 years ago. The earlier australopithecines of Africa were -possibly not tool-users and may not have been ancestral to men at all. -But there is an alternate and evidently increasingly stronger chance -that some of them may have been. The Kanam jaw from Kenya, another -early possibility, is not only very incomplete but its find-spot is -very questionable. - -Java man proper, _Pithecanthropus_, comes next, at about 400,000 years -ago, and the big Heidelberg jaw in Germany must be of about the same -date. Next comes Swanscombe in England, Steinheim in Germany, the -Ternafine jaws in Algeria, and Peking man, _Sinanthropus_. They all -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 -Heidelberg to the Neanderthalers where it became extinct, and (2) a -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 -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. -But on Mount Carmel, the Skhul cave-find with its 70 per cent modern -features shows what could happen elsewhere at the same time. - -Lastly, from about 40,000 or 35,000 years ago--the time of the onset -of the second phase of the last glaciation--we begin to find the fully -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. -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 -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 -little Cro-Magnon boy bragging to his friends about his tough, strong, -Neanderthaler great-great-great-great-grandfather! - - - - -Cultural BEGINNINGS - -[Illustration] - - -Men, unlike the lower animals, are made up of much more than flesh and -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 -whether or not he eats peas with his knife. - -The anthropologists use the word too, and argue heatedly over its finer -meanings; but they all agree that every human being is part of or has -some kind of culture. Each particular human group has a particular -culture; that is one of the ways in which we can tell one group of -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, -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 -generation to another. They are what really characterize different -human groups. - - -SOME CHARACTERISTICS OF CULTURE - -A culture lasts, although individual men in the group die off. On -the other hand, a culture changes as the different conventions and -understandings change. You could almost say that a culture lives in the -minds of the men who have it. But people are not born with it; they -get it as they grow up. Suppose a day-old Hungarian baby is adopted by -a family in Oshkosh, Wisconsin, and the child is not told that he is -Hungarian. He will grow up with no more idea of Hungarian culture than -anyone else in Oshkosh. - -So when I speak of ancient Egyptian culture, I mean the whole body -of understandings and beliefs and knowledge possessed by the ancient -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. - - -PREHISTORIC CULTURES - -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 -tell us something about the ways these men lived and the things they -believed. But the story we learn of the very early cultures must be -only a very small part of the whole, for we find so few things. The -rest of the story is gone forever. We have to do what we can with what -we find. - -For all of the time up to about 75,000 years ago, which was the time -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 -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 -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. - - -WHY LITTLE HAS LASTED FROM EARLY TIMES - -Except for the rare find-spots mentioned above, all our very early -finds come from geological deposits, or from the wind-blown surfaces -of deserts. Here is what the business of geological deposits really -means. Let us say that a group of people was living in England about -300,000 years ago. They made the tools they needed, lived in some sort -of camp, almost certainly built fires, and perhaps buried their dead. -While the climate was still warm, many generations may have lived in -the same place, hunting, and gathering nuts and berries; but after some -few thousand years, the weather began very gradually to grow colder. -These early Englishmen would not have known that a glacier was forming -over northern Europe. They would only have noticed that the animals -they hunted seemed to be moving south, and that the berries grew larger -toward the south. So they would have moved south, too. - -The camp site they left is the place we archeologists would really have -liked to find. All of the different tools the people used would have -been there together--many broken, some whole. The graves, and traces -of fire, and the tools would have been there. But the glacier got -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 -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 -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 -materials were dropped by the rushing waters as gravels, miles from -the front of the glacier. At last the glacier reached its greatest -extent; then it melted backward toward the north. Debris held in the -ice was dropped where the ice melted, or was flushed off by more melt -water. When the glacier, leaving the land, had withdrawn to the sea, -great hunks of ice were broken off as icebergs. These icebergs probably -dropped the materials held in their ice wherever they floated and -melted. There must be many tools and fragmentary bones of prehistoric -men on the bottom of the Atlantic Ocean and the North Sea. - -Remember, too, that these glaciers came and went at least three or four -times during the Ice Age. Then you will realize why the earlier things -we find are all mixed up. Stone tools from one camp site got mixed up -with stone tools from many other camp sites--tools which may have been -made tens of thousands or more years apart. The glaciers mixed them -all up, and so we cannot say which particular sets of tools belonged -together in the first place. - - -“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 -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 -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” -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.” - - -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 -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 -_standardization_. Here, men began to make tools according to certain -set traditions. Counting the better-made pebble tools, there are four -such traditions or sets of habits for the production of stone tools in -earliest prehistoric times. Toward the end of the Pleistocene, a fifth -tradition appears. - - -PEBBLE TOOLS - -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 -come from Africa. They date back to the time of the first great -alpine glaciation and are at least 500,000 years old. The earliest -ones are made of split pebbles, about the size of your fist or a bit -bigger. They go under the name of pebble tools. There are many natural -exposures of early Pleistocene geological beds in Africa, and the -prehistoric archeologists of south and central Africa have concentrated -on searching for early tools. Other finds of early pebble tools have -recently been made in Algeria and Morocco. - -[Illustration: SOUTH AFRICAN PEBBLE TOOL] - -There are probably early pebble tools to be found in areas of the -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” -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. - - -CORE-BIFACE 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. -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 -trimmed to a thin tip. The real purpose in trimming down the two faces -was to get a good cutting edge all around. You can see all this in the -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 -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 -be pointed, it seems likely that they were used for hitting, picking, -and prying. But they have rough cutting edges, so they could have been -used for chopping, scraping, and cutting. - - -FLAKE TOOLS - -The third tradition is the _flake_ tradition. The idea was to get a -tool with a good cutting edge by simply knocking a nice large flake off -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. - -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 -to have been to produce a broad, sharp, cutting edge. - -[Illustration: CLACTONIAN FLAKE] - -The core-biface and the flake traditions were spread, from earliest -times, over much of Europe, Africa, and western Asia. The map on page -52 shows the general area. Over much of this great region there was -flint. Both of these traditions seem well adapted to flint, although -good core-bifaces and flakes were made from other kinds of stone, -especially in Africa south of the Sahara. - - -CHOPPERS AND ADZE-LIKE TOOLS - -The fourth early tradition is found in southern and eastern Asia, from -northwestern India through Java and Burma into China. Father Maringer -recently reported an early group of tools in Japan, which most resemble -those of Java, called Patjitanian. The prehistoric men in this general -area mostly used quartz and tuff and even petrified wood for their -stone tools (see illustration, p. 46). - -This fourth early tradition is called the _chopper-chopping tool_ -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 -an ax is < shaped. - -[Illustration: ANYATHIAN ADZE-LIKE TOOL] - -There are also pointed pebble tools. Thus the tool kit of these early -south and east Asiatic peoples seems to have included tools for doing -as many different jobs as did the tools of the Western traditions. - -Dr. H. L. Movius has emphasized that the tools which were found in the -Peking cave with Peking man belong to the chopper-tool tradition. This -is the only case as yet where the tools and the man have been found -together from very earliest times--if we except Sterkfontein. - - -DIFFERENCES WITHIN THE TOOL-MAKING TRADITIONS - -The latter three great traditions in the manufacture of stone -tools--and the less clear-cut pebble tools before them--are all we have -to show of the cultures of the men of those times. Changes happened in -each of the traditions. As time went on, the tools in each tradition -were better made. There could also be slight regional differences in -the tools within one tradition. Thus, tools with small differences, but -all belonging to one tradition, can be given special group (facies) -names. - -This naming of special groups has been going on for some time. Here are -some of these names, since you may see them used in museum displays -of flint tools, or in books. Within each tradition of tool-making -(save the chopper tools), the earliest tool type is at the bottom -of the list, just as it appears in the lowest beds of a geological -stratification.[3] - - [3] Archeologists usually make their charts and lists with the - earliest materials at the bottom and the latest on top, since - this is the way they find them in the ground. - - Chopper tool (all about equally early): - Anyathian (Burma) - Choukoutienian (China) - Patjitanian (Java) - Soan (India) - - Flake: - “Typical Mousterian” - Levalloiso-Mousterian - Levalloisian - Tayacian - Clactonian (localized in England) - - Core-biface: - Some blended elements in “Mousterian” - Micoquian (= Acheulean 6 and 7) - Acheulean - Abbevillian (once called “Chellean”) - - Pebble tool: - Oldowan - Ain Hanech - pre-Stellenbosch - Kafuan - -The core-biface and the flake traditions appear in the chart (p. 65). - -The early archeologists had many of the tool groups named before they -ever realized that there were broader tool preparation traditions. This -was understandable, for in dealing with the mixture of things that come -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 -of the early traditions the various early groups belong. - - -THE MEANING OF THE DIFFERENT TRADITIONS - -What do the traditions really mean? I see them as the standardization -of ways to make tools for particular jobs. We may not know exactly what -job the maker of a particular core-biface or flake tool had in mind. We -can easily see, however, that he already enjoyed a know-how, a set of -persistent habits of tool preparation, which would always give him the -same type of tool when he wanted to make it. Therefore, the traditions -show us that persistent habits already existed for the preparation of -one type of tool or another. - -This tells us that one of the characteristic aspects of human culture -was already present. There must have been, in the minds of these -early men, a notion of the ideal type of tool for a particular job. -Furthermore, since we find so many thousands upon thousands of tools -of one type or another, the notion of the ideal types of tools _and_ -the know-how for the making of each type must have been held in common -by many men. The notions of the ideal types and the know-how for their -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 -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 -suppose that it is not impossible. - -There may, of course, be a real question whether tool-utilizing -creatures--our first step, on page 42--were actually men. Other -animals utilize things at hand as tools. The tool-fashioning creature -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 -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 -no longer preserved for the archeologist to find--I believe there would -be clear signs of further conventional understandings. The men may have -been physically primitive and pretty shaggy in appearance, but I think -we must surely call them men. - - -AN OLDER INTERPRETATION OF THE WESTERN TRADITIONS - -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 -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 -said to include Heidelberg, the proto-neanderthaloids and classic -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 -Piltdown liquidated, Galley Hill known to be very late, and Swanscombe -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 -archeological evidence was looked at in one short-sighted way, the -archeological materials _seemed_ to fit the parallel phyla theory. It -was simply necessary to believe that the flake tools were made only -by the paleoanthropic Neanderthaler line, and that the more handsome -core-biface tools were the product of the neanthropic modern-man line. - -Remember that _almost_ all of the early prehistoric European tools -came only from the redeposited gravel beds. This means that the tools -were not normally found in the remains of camp sites or work shops -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. -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 -think that the groups stand for something _on their own_. - -In 1906, M. Victor Commont actually made a rare find of what seems -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 -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 -ran heavily to flake tools. All in all, the parallel phyla theory -flourished because it seemed so neat and easy to understand. - - -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 -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 -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 -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. - -[Illustration: DISTRIBUTION OF TOOL-PREPARATION TRADITIONS - -Time approximately 100,000 years ago] - -There are certainly a few cases in which flake tools did appear with -few or no core-bifaces. The flake-tool group called Clactonian in -England is such a case. Another good, but certainly later case is -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 -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 -circumstances. - - -LIFE IN EARLIEST TIMES - -What do we actually know of life in these earliest times? In the -glacial gravels, or in the terrace gravels of rivers once swollen by -floods of melt water or heavy rains, or on the windswept deserts, we -find stone tools. The earliest and coarsest of these are the pebble -tools. We do not yet know what the men who made them looked like, -although the Sterkfontein australopithecines probably give us a good -hint. Then begin the more formal tool preparation traditions of the -west--the core-bifaces and the flake tools--and the chopper-chopping -tool series of the farther east. There is an occasional roughly worked -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” -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 -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 -mixture within the geological contexts. - -The stone tools of each of the earlier traditions are the simplest -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. - -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 -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 -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 -groups of three round stones on the site which suggest that the people -used bolas. The South American Indians used three-ball bolas, with the -stones in separate leather bags connected by thongs. These were whirled -and then thrown through the air so as to entangle the feet of a fleeing -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 -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 -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. - - -WHAT THE PEKING CAVE-FINDS TELL US - -The one early cave-dwelling we have found is that of Peking man, in -China. Peking man had fire. He probably cooked his meat, or used -the fire to keep dangerous animals away from his den. In the cave -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 -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 -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 -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 -eat the bone marrow, but he may have used some of them as tools. - -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 -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. - - * * * * * - -This rounds out about all we can say of the life and times of early -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, -and that these early people were really _men_. - - - - -MORE EVIDENCE of Culture - -[Illustration] - - -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 -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 -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. - - -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, -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 -thicker. Then there might have been a break in the occupation of the -cave for a while. Perhaps the game animals got scarce and the people -moved away; or maybe the cave became flooded. Later on, other people -moved in and began making a new layer of their own on top of the first -layer. Perhaps this process of layering went on in the same cave for a -hundred thousand years; you can see what happened. The drawing on this -page shows a section through such a cave. The earliest layer is on the -bottom, the latest one on top. They go in order from bottom to top, -earliest to latest. This is the _stratification_ we talked about (p. -12). - -[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 -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 -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 -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, -as a place in which people actually lived. - -The most important European cave sites are in Spain, France, and -central Europe; there are also sites in England and Italy. A few caves -are known in the Near East and Africa, and no doubt more sites will be -found when the out-of-the-way parts of Europe, Africa, and Asia are -studied. - - -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 -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 -(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.” -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. -Archeological context means finding things just as men left them. 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 -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 -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 -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 -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 -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, -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 -a European cave. There is so little of this material available that -we do not know which is really typical and which is not. You would -probably find it hard to see much difference between this industry and -a collection of tools of the type called Clactonian, taken from the -glacial gravels, especially if the Clactonian tools were small-sized. - -The stone industry of the bottommost layer of the Mount Carmel cave, -in Palestine, where somewhat similar tools were found, has also been -called Tayacian. - -I shall have to bring in many unfamiliar words for the names of the -industries. The industries are usually named after the places where -they were first found, and since these were in most cases in France, -most of the names which follow will be of French origin. However, -the names have simply become handles and are in use far beyond the -boundaries of France. It would be better if we had a non-place-name -terminology, but archeologists have not yet been able to agree on such -a terminology. - - -THE ACHEULEAN INDUSTRY - -Both in France and in Palestine, as well as in some African cave -sites, the next layers in the deep caves have an industry in both the -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 -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. - -The Acheulean core-biface type of tool is worked on two faces so as -to give a cutting edge all around. The outline of its front view may -be oval, or egg-shaped, or a quite pointed pear shape. The large -chip-scars of the Acheulean core-bifaces are shallow and flat. It is -suspected that this resulted from the removal of the chips with a -wooden club; the deep chip-scars of the earlier Abbevillian core-biface -came from beating the tool against a stone anvil. These tools are -really the best and also the final products of the core-biface -tradition. We first noticed the tradition in the early glacial gravels -(p. 43); now we see its end, but also its finest examples, in the -deeper cave levels. - -The flake tools, which really make up the greater bulk of this -industry, are simple scrapers and chips with sharp cutting edges. The -habits used to prepare them must have been pretty much the same as -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 -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 -animal bones are those of beasts that live in a warm climate. They -belonged in the last interglacial period. We have not yet found the -bones of fossil men definitely in place with this industry. - -[Illustration: ACHEULEAN BIFACE] - - -FLAKE INDUSTRIES FROM THE CAVES - -Two more stone industries--the _Levalloisian_ and the -“_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” -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 -there may be some kinds of Acheulean industries that are later than -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 -of the two traditions--core-biface or flake--or on combinations of -the habits used in the preparation techniques of both traditions. All -were popular at about the same time. So we find that people who made -one kind of stone tool industry lived in a cave for a while. Then they -gave up the cave for some reason, and people with another industry -moved in. Then the first people came back--or at least somebody with -the same tool-making habits as the first people. Or maybe a third group -of tool-makers moved in. The people who had these different habits for -making their stone tools seem to have moved around a good deal. They no -doubt borrowed and exchanged tricks of the trade with each other. There -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 -method of great importance for understanding these tool preparation -habits. - - -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 -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 -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 -points and side scrapers are illustrated (pp. 64 and 66). - -[Illustration: LEVALLOIS FLAKE] - - -THE MIXING OF TRADITIONS - -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’ -systematic work is beginning to clear up some of our confusion. - -The time of these late Acheuleo-Levalloiso-Mousterioid industries -is from perhaps as early as 100,000 years ago. It may have lasted -until well past 50,000 years ago. This was the time of the first -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 -of industries with tools of both core-biface and flake habits, and -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). - -[Illustration: MOUSTERIAN POINT] - - -OTHER SUGGESTIONS OF LIFE IN THE EARLY CAVE LAYERS - -Except for the stone tools, what do we know of the way men lived in the -time range after 100,000 to perhaps 40,000 years ago or even later? -We know that in the area from Europe to Palestine, at least some of -the people (some of the time) lived in the fronts of caves and warmed -themselves over fires. In Europe, in the cave layers of these times, -we find the bones of different animals; the bones in the lowest layers -belong to animals that lived in a warm climate; above them are the -bones of those who could stand the cold, like the reindeer and mammoth. -Thus, the meat diet must have been changing, as the glacier crept -farther south. Shells and possibly fish bones have lasted in these -cave layers, but there is not a trace of the vegetable foods and the -nuts and berries and other wild fruits that must have been eaten when -they could be found. - -[Illustration: CHART SHOWING PRESENT UNDERSTANDING OF RELATIONSHIPS AND -SUCCESSION OF TOOL-PREPARATION TRADITIONS, INDUSTRIES, AND ASSEMBLAGES -OF WEST-CENTRAL EUROPE - -Wavy lines indicate transitions in industrial habits. These transitions -are not yet understood in detail. The glacial and climatic scheme shown -is the alpine one.] - -Bone tools have also been found from this period. Some are called -scrapers, and there are also long chisel-like leg-bone fragments -believed to have been used for skinning animals. Larger hunks of bone, -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. - -[Illustration: MOUSTERIAN SIDE SCRAPER] - -There is a small but certain number of cases of intentional burials. -These burials have been found on the floors of the caves; in other -words, the people dug graves in the places where they lived. The holes -made for the graves were small. For this reason (or perhaps for some -other?) the bodies were in a curled-up or contracted position. Flint or -bone tools or pieces of meat seem to have been put in with some of the -bodies. In several cases, flat stones had been laid over the graves. - - -TOOLS FROM AFRICA AND ASIA ABOUT 100,000 YEARS AGO - -Professor Movius characterizes early prehistoric Africa as a continent -showing a variety of stone industries. Some of these industries were -purely local developments and some were practically identical with -industries found in Europe at the same time. From northwest Africa -to Capetown--excepting the tropical rain forest region of the west -center--tools of developed Acheulean, Levalloisian, and Mousterian -types have been recognized. Often they are named after African place -names. - -In east and south Africa lived people whose industries show a -development of the Levalloisian technique. Such industries are -called Stillbay. Another industry, developed on the basis of the -Acheulean technique, is called Fauresmith. From the northwest comes -an industry with tanged points and flake-blades; this is called the -Aterian. The tropical rain forest region contained people whose stone -tools apparently show adjustment to this peculiar environment; the -so-called Sangoan industry includes stone picks, adzes, core-bifaces -of specialized Acheulean type, and bifacial points which were probably -spearheads. - -In western Asia, even as far as the east coast of India, the tools of -the Eurafrican core-biface and flake tool traditions continued to be -used. But in the Far East, as we noted in the last chapter, men had -developed characteristic stone chopper and chopping tools. This tool -preparation tradition--basically a pebble tool tradition--lasted to the -very end of the Ice Age. - -When more intact open air sites such as that of an earlier time at -Olorgesailie, and more stratified cave sites are found and excavated -in Asia and Africa, we shall be able to get a more complete picture. -So far, our picture of the general cultural level of the Old World at -about 100,000 years ago--and soon afterwards--is best from Europe, but -it is still far from complete there, too. - - -CULTURE AT THE BEGINNING OF THE LAST GREAT GLACIAL PERIOD - -The few things we have found must indicate only a very small part -of the total activities of the people who lived at the time. All of -the things they made of wood and bark, of skins, of anything soft, -are gone. The fact that burials were made, at least in Europe and -Palestine, is pretty clear proof that the people had some notion of a -life after death. But what this notion really was, or what gods (if -any) men believed in, we cannot know. Dr. Movius has also reminded me -of the so-called bear cults--cases in which caves have been found which -contain the skulls of bears in apparently purposeful arrangement. This -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 -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 -growing, and how there may have been witch doctors who made beginnings -in medicine or in art, in the materials they gathered for their trade. - -The stone tools help us most. They have lasted, and we can find -them. As they come to us, from this cave or that, and from this -layer or that, the tool industries show a variety of combinations -of the different basic habits or traditions of tool preparation. -This seems only natural, as the groups of people must have been very -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. -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 -of men to adapt themselves to a variety of conditions. We shall see -this adaptive ability even more clearly as time goes on and the record -becomes more complete. - -Over how great an area did these loosely related cultures reach in -the time 75,000 to 45,000 or even as late as 35,000 years ago? We -have described stone tools made in one or another of the flake and -core-biface habits, for an enormous area. It covers all of Europe, all -of Africa, the Near East, and parts of India. It is perfectly possible -that the flake and core-biface habits lasted on after 35,000 years ago, -in some places outside of Europe. In northern Africa, for example, we -are certain that they did (see chart, p. 72). - -On the other hand, in the Far East (China, Burma, Java) and in northern -India, the tools of the old chopper-tool tradition were still being -made. Out there, we must assume, there was a different set of loosely -related cultures. At least, there was a different set of loosely -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 -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 -Teshik Tash, in Trans-caspian Russia. It is still too soon to know -whether certain combinations of tools within industries were made -only by certain physical types of men. But since tools of both the -core-biface and the flake traditions, and their blends, turn up from -South Africa to England to India, it is most unlikely that only one -type of man used only one particular habit in the preparation of tools. -What seems perfectly clear is that men in Africa and men in India were -making just as good tools as the men who lived in western Europe. - - - - -EARLY MODERNS - -[Illustration] - - -From some time during the first inter-stadial of the last great -glaciation (say some time after about 40,000 years ago), we have -more accurate dates for the European-Mediterranean area and less -accurate ones for the rest of the Old World. This is probably -because the effects of the last glaciation have been studied in the -European-Mediterranean area more than they have been elsewhere. - - -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 -for dates is very inaccurate for the areas outside of Europe and the -Mediterranean. - -We can at least make a guess. In Egypt and north Africa, men were still -using the old methods of making stone tools. This was especially true -of flake tools of the Levalloisian type, save that they were growing -smaller and smaller as time went on. But at the same time, a new -tradition was becoming popular in westernmost Asia and in Europe. This -was the blade-tool tradition. - - -BLADE TOOLS - -A stone blade is really just a long parallel-sided flake, as the -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 -which to whack off blades. - -[Illustration: PLAIN BLADE] - -You whack with a hammer stone against a bone or antler punch which is -directed at the proper place on the blade-core. The blade-core has to -be well supported or gripped while this is going on. To get a good -flint blade tool takes a great deal of know-how. - -Remember that a tradition in stone tools means no more than that some -particular way of making the tools got started and lasted a long time. -Men who made some tools in one tradition or set of habits would also -make other tools for different purposes by means of another tradition -or set of habits. It was even possible for the two sets of habits to -become combined. - - -THE EARLIEST BLADE TOOLS - -The oldest blade tools we have found were deep down in the layers of -the Mount Carmel caves, in Tabun Eb and Ea. Similar tools have been -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. -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. - -[Illustration: SUCCESSION OF ICE AGE FLINT TYPES, INDUSTRIES, AND -ASSEMBLAGES, AND OF FOSSIL MEN, IN NORTHWESTERN EURAFRASIA] - -For some reason, the habit of making blades in Palestine and Syria was -interrupted. Blades only reappeared there at about the same time they -were first made in Europe, some time after 45,000 years ago; that is, -after the first phase of the last glaciation was ended. - -[Illustration: BACKED BLADE] - -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 -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 -the blade tools reappear in the Syro-Palestinian area, they do so in -industries which also include Levalloiso-Mousterian flake tools. From -the point of view of form and workmanship, the blade tools themselves -are not so fine as those which seem to be making their appearance -in western Europe about the same time. There is a characteristic -Syro-Palestinian flake point, possibly a projectile tip, called the -Emiran, which is not known from Europe. The appearance of blade tools, -together with Levalloiso-Mousterian flakes, continues even after the -Emiran point has gone out of use. - -It seems clear that the production of blade tools did not immediately -swamp the set of older habits in Europe, too; the use of flake -tools also continued there. This was not so apparent to the older -archeologists, whose attention was focused on individual tool types. It -is not, in fact, impossible--although it is certainly not proved--that -the technique developed in the preparation of the Levalloisian tortoise -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 -having enough individual character to be classified as distinct ... -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 -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. - -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 -complication in what we find. The chart (p. 72) tries to simplify -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 -simplified. - -You will realize that all this complication comes not only from -the fact that we are finding more material. It is due also to the -increasing ability of men to adapt themselves to a great variety of -situations. Their tools indicate this adaptiveness. We know there was -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 -increasing ability to adapt themselves. - - -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 - 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 - 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 - and bone; that is, to make handles or shafts for other tools. - They must also be the tools with which much of the engraving on - bone (see p. 83) was done. There is a bewildering variety of - different kinds of burins. - -[Illustration: TANGED POINT] - - 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 - either one or two shoulders. - -[Illustration: NOTCHED BLADE] - - 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. - Our spoke-shaves have sharp straight cutting blades and really - “shave.” Notched blades of flint probably scraped rather than cut. - - 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 - 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). - -There is one very special type of flint tool, which is best known from -western Europe in an industry called the Solutrean. These tools were -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 - dagger-like, and must have been used as knives or daggers. Others - 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 - types are illustrated (see above and p. 79). - -[Illustration: END-SCRAPER ON A BLADE] - -[Illustration: LAUREL LEAF POINT] - -The industries characterized by tools in the blade tradition also -yield some flake and core tools. We will end this list with two types -of tools that appear at this time. The first is made of a flake; the -second is a core tool. - -[Illustration: SHOULDERED POINT] - - 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) - 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 - 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 - large enough to be easily grasped, and probably were used like - push planes (see illustration, p. 81). - -[Illustration: KEEL-SHAPED ROUND SCRAPER] - -[Illustration: KEEL-SHAPED NOSED SCRAPER] - -The stone tools (usually made of flint) we have just listed are among -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 -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 -layer. - - -OTHER KINDS OF TOOLS - -By this time in Europe--say from about 40,000 to about 10,000 years -ago--we begin to find other kinds of material too. Bone tools begin -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. - -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 -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. - -[Illustration: SPLIT-BASED BONE POINT] - -[Illustration: SPEAR-THROWER] - -[Illustration: BONE HARPOON] - -Antler came into use for tools, especially in central and western -Europe. We do not know the use of one particular antler tool that -has a large hole bored in one end. One suggestion is that it was -a thong-stropper used to strop or work up hide thongs (see -illustration, below); another suggestion is that it was an arrow-shaft -straightener. - -Another interesting tool, usually of antler, is the spear-thrower, -which is little more than a stick with a notch or hook on one end. -The hook fits into the butt end of the spear, and the length of the -spear-thrower allows you to put much more power into the throw (p. -82). It works on pretty much the same principle as the sling. - -Very fancy harpoons of antler were also made in the latter half of -the period in western Europe. These harpoons had barbs on one or both -sides and a base which would slip out of the shaft (p. 82). Some have -engraved decoration. - - -THE BEGINNING OF ART - -[Illustration: THONG-STROPPER] - -In western Europe, at least, the period saw the beginning of several -kinds of art work. It is handy to break the art down into two great -groups: the movable art, and the cave paintings and sculpture. The -movable art group includes the scratchings, engravings, and modeling -which decorate tools and weapons. Knives, stroppers, spear-throwers, -harpoons, and sometimes just plain fragments of bone or antler are -often carved. There is also a group of large flat pebbles which seem -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] - -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 -meant to show the great forces of nature--fertility and the birth of -life. - - -CAVE PAINTINGS - -In the paintings on walls and ceilings of caves we have some examples -that compare with the best art of any time. The subjects were usually -animals, the great cold-weather beasts of the end of the Ice Age: the -mammoth, the wooly rhinoceros, the bison, the reindeer, the wild horse, -the bear, the wild boar, and wild cattle. As in the movable art, there -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 -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 -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 -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. - -The cave art of the Franco-Cantabrian style is one of the great -artistic achievements of all time. The subjects drawn are almost always -the larger animals of the time: the bison, wild cattle and horses, the -wooly rhinoceros, the mammoth, the wild boar, and the bear. In some of -the best examples, the beasts are drawn in full color and the paintings -are remarkably alive and charged with energy. They come from the hands -of men who knew the great animals well--knew the feel of their fur, the -tremendous drive of their muscles, and the danger one faced when he -hunted them. - -Another artistic style has been found in eastern Spain. It includes -lively drawings, often of people hunting with bow and arrow. The East -Spanish art is found on open rock faces and in rock-shelters. It is -less spectacular and apparently more recent than the Franco-Cantabrian -cave art. - - -LIFE AT THE END OF THE ICE AGE IN EUROPE - -Life in these times was probably as good as a hunter could expect it -to be. Game and fish seem to have been plentiful; berries and wild -fruits probably were, too. From France to Russia, great pits or -piles of animal bones have been found. Some of this killing was done -as our Plains Indians killed the buffalo--by stampeding them over -steep river banks or cliffs. There were also good tools for hunting, -however. In western Europe, people lived in the openings of caves and -under overhanging rocks. On the great plains of eastern Europe, very -crude huts were being built, half underground. The first part of this -time must have been cold, for it was the middle and end phases of the -last great glaciation. Northern Europe from Scotland to Scandinavia, -northern Germany and Russia, and also the higher mountains to the -south, were certainly covered with ice. But people had fire, and the -needles and tools that were used for scraping hides must mean that they -wore clothing. - -It is clear that men were thinking of a great variety of things beside -the tools that helped them get food and shelter. Such burials as we -find have more grave-gifts than before. Beads and ornaments and often -flint, bone, or antler tools are included in the grave, and sometimes -the body is sprinkled with red ochre. Red is the color of blood, which -means life, and of fire, which means heat. Professor Childe wonders if -the red ochre was a pathetic attempt at magic--to give back to the body -the heat that had gone from it. But pathetic or not, it is sure proof -that these people were already moved by death as men still are moved by -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 -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 -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, -and were provided with shell beads and ornaments and with some blade -tools of flint. Regardless of the different names once given them by -the human paleontologists, each of these groups seems to have shared -equally in the cultural achievements of the times, for all that the -archeologists can say. - - -MICROLITHS - -One peculiar set of tools seems to serve as a marker for the very last -phase of the Ice Age in southwestern Europe. This tool-making habit is -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, -and several other geometric forms. These little tools are called -“microliths.” They are so small that most of them must have been fixed -in handles or shafts. - -[Illustration: MICROLITHS - - BLADE FRAGMENT - BURIN - LUNATE - TRAPEZOID - SCALENE TRIANGLE - ARROWHEAD] - -We have found several examples of microliths mounted in shafts. In -northern Europe, where their use soon spread, the microlithic triangles -or lunates were set in rows down each side of a bone or wood point. -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 -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 -fleet-footed animals like the gazelle, which might run for miles after -being shot with an arrow, it was an advantage to cause as much bleeding -as possible, for the animal would drop sooner. - -We are not really sure where the microliths were first invented. There -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. - -Remember that the microliths we are talking about here were made from -carefully prepared little blades, and are often geometric in outline. -Each microlithic industry proper was made up, in good part, of such -tiny blade tools. But there were also some normal-sized blade tools and -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 -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. - - -LATER BLADE-TOOL INDUSTRIES OF THE NEAR EAST AND AFRICA - -The blade-tool industries of normal size we talked about earlier spread -from Europe to central Siberia. We noted that blade tools were made -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 -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 -example; its industry includes a long spike-like blade point which has -no western European counterpart. - -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 -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 -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. - - -NETHER AFRICA - -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.” -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 -time. - - -THE FAR EAST - -India and the Far East still seem to be going their own way. In India, -some blade tools have been found. These are not well dated, save that -we believe they must be post-Pleistocene. In the Far East it looks as -if the old chopper-tool tradition was still continuing. For Burma, -Dr. Movius feels this is fairly certain; for China he feels even more -certain. Actually, we know very little about the Far East at about the -time of the last glaciation. This is a shame, too, as you will soon -agree. - - -THE NEW WORLD BECOMES INHABITED - -At some time toward the end of the last great glaciation--almost -certainly after 20,000 years ago--people began to move over Bering -Strait, from Asia into America. As you know, the American Indians have -been assumed to be basically Mongoloids. New studies of blood group -types make this somewhat uncertain, but there is no doubt that the -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 -found here. - -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 -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 -spreading up that far. - - -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 -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 -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 -begun. We shall see further intensifications of it in the next chapter. - - - - -End and PRELUDE - -[Illustration] - - -Up to the end of the last glaciation, we prehistorians have a -relatively comfortable time schedule. The farther back we go the less -exact we can be about time and details. Elbow-room of five, ten, -even fifty or more thousands of years becomes available for us to -maneuver in as we work backward in time. But now our story has come -forward to the point where more exact methods of dating are at hand. -The radioactive carbon method reaches back into the span of the last -glaciation. There are other methods, developed by the geologists and -paleobotanists, which supplement and extend the usefulness of the -radioactive carbon dates. And, happily, as our means of being more -exact increases, our story grows more exciting. There are also more -details of culture for us to deal with, which add to the interest. - - -CHANGES AT THE END OF THE ICE AGE - -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 -always changing for the better; but there was at least one time when -European weather was even better than it is now. - -The melting back of the glaciers and the weather fluctuations caused -other changes, too. We know a fair amount about these changes in -Europe. In an earlier chapter, we said that the whole Ice Age was a -matter of continual change over long periods of time. As the last -glaciers began to melt back some interesting things happened to mankind. - -In Europe, along with the melting of the last glaciers, geography -itself was changing. Britain and Ireland had certainly become islands -by 5000 B.C. The Baltic was sometimes a salt sea, sometimes a large -fresh-water lake. Forests began to grow where the glaciers had been, -and in what had once been the cold tundra areas in front of the -glaciers. The great cold-weather animals--the mammoth and the wooly -rhinoceros--retreated northward and finally died out. It is probable -that the efficient hunting of the earlier people of 20,000 or 25,000 -to about 12,000 years ago had helped this process along (see p. 86). -Europeans, especially those of the post-glacial period, had to keep -changing to keep up with the times. - -The archeological materials for the time from 10,000 to 6000 B.C. seem -simpler than those of the previous five thousand years. The great cave -art of France and Spain had gone; so had the fine carving in bone and -antler. Smaller, speedier animals were moving into the new forests. New -ways of hunting them, or ways of getting other food, had to be found. -Hence, new tools and weapons were necessary. Some of the people who -moved into northern Germany were successful reindeer hunters. Then the -reindeer moved off to the north, and again new sources of food had to -be found. - - -THE READJUSTMENTS COMPLETED IN EUROPE - -After a few thousand years, things began to look better. Or at least -we can say this: By about 6000 B.C. we again get hotter archeological -materials. The best of these come from the north European area: -Britain, Belgium, Holland, Denmark, north Germany, southern Norway and -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 - archeological materials belonging together, from one area and of - 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 - 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, - canals, dams, irrigation ditches, and any other traces we might - leave of ourselves, from graves to garbage dumps. Not everything - would last, so that an archeologist digging us up--say 2,000 - years from now--would find only the most durable items in our - assemblage. - -The best known of these assemblages is the _Maglemosian_, named after a -great Danish peat-swamp where much has been found. - -[Illustration: SKETCH OF MAGLEMOSIAN ASSEMBLAGE - - CHIPPED STONE - HEMP - GROUND STONE - BONE AND ANTLER - WOOD] - -In the Maglemosian assemblage the flint industry was still very -important. Blade tools, tanged arrow points, and burins were still -made, but there were also axes for cutting the trees in the new -forests. Moreover, the tiny microlithic blades, in a variety of -geometric forms, are also found. Thus, a specialized tradition that -possibly began east of the Mediterranean had reached northern Europe. -There was also a ground stone industry; some axes and club-heads were -made by grinding and polishing rather than by chipping. The industries -in bone and antler show a great variety of tools: axes, fish-hooks, -fish spears, handles and hafts for other tools, harpoons, and clubs. -A remarkable industry in wood has been preserved. Paddles, sled -runners, handles for tools, and bark floats for fish-nets have been -found. There are even fish-nets made of plant fibers. Canoes of some -kind were no doubt made. Bone and antler tools were decorated with -simple patterns, and amber was collected. Wooden bows and arrows are -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 -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, -their assemblage shows us again that remarkable ability of men to adapt -themselves to change. They had succeeded in domesticating the dog; he -was still a very wolf-like dog, but his long association with mankind -had now begun. Professor Coon believes that these people were direct -descendants of the men of the glacial age and that they had much the -same appearance. He believes that most of the Ice Age survivors still -extant are living today in the northwestern European area. - - -SOUTH AND CENTRAL EUROPE PERHAPS AS READJUSTED AS THE NORTH - -There is always one trouble with things that come from areas where -preservation is exceptionally good: The very quantity of materials in -such an assemblage tends to make things from other areas look poor -and simple, although they may not have been so originally at all. The -assemblages of the people who lived to the south of the Maglemosian -area may also have been quite large and varied; but, unfortunately, -relatively little of the southern assemblages has lasted. The -water-logged sites of the Maglemosian area preserved a great deal -more. Hence the Maglemosian itself _looks_ quite advanced to us, when -we compare it with the few things that have happened to last in other -areas. If we could go back and wander over the Europe of eight thousand -years ago, we would probably find that the peoples of France, central -Europe, and south central Russia were just as advanced as those of the -north European-Baltic belt. - -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 -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. - - -HOW MUCH REAL CHANGE WAS THERE? - -You can see that no really _basic_ change in the way of life has yet -been described. Childe sees the problem that faced the Europeans of -10,000 to 3000 B.C. as a problem in readaptation to the post-glacial -forest environment. By 6000 B.C. some quite successful solutions of -the problem--like the Maglemosian--had been made. The upsets that came -with the melting of the last ice gradually brought about all sorts of -changes in the tools and food-getting habits, but the people themselves -were still just as much simple hunters, fishers, and food-collectors as -they had been in 25,000 B.C. It could be said that they changed just -enough so that they would not have to change. But there is a bit more -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 -were able to adapt themselves quickly when the next change did come. -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 -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, -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 -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 -the microliths but I do not know of one. Remember, too, that as yet -we can only use the microliths as a marker in Europe and about the -Mediterranean. - - -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 -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 -greater part of Africa and Asia, we do not know exactly what was -happening. Some day, we shall no doubt find out; today we are without -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 -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 -collecting in new ways to fit the post-glacial environment, something -completely new was being made ready in western Asia. - -Unfortunately, we do not have as much understanding of the climate and -environment of the late Ice Age in western Asia as we have for most -of Europe. Probably the weather was never so violent or life quite -so rugged as it was in northern Europe. We know that the microliths -made their appearance in western Asia at least by 10,000 B.C. and -possibly earlier, marking the beginning of the terminal phase of -food-collecting. Then, gradually, we begin to see the build-up towards -the first _basic change_ in human life. - -This change amounted to a revolution just as important as the -Industrial Revolution. In it, men first learned to domesticate -plants and animals. They began _producing_ their food instead of -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 -indeed a revolution. - - -QUESTIONS ABOUT CAUSE - -We do not yet know _how_ and _why_ this great revolution took place. We -are only just beginning to put the questions properly. I suspect the -answers will concern some delicate and subtle interplay between man and -nature. Clearly, both the level of culture and the natural condition of -the environment must have been ready for the great change, before the -change itself could come about. - -It is going to take years of co-operative field work by both -archeologists and the natural scientists who are most helpful to them -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 -environmental changes with the coming of post-glacial times. - -In western Asia, we archeologists can prove that the food-producing -revolution actually took place. We can see _the_ important consequence -of effective domestication of plants and animals in the appearance of -the settled village-farming community. And within the village-farming -community was the seed of civilization. The way in which effective -domestication of plants and animals came about, however, must also be -linked closely with the natural environment. Thus the archeologists -will not solve the _how_ and _why_ questions alone--they will need the -help of interested natural scientists in the field itself. - - -PRECONDITIONS FOR THE REVOLUTION - -Especially at this point in our story, we must remember how culture and -environment go hand in hand. Neither plants nor animals domesticate -themselves; men domesticate them. Furthermore, men usually domesticate -only those plants and animals which are useful. There is a good -question here: What is cultural usefulness? But I shall side-step it to -save time. Men cannot domesticate plants and animals that do not exist -in the environment where the men live. Also, there are certainly some -animals and probably some plants that resist domestication, although -they might be useful. - -This brings me back again to the point that _both_ the level of culture -and the natural condition of the environment--with the proper plants -and animals in it--must have been ready before domestication could -have happened. But this is precondition, not cause. Why did effective -food-production happen first in the Near East? Why did it happen -independently in the New World slightly later? Why also in the Far -East? Why did it happen at all? Why are all human beings not still -living as the Maglemosians did? These are the questions we still have -to face. - - -CULTURAL “RECEPTIVENESS” AND PROMISING ENVIRONMENTS - -Until the archeologists and the natural scientists--botanists, -geologists, zoologists, and general ecologists--have spent many more -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, -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. - -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 -natural conditions most promising? Did this promise include plants and -animals which could be domesticated, or did it only offer new ways of -food-collecting? There is much work to do on this problem, but we are -beginning to get some general hints. - -Before I begin to detail the hints we now have from western Asia, I -want to do two things. First, I shall tell you of an old theory as to -how food-production might have appeared. Second, I will bother you with -some definitions which should help us in our thinking as the story goes -on. - - -AN OLD THEORY AS TO THE CAUSE OF THE REVOLUTION - -The idea that change would result, if the balance between nature -and culture became upset, is of course not a new one. For at least -twenty-five years, there has been a general theory as to _how_ the -food-producing revolution happened. This theory depends directly on the -idea of natural change in the environment. - -The five thousand years following about 10,000 B.C. must have been -very difficult ones, the theory begins. These were the years when -the most marked melting of the last glaciers was going on. While the -glaciers were in place, the climate to the south of them must have been -different from the climate in those areas today. You have no doubt read -that people once lived in regions now covered by the Sahara Desert. -This is true; just when is not entirely clear. The theory is that -during the time of the glaciers, there was a broad belt of rain winds -south of the glaciers. These rain winds would have kept north Africa, -the Nile Valley, and the Middle East green and fertile. But when the -glaciers melted back to the north, the belt of rain winds is supposed -to have moved north too. Then the people living south and east of the -Mediterranean would have found that their water supply was drying up, -that the animals they hunted were dying or moving away, and that the -plant foods they collected were dried up and scarce. - -According to the theory, all this would have been true except in the -valleys of rivers and in oases in the growing deserts. Here, in the -only places where water was left, the men and animals and plants would -have clustered. They would have been forced to live close to one -another, in order to live at all. Presently the men would have seen -that some animals were more useful or made better food than others, -and so they would have begun to protect these animals from their -natural enemies. The men would also have been forced to try new plant -foods--foods which possibly had to be prepared before they could be -eaten. Thus, with trials and errors, but by being forced to live close -to plants and animals, men would have learned to domesticate them. - - -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. -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 -began melting after 12,000 years ago was merely the last sub-phase of -the last great glaciation. There had also been three earlier periods -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 -earlier, then? - -Furthermore, it does not seem to be in the oases and river valleys -that we have our first or only traces of either food-production -or the earliest farming villages. These traces are also in the -hill-flanks of the mountains of western Asia. Our earliest sites of the -village-farmers do not seem to indicate a greatly different climate -from that which the same region now shows. In fact, everything we now -know suggests that the old theory was just too simple an explanation to -have been the true one. The only reason I mention it--beyond correcting -the ideas you may get in the general texts--is that it illustrates the -kind of thinking we shall have to do, even if it is doubtless wrong in -detail. - -We archeologists shall have to depend much more than we ever have on -the natural scientists who can really help us. I can tell you this from -experience. I had the great good fortune to have on my expedition staff -in Iraq in 1954-55, a geologist, a botanist, and a zoologist. Their -studies added whole new bands of color to my spectrum of thinking about -_how_ and _why_ the revolution took place and how the village-farming -community began. But it was only a beginning; as I said earlier, we are -just now learning to ask the proper questions. - - -ABOUT STAGES AND ERAS - -Now come some definitions, so I may describe my material more easily. -Archeologists have always loved to make divisions and subdivisions -within the long range of materials which they have found. They often -disagree violently about which particular assemblage of material -goes into which subdivision, about what the subdivisions should be -named, about what the subdivisions really mean culturally. Some -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. - -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 -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. -91). - -Now, while I think we could make several possible subdivisions of the -food-gathering stage--I call my subdivisions of stages _eras_[5]--I -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 -post-glacial environment in northwestern Europe. We talked about -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 - 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 - _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 - allow it. - -The food-producing revolution ushers in the _food-producing stage_. -This stage began to be replaced by the _industrial stage_ only about -two hundred years ago. Now notice that my stage divisions are in terms -of technology and economics. We must think sharply to be sure that the -subdivisions of the stages, the eras, are in the same terms. This does -not mean that I think technology and economics are the only important -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 -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 -a universal historian. - - -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 -of these sites themselves, and the remarkable growth in human -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 -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” -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 -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 -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 -dog. In this sense, the whole era of food-collecting was becoming ready -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 -place in one of those natural environments of promise, where a variety -of plants and animals, each possible of domestication, was available in -the wild state. Let me spell this out. Really effective food-production -must include a variety of items to make up a reasonably well-rounded -diet. The food-supply so produced must be trustworthy, even though -the food-producing peoples themselves might be happy to supplement -it with fish and wild strawberries, just as we do when such things -are available. So, as we said earlier, part of our problem is that -of finding a region with a natural environment which includes--and -did include, some ten thousand years ago--a variety of possibly -domesticable wild plants and animals. - - -NUCLEAR AREAS - -Now comes the last of my definitions. A region with a natural -environment which included a variety of wild plants and animals, -both possible and ready for domestication, would be a central -or core or _nuclear area_, that is, it would be when and _if_ -food-production took place within it. It is pretty hard for me to -imagine food-production having ever made an independent start outside -such a nuclear area, although there may be some possible nuclear areas -in which food-production never took place (possibly in parts of Africa, -for example). - -We know of several such nuclear areas. In the New World, Middle America -and the Andean highlands make up one or two; it is my understanding -that the evidence is not yet clear as to which. There seems to have -been a nuclear area somewhere in southeastern Asia, in the Malay -peninsula or Burma perhaps, connected with the early cultivation of -taro, breadfruit, the banana and the mango. Possibly the cultivation -of rice and the domestication of the chicken and of zebu cattle and -the water buffalo belong to this southeast Asiatic nuclear area. We -know relatively little about it archeologically, as yet. The nuclear -area which was the scene of the earliest experiment in effective -food-production was in western Asia. Since I know it best, I shall use -it as my example. - - -THE NUCLEAR NEAR EAST - -The nuclear area of western Asia is naturally the one of greatest -interest to people of the western cultural tradition. Our cultural -heritage began within it. The area itself is the region of the hilly -flanks of rain-watered grass-land which build up to the high mountain -ridges of Iran, Iraq, Turkey, Syria, and Palestine. The map on page -125 indicates the region. If you have a good atlas, try to locate the -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 -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 -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 -to Turkey, the Taurus Mountains of southern Turkey, and the Zagros -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 -rain to the foothills of the Taurus and the Zagros. - -The picture I hope you will have from this description is that of an -intermediate hilly-flanks zone lying between two regions of extremes. -The lower Tigris-Euphrates basin land is low and far too dry and hot -for agriculture based on rainfall alone; to the south and southwest, it -merges directly into the great desert of Arabia. The mountains which -lie above the hilly-flanks zone are much too high and rugged to have -encouraged farmers. - - -THE NATURAL ENVIRONMENT OF THE NUCLEAR NEAR EAST - -The more we learn of this hilly-flanks zone that I describe, the -more it seems surely to have been a nuclear area. This is where we -archeologists need, and are beginning to get, the help of natural -scientists. They are coming to the conclusion that the natural -environment of the hilly-flanks zone today is much as it was some eight -to ten thousand years ago. There are still two kinds of wild wheat and -a wild barley, and the wild sheep, goat, and pig. We have discovered -traces of each of these at about nine thousand years ago, also traces -of wild ox, horse, and dog, each of which appears to be the probable -ancestor of the domesticated form. In fact, at about nine thousand -years ago, the two wheats, the barley, and at least the goat, were -already well on the road to domestication. - -The wild wheats give us an interesting clue. They are only available -together with the wild barley within the hilly-flanks zone. While the -wild barley grows in a variety of elevations and beyond the zone, -at least one of the wild wheats does not seem to grow below the hill -country. As things look at the moment, the domestication of both the -wheats together could _only_ have taken place within the hilly-flanks -zone. Barley seems to have first come into cultivation due to its -presence as a weed in already cultivated wheat fields. There is also -a suggestion--there is still much more to learn in the matter--that -the animals which were first domesticated were most at home up in the -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 -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 -stage, that of incipient cultivation and animal domestication. - - -THE ERA OF INCIPIENT CULTIVATION AND ANIMAL DOMESTICATION - -I said above (p. 105) that my era of incipient cultivation and animal -domestication is mainly set up by playing a hunch. Although we cannot -really demonstrate it--and certainly not in the Near East--it would -be very strange for food-collectors not to have known a great deal -about the plants and animals most useful to them. They do seem to have -domesticated the dog. We can easily imagine them remembering to go -back, season after season, to a particular patch of ground where seeds -or acorns or berries grew particularly well. Most human beings, unless -they are extremely hungry, are attracted to baby animals, and many wild -pups or fawns or piglets must have been brought back alive by hunting -parties. - -In this last sense, man has probably always been an incipient -cultivator and domesticator. But I believe that Adams is right in -suggesting that this would be doubly true with the experimenters of -the terminal era of food-collecting. We noticed that they also seem -to have had a tendency to settle down. Now my hunch goes that _when_ -this experimentation and settling down took place within a potential -nuclear area--where a whole constellation of plants and animals -possible of domestication was available--the change was easily made. -Professor Charles A. Reed, our field colleague in zoology, agrees that -year-round settlement with plant domestication probably came before -there were important animal domestications. - - -INCIPIENT ERAS AND NUCLEAR AREAS - -I have put this scheme into a simple chart (p. 111) with the names -of a few of the sites we are going to talk about. You will see that my -hunch means that there are eras of incipient cultivation _only_ within -nuclear areas. In a nuclear area, the terminal era of food-collecting -would probably have been quite short. I do not know for how long a time -the era of incipient cultivation and domestication would have lasted, -but perhaps for several thousand years. Then it passed on into the era -of the primary village-farming community. - -Outside a nuclear area, the terminal era of food-collecting would last -for a long time; in a few out-of-the-way parts of the world, it still -hangs on. It would end in any particular place through contact with -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 -many) who had reached at least the level of the primary village-farming -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. - - -ARCHEOLOGICAL DIFFICULTIES IN SEEING THE INCIPIENT ERA - -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 -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 -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 -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! - -[Illustration: POSSIBLE RELATIONSHIPS OF STAGES AND ERAS IN WESTERN -ASIA AND NORTHEASTERN AFRICA] - -Now since this was an era of incipience, of the birth of new ideas, -and of experimentation, it is very difficult to see its traces -archeologically. New tools having to do with the new ways of getting -and, in fact, producing food would have taken some time to develop. -It need not surprise us too much if we cannot find hoes for planting -and sickles for reaping grain at the very beginning. We might expect -a time of making-do with some of the older tools, or with make-shift -tools, for some of the new jobs. The present-day wild cousin of the -domesticated sheep still lives in the mountains of western Asia. It has -no wool, only a fine down under hair like that of a deer, so it need -not surprise us to find neither the whorls used for spinning nor traces -of woolen cloth. It must have taken some time for a wool-bearing sheep -to develop and also time for the invention of the new tools which go -with weaving. It would have been the same with other kinds of tools for -the new way of life. - -It is difficult even for an experienced comparative zoologist to tell -which are the bones of domesticated animals and which are those of -their wild cousins. This is especially so because the animal bones the -archeologists find are usually fragmentary. Furthermore, we do not have -a sort of library collection of the skeletons of the animals or an -herbarium of the plants of those times, against which the traces which -the archeologists find may be checked. We are only beginning to get -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 -from some slightly later sites. - - -SITES WITH ANTIQUITIES OF THE INCIPIENT ERA - -So far, we know only a very few sites which would suit my notion of the -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 -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 -they are already used to. I would suspect that a great deal of this -making-do went on almost to the end of this era. - - -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 -guess that it probably falls within five hundred years either way of -about 5000 B.C. - -Until recently, the people who produced the Natufian assemblage were -thought to have been only cave dwellers, but now at least three open -air Natufian sites have been briefly described. In their best-known -dwelling place, on Mount Carmel, the Natufian folk lived in the open -mouth of a large rock-shelter and on the terrace in front of it. On the -terrace, they had set at least two short curving lines of stones; but -these were hardly architecture; they seem more like benches or perhaps -the low walls of open pens. There were also one or two small clusters -of stones laid like paving, and a ring of stones around a hearth or -fireplace. One very round and regular basin-shaped depression had been -cut into the rocky floor of the terrace, and there were other less -regular basin-like depressions. In the newly reported open air sites, -there seem to have been huts with rounded corners. - -Most of the finds in the Natufian layer of the Mount Carmel cave were -flints. About 80 per cent of these flint tools were microliths made -by the regular working of tiny blades into various tools, some having -geometric forms. The larger flint tools included backed blades, burins, -scrapers, a few arrow points, some larger hacking or picking tools, and -one special type. This last was the sickle blade. - -We know a sickle blade of flint when we see one, because of a strange -polish or sheen which seems to develop on the cutting edge when the -blade has been used to cut grasses or grain, or--perhaps--reeds. In -the Natufian, we have even found the straight bone handles in which a -number of flint sickle blades were set in a line. - -There was a small industry in ground or pecked stone (that is, abraded -not chipped) in the Natufian. This included some pestle and mortar -fragments. The mortars are said to have a deep and narrow hole, -and some of the pestles show traces of red ochre. We are not sure -that these mortars and pestles were also used for grinding food. In -addition, there were one or two bits of carving in stone. - - -NATUFIAN ANTIQUITIES IN OTHER MATERIALS; BURIALS AND PEOPLE - -The Natufian industry in bone was quite rich. It included, beside the -sickle hafts mentioned above, points and harpoons, straight and curved -types of fish-hooks, awls, pins and needles, and a variety of beads and -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 -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, -and several of the best collections of beads come from the burials. On -two of the skulls there were traces of elaborate head-dresses of shell -beads. - -[Illustration: SKETCH OF NATUFIAN ASSEMBLAGE - - MICROLITHS - ARCHITECTURE? - BURIAL - CHIPPED STONE - GROUND STONE - BONE] - -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 -the same climate that it has today. Some of the animal bones were those -of large or dangerous beasts: the hyena, the bear, the wild boar, -and the leopard. But the Natufian people may have had the help of a -large domesticated dog. If our guess at a date for the Natufian is -right (about 7750 B.C.), this is an earlier dog than was that in the -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.” - - -THE KARIM SHAHIR ASSEMBLAGE - -Karim Shahir differs from the Natufian sites in that it shows traces -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 -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. - -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 -on it spread out over about three acres of area. In spots, the single -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. - -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 -were microlithic bladelets and geometric forms. The flint sickle blade -was almost non-existent, being far scarcer than in the Natufian. The -people of Karim Shahir did a modest amount of work in the grinding of -stone; there were milling stone fragments of both the mortar and the -quern type, and stone hoes or axes with polished bits. Beads, pendants, -rings, and bracelets were made of finer quality stone. We found a few -simple points and needles of bone, and even two rather formless unbaked -clay figurines which seemed to be of animal form. - -[Illustration: SKETCH OF KARIM SHAHIR ASSEMBLAGE - - CHIPPED STONE - GROUND STONE - UNBAKED CLAY - SHELL - BONE - “ARCHITECTURE”] - -Karim Shahir did not yield direct evidence of the kind of vegetable -food its people ate. The animal bones showed a considerable -increase in the proportion of the bones of the species capable of -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 -at the moment that will tell us from the bones alone. - - -WERE THE NATUFIAN AND KARIM SHAHIR PEOPLES FOOD-PRODUCERS? - -It is clear that a great part of the food of the Natufian people -must have been hunted or collected. Shells of land, fresh-water, and -sea animals occur in their cave layers. The same is true as regards -Karim Shahir, save for sea shells. But on the other hand, we have -the sickles, the milling stones, the possible Natufian dog, and the -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 -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 -food-production--that the hints we seek are there. But in the sense -that the peoples of the era of the primary village-farming community, -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 -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. - - -SUMMARY - -I am sorry that this chapter has had to be so much more about ideas -than about the archeological traces of prehistoric men themselves. -But the antiquities of the incipient era of cultivation and animal -domestication will not be spectacular, even when we do have them -excavated in quantity. Few museums will be interested in these -antiquities for exhibition purposes. The charred bits or impressions -of plants, the fragments of animal bone and shell, and the varied -clues to climate and environment will be as important as the artifacts -themselves. It will be the ideas to which these traces lead us that -will be important. I am sure that this unspectacular material--when we -have much more of it, and learn how to understand what it says--will -lead us to how and why answers about the first great change in human -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 -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 -in existence. Without effective food-production and the settled -village-farming communities, civilization never could have followed. -How effective food-production came into being by the end of the -incipient era, is, I believe, one of the most fascinating questions any -archeologist could face. - -It now seems probable--from possibly two of the Palestinian sites with -varieties of the Natufian (Jericho and Nahal Oren)--that there were -one or more local Palestinian developments out of the Natufian into -later times. In the same way, what followed after the Karim Shahir type -of assemblage in northeastern Iraq was in some ways a reflection of -beginnings made at Karim Shahir and Zawi Chemi. - - - - -THE First Revolution - -[Illustration] - - -As the incipient era of cultivation and animal domestication passed -onward into the era of the primary village-farming community, the first -basic change in human economy was fully achieved. In southwestern Asia, -this seems to have taken place about nine thousand years ago. I am -going to restrict my description to this earliest Near Eastern case--I -do not know enough about the later comparable experiments in the Far -East and in the New World. Let us first, once again, think of the -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 -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 -moved. Not many people can be fed in this way in one area, and small -children and old folks are a burden. There is not enough food to store, -and it is not the kind that can be stored for long. - -Do you see how this all fits into a picture? Small groups of people -living now in this cave, now in that--or out in the open--as they moved -after the animals they hunted; no permanent villages, a few half-buried -huts at best; no breakable utensils; no pottery; no signs of anything -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. -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. - - -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,” -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 -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 -already of a fair size, and it was growing, too. Everybody had more to -eat; they were presumably all stronger, and there were more children. -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 -empty-handed. - -There was more time to do different things, too. They began to modify -nature. They made pottery out of raw clay, and textiles out of hair -or fiber. People who became good at pottery-making traded their pots -for food and spent all of their time on pottery alone. Other people -were learning to weave cloth or to make new tools. There were already -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 -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 -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”? - -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 -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 -been established, however, things did begin to move very fast. By -six thousand years ago, the descendants of the first villagers had -developed irrigation and plow agriculture in the relatively rainless -Mesopotamian alluvium and were living in towns with temples. Relative -to the half million years of food-gathering which lay behind, this had -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 -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 -to the intelligent co-operation given foreign excavators by the -Iraq Directorate General of Antiquities, our understanding of the -sequence in Iraq is growing more complete. I shall use Iraq as my main -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.” - -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 -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 -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 -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 -on such a dig holds--halcyon days for the body and acute pleasurable -stimulation for the mind. Old things coming newly out of the good dirt, -and the pieces of the human puzzle fitting into place! I think I am -an honest man; I cannot tell you that I am sorry the job is not yet -finished and that there are still gaps in this part of the Near Eastern -archeological sequence. - - -EARLIEST SITES OF THE VILLAGE FARMERS - -So far, the Karim Shahir type of assemblage, which we looked at in the -last chapter, is the earliest material available in what I take to -be the nuclear area. We do not believe that Karim Shahir was a village -site proper: it looks more like the traces of a temporary encampment. -Two caves, called Belt and Hotu, which are outside the nuclear area -and down on the foreshore of the Caspian Sea, have been excavated -by Professor Coon. These probably belong in the later extension of -the terminal era of food-gathering; in their upper layers are traits -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, -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 -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 -sites and assemblages which lie along the hilly flanks of the crescent -and just below it. I am going to describe and illustrate some of these -for you. - -Since not very much archeological excavation has yet been done on sites -of this range of time, I shall have to mention the names of certain -single sites which now alone stand for an assemblage. This does not -mean that I think the individual sites I mention were unique. In the -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 -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 -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). - - -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 -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 -uncertain about this two thousand years--I do not think it can have -been so long. - -This suggests that we still have much work to do in Iraq. You can -imagine how earnestly we await the return of political stability in the -Republic of Iraq. - - -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 -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 -east of the modern oil town of Kirkuk. The Iraq Directorate General of -Antiquities suggested that we look at it in 1948, and we have had three -seasons of digging on it since. - -The people of Jarmo grew the barley plant and two different kinds of -wheat. They made flint sickles with which to reap their grain, mortars -or querns on which to crack it, ovens in which it might be parched, and -stone bowls out of which they might eat their porridge. We are sure -that they had the domesticated goat, but Professor Reed (the staff -zoologist) is not convinced that the bones of the other potentially -domesticable animals of Jarmo--sheep, cattle, pig, horse, dog--show -sure signs of domestication. We had first thought that all of these -animals were domesticated ones, but Reed feels he must find out much -more before he can be sure. As well as their grain and the meat from -their animals, the people of Jarmo consumed great quantities of land -snails. Botanically, the Jarmo wheat stands about half way between -fully bred wheat and the wild forms. - - -ARCHITECTURE: HALL-MARK OF THE VILLAGE - -The sure sign of the village proper is in its traces of architectural -permanence. The houses of Jarmo were only the size of a small cottage -by our standards, but each was provided with several rectangular rooms. -The walls of the houses were made of puddled mud, often set on crude -foundations of stone. (The puddled mud wall, which the Arabs call -_touf_, is built by laying a three to six inch course of soft mud, -letting this sun-dry for a day or two, then adding the next course, -etc.) The village probably looked much like the simple Kurdish farming -village of today, with its mud-walled houses and low mud-on-brush -roofs. I doubt that the Jarmo village had more than twenty houses at -any one moment of its existence. Today, an average of about seven -people live in a comparable Kurdish house; probably the population of -Jarmo was about 150 people. - -[Illustration: SKETCH OF JARMO ASSEMBLAGE - - CHIPPED STONE - UNBAKED CLAY - GROUND STONE - POTTERY _UPPER THIRD OF SITE ONLY._ - REED MATTING - BONE - ARCHITECTURE] - -It is interesting that portable pottery does not appear until the -last third of the life of the Jarmo village. Throughout the duration -of the village, however, its people had experimented with the plastic -qualities of clay. They modeled little figurines of animals and of -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 -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 -art. - -On the other hand, the old tradition of making flint blades and -microlithic tools was still very strong at Jarmo. The sickle-blade was -made in quantities, but so also were many of the much older tool types. -Strangely enough, it is within this age-old category of chipped stone -tools that we see one of the clearest pointers to a newer age. Many of -the Jarmo chipped stone tools--microliths--were made of obsidian, a -black volcanic natural glass. The obsidian beds nearest to Jarmo are -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” -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 -achieved almost nine thousand years ago. - - -HASSUNA, IN UPPER MESOPOTAMIAN IRAQ - -We are not sure just how soon after Jarmo the next assemblage of Iraqi -material is to be placed. I do not think the time was long, and there -are a few hints that detailed habits in the making of pottery and -ground stone tools were actually continued from Jarmo times into the -time of the next full assemblage. This is called after a site named -Hassuna, a few miles to the south and west of modern Mosul. We also -have Hassunan type materials from several other sites in the same -general region. It is probably too soon to make generalizations about -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 -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 -different ways of making and decorating pottery vessels. One style of -pottery painting, called the Samarran style, is an extremely handsome -one and must have required a great deal of concentration and excellence -of draftsmanship. On the other hand, the old habits for the preparation -of good chipped stone tools--still apparent at Jarmo--seem to have -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 -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. - - -OTHER EARLY VILLAGE SITES IN THE NUCLEAR AREA - -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, -roughly comparable to that of Hassuna, for example, must reflect a -culture which lived at just about the same time as that of Hassuna. We -do this guessing on the basis of the general similarity and degree of -complexity of the Sears Roebuck catalogues of the particular assemblage -and that of Hassuna. We suppose that for sites near at hand and of a -comparable cultural level, as indicated by their generally similar -assemblages, the dating must be about the same. We may also know that -in a general stratigraphic sense, the sites in question may both appear -at the bottom of the ascending village sequence in their respective -areas. Without a number of consistent radioactive carbon dates, we -cannot be precise about priorities. - -[Illustration: SKETCH OF HASSUNA ASSEMBLAGE - - POTTERY - POTTERY OBJECTS - CHIPPED STONE - BONE - GROUND STONE - ARCHITECTURE - REED MATTING - BURIAL] - -The ancient mound at Jericho, in the Dead Sea valley in Palestine, -yields some very interesting material. Its catalogue somewhat resembles -that of Jarmo, especially in the sense that there is a fair depth -of deposit without portable pottery vessels. On the other hand, the -architecture of Jericho is surprisingly complex, with traces of massive -stone fortification walls and the general use of formed sun-dried -mud brick. Jericho lies in a somewhat strange and tropically lush -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 -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. - -The mound at Jericho (Tell es-Sultan) contains a remarkably -fine sequence, which perhaps does not have the gap we noted in -Iraqi-Kurdistan between the Karim Shahir group and Jarmo. While I am -not sure that the Jericho sequence will prove valid for those parts -of Palestine outside the special Dead Sea environmental niche, the -sequence does appear to proceed from the local variety of Natufian into -that of a very well settled community. So far, we have little direct -evidence for the food-production basis upon which the Jericho people -subsisted. - -There is an early village assemblage with strong characteristics of its -own in the land bordering the northeast corner of the Mediterranean -Sea, where Syria and the Cilician province of Turkey join. This early -Syro-Cilician assemblage must represent a general cultural pattern -which was at least in part contemporary with that of the Hassuna -assemblage. These materials from the bases of the mounds at Mersin, and -from Judaidah in the Amouq plain, as well as from a few other sites, -represent the remains of true villages. The walls of their houses were -built of puddled mud, but some of the house foundations were of stone. -Several different kinds of pottery were made by the people of these -villages. None of it resembles the pottery from Hassuna or from the -upper levels of Jarmo or Jericho. The Syro-Cilician people had not -lost their touch at working flint. An important southern variation of -the Syro-Cilician assemblage has been cleared recently at Byblos, a -port town famous in later Phoenician times. There are three radiocarbon -determinations which suggest that the time range for these developments -was in the sixth or early fifth millennium B.C. - -It would be fascinating to search for traces of even earlier -village-farming communities and for the remains of the incipient -cultivation era, in the Syro-Cilician region. - - -THE IRANIAN PLATEAU AND THE NILE VALLEY - -The map on page 125 shows some sites which lie either outside or in -an extension of the hilly-flanks zone proper. From the base of the -great mound at Sialk on the Iranian plateau came an assemblage of -early village material, generally similar, in the kinds of things it -contained, to the catalogues of Hassuna and Judaidah. The details of -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 -appearance. - -There is at least one very early Iranian site on the inward slopes -of the hilly-flanks zone. It is the earlier of two mounds at a place -called Bakun, in southwestern Iran; the results of the excavations -there are not yet published and we only know of its coarse and -primitive pottery. I only mention Bakun because it helps us to plot the -extent of the hilly-flanks zone villages on the map. - -The Nile Valley lies beyond the peculiar environmental zone of the -hilly flanks of the crescent, and it is probable that the earliest -village-farming communities in Egypt were established by a few people -who wandered into the Nile delta area from the nuclear area. The -assemblage which is most closely comparable to the catalogue of Hassuna -or Judaidah, for example, is that from little settlements along the -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 -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 -generalized cultural pattern of village-farming community life to -spread from the nuclear area down into Egypt, but as yet we have no way -of testing these matters. - -In this same vein, we have two radioactive carbon dates for an -assemblage from sites near Khartoum in the Sudan, best represented by -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. - -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” -may be mainly an artificial selection of poor examples of objects which -belong in the following range of time. - - -SPREAD IN TIME AND SPACE - -There are now two things we can do; in fact, we have already begun to -do them. We can watch the spread of the new way of life upward through -time in the nuclear area. We can also see how the new way of life -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 -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 -into Africa, to reach Khartoum in the Sudan some thirty-five hundred -years later than we had seen it at Jarmo or Jericho. We caught glimpses -of it in the Fayum and perhaps at Tasa along the way. - -For the remainder of this chapter, I shall try to suggest briefly for -you the directions taken by the spread of the new way of life from the -nuclear area in the Near East. First, let me make clear again that -I _do not_ believe that the village-farming community way of life -was invented only once and in the Near East. It seems to me that the -evidence is very clear that a separate experiment arose in the New -World. For China, the question of independence or borrowing--in the -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 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 -China. However, the appearance of the village-farming community in the -northwest of India, at least, seems to have depended on the earlier -development in the Near East. It is also probable that ideas of the new -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 -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 -just as complicated. - -This much is clear for Europe, as far as the spread of the -village-community way of life is concerned. The general idea and much -of the know-how and the basic tools of food-production moved from the -Near East to Europe. So did the plants and animals which had been -domesticated; they were not naturally at home in Europe, as they were -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 -the groups that moved. - -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 -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 -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.” - - -THE WAYS TO EUROPE - -Suppose we want to follow the traces of those earliest village-farmers -who did travel from western Asia into Europe. Let us start from -Syro-Cilicia, that part of the hilly-flanks zone proper which lies in -the very northeastern corner of the Mediterranean. Three ways would be -open to us (of course we could not be worried about permission from the -Soviet authorities!). We would go north, or north and slightly east, -across Anatolian Turkey, and skirt along either shore of the Black Sea -or even to the east of the Caucasus Mountains along the Caspian Sea, -to reach the plains of Ukrainian Russia. From here, we could march -across eastern Europe to the Baltic and Scandinavia, or even hook back -southwestward to Atlantic Europe. - -Our second way from Syro-Cilicia would also lie over Anatolia, to the -northwest, where we would have to swim or raft ourselves over the -Dardanelles or the Bosphorus to the European shore. Then we would bear -left toward Greece, but some of us might turn right again in Macedonia, -going up the valley of the Vardar River to its divide and on down -the valley of the Morava beyond, to reach the Danube near Belgrade -in Jugoslavia. Here we would turn left, following the great river -valley of the Danube up into central Europe. We would have a number of -tributary valleys to explore, or we could cross the divide and go down -the valley of the Rhine to the North Sea. - -Our third way from Syro-Cilicia would be by sea. We would coast along -southern Anatolia and visit Cyprus, Crete, and the Aegean islands on -our way to Greece, where, in the north, we might meet some of those who -had taken the second route. From Greece, we would sail on to Italy and -the western isles, to reach southern France and the coasts of Spain. -Eventually a few of us would sail up the Atlantic coast of Europe, to -reach western Britain and even Ireland. - -[Illustration: PROBABLE ROUTES AND TIMING IN THE SPREAD OF THE -VILLAGE-FARMING COMMUNITY WAY OF LIFE FROM THE NEAR EAST TO EUROPE] - -Of course none of us could ever take these journeys as the first -farmers took them, since the whole course of each journey must have -lasted many lifetimes. The date given to the assemblage called Windmill -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 -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.” - -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 -(p. 139). - - -THE EARLIEST FARMERS OF ENGLAND - -To describe the later prehistory of all Europe for you would take -another book and a much larger one than this is. Therefore, I have -decided to give you only a few impressions of the later prehistory of -Britain. Of course the British Isles lie at the other end of Europe -from our base-line in western Asia. Also, they received influences -along at least two of the three ways in which the new way of life -moved into Europe. We will look at more of their late prehistory in a -following chapter: here, I shall speak only of the first farmers. - -The assemblage called Windmill Hill, which appears in the south of -England, exhibits three different kinds of structures, evidence of -grain-growing and of stock-breeding, and some distinctive types of -pottery and stone implements. The most remarkable type of structure -is the earthwork enclosures which seem to have served as seasonal -cattle corrals. These enclosures were roughly circular, reached over -a thousand feet in diameter, and sometimes included two or three -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 -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 -convinced, however, that important elements of Windmill Hill stem from -northern Germany and Denmark--products of the first way into Europe -from the east. - -The archeological traces of a second early culture are to be found -in the west of England, western and northern Scotland, and most of -Ireland. The bearers of this culture had come up the Atlantic coast -by sea from southern France and Spain. The evidence they have left us -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 -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 -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. - -The cultures of the people who produced the Windmill Hill assemblage -and of those who made the megalithic tombs flourished, at least in -part, at the same time. Although the distributions of the two different -types of archeological traces are in quite different parts of the -country, there is Windmill Hill pottery in some of the megalithic -tombs. But the tombs also contain pottery which seems to have arrived -with the tomb builders themselves. - -The third early British group of antiquities of this general time -(following 2500 B.C.) comes from sites in southern and eastern England. -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 -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 -finds extends into eastern Britain, where the other cultures have left -no trace. The Peterborough people had villages with semi-subterranean -huts, and the bones of oxen, pigs, and sheep have been found in a few -of these. On the whole, however, hunting and fishing seem to have been -their vital occupations. They also established trade routes especially -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 -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 -bedsteads, the shelves, the chests for clothes and oddments--all built -of thin stone-slabs--may still be seen in place. But the Skara Brae -people lived entirely by sheep- and cattle-breeding, and by catching -shellfish. Neither grain nor the instruments of agriculture appeared at -Skara Brae. - - -THE EUROPEAN ACHIEVEMENT - -The above is only a very brief description of what went on in Britain -with the arrival of the first farmers. There are many interesting -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 -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” -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_ -superior. The ideas and things and the people had to prove themselves -in their adaptation to each new environment. Once this was done another -link to the chain would be added, and then the forest-dwellers and -other indigenous folk of Europe along the way might accept the new -ideas and things. It is quite reasonable to expect that there must have -been much mixture of the migrants and the indigenes along the way; the -Peterborough and Skara Brae assemblages we mentioned above would seem -to be clear traces of such fused cultures. Sometimes, especially if the -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 -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 -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. - -What we know of the ways of men suggests to us that when the details -of the later prehistory of further Asia and Africa are learned, their -stories will be just as exciting. - - - - -THE Conquest of Civilization - -[Illustration] - - -Now we must return to the Near East again. We are coming to the point -where history is about to begin. I am going to stick pretty close -to Iraq and Egypt in this chapter. These countries will perhaps be -the most interesting to most of us, for the foundations of western -civilization were laid in the river lands of the Tigris and Euphrates -and of the Nile. I shall probably stick closest of all to Iraq, because -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 -traditions we live by today are based, ultimately, on those ancient -beginnings in food-production and civilization in the Near East. - - -WHAT “CIVILIZATION” MEANS - -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 -people have set up. It means formal laws--rules of conduct--which the -government (if not the people) believes are necessary. It probably -means that there are formalized projects--roads, harbors, irrigation -canals, and the like--and also some sort of army or police force -to protect them. It means quite new and different art forms. It -also usually means there is writing. (The people of the Andes--the -Incas--had everything which goes to make up a civilization but formal -writing. I can see no reason to say they were not civilized.) Finally, -as the late Professor Redfield reminded us, civilization seems to bring -with it the dawn of a new kind of moral order. - -In different civilizations, there may be important differences in the -way such things as the above are managed. In early civilizations, it is -usual to find religion very closely tied in with government, law, and -so forth. The king may also be a high priest, or he may even be thought -of as a god. The laws are usually thought to have been given to the -people by the gods. The temples are protected just as carefully as the -other projects. - - -CIVILIZATION IMPOSSIBLE WITHOUT FOOD-PRODUCTION - -Civilizations have to be made up of many people. Some of the people -live in the country; some live in very large towns or cities. Classes -of society have begun. There are officials and government people; there -are priests or religious officials; there are merchants and traders; -there are craftsmen, metal-workers, potters, builders, and so on; there -are also farmers, and these are the people who produce the food for the -whole population. It must be obvious that civilization cannot exist -without food-production and that food-production must also be at a -pretty efficient level of village-farming before civilization can even -begin. - -But people can be food-producing without being civilized. In many -parts of the world this is still the case. When the white men first -came to America, the Indians in most parts of this hemisphere were -food-producers. They grew corn, potatoes, tomatoes, squash, and many -other things the white men had never eaten before. But only the Aztecs -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? - -Once you have food-production, even at the well-advanced level of -the village-farming community, what else has to happen before you -get civilization? Many men have asked this question and have failed -to give a full and satisfactory answer. There is probably no _one_ -answer. I shall give you my own idea about how civilization _may_ have -come about in the Near East alone. Remember, it is only a guess--a -putting together of hunches from incomplete evidence. It is _not_ meant -to explain how civilization began in any of the other areas--China, -southeast Asia, the Americas--where other early experiments in -civilization went on. The details in those areas are quite different. -Whether certain general principles hold, for the appearance of any -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 -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). - -Now notice that this hilly-flanks zone does not include southern -Mesopotamia, the alluvial land of the lower Tigris and Euphrates in -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 -river lands, however, that the immediate beginnings of civilization -were made. - -We know that by about 3200 B.C. the Early Dynastic period had begun -in southern Mesopotamia. The beginnings of writing go back several -hundred years earlier, but we can safely say that civilization had -begun in Mesopotamia by 3200 B.C. In Egypt, the beginning of the First -Dynasty is slightly later, at about 3100 B.C., and writing probably -did not appear much earlier. There is no question but that history and -civilization were well under way in both Mesopotamia and Egypt by 3000 -B.C.--about five thousand years ago. - - -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 -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. - -The land in the hilly flanks is of a sort which people can farm without -too much trouble. There is a fairly fertile coastal strip in Palestine -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 -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 -domestic animals had been successful, a pleasant living could be -made--and without too much trouble. - -I should add here again, that our evidence points increasingly to a -climate for those times which is very little different from that for -the area today. Now look at Egypt and southern Mesopotamia. Both are -lands without rain, for all intents and purposes. Both are lands with -rivers that have laid down very fertile soil--soil perhaps superior to -that in the hilly flanks. But in both lands, the rivers are of no great -aid without some control. - -The Nile floods its banks once a year, in late September or early -October. It not only soaks the narrow fertile strip of land on either -side; it lays down a fresh layer of new soil each year. Beyond the -fertile strip on either side rise great cliffs, and behind them is the -desert. In its natural, uncontrolled state, the yearly flood of the -Nile must have caused short-lived swamps that were full of crocodiles. -After a short time, the flood level would have dropped, the water and -the crocodiles would have run back into the river, and the swamp plants -would have become parched and dry. - -The Tigris and the Euphrates of Mesopotamia are less likely to flood -regularly than the Nile. The Tigris has a shorter and straighter course -than the Euphrates; it is also the more violent river. Its banks are -high, and when the snows melt and flow into all of its tributary rivers -it is swift and dangerous. The Euphrates has a much longer and more -curving course and few important tributaries. Its banks are lower and -it is less likely to flood dangerously. The land on either side and -between the two rivers is very fertile, south of the modern city of -Baghdad. Unlike the Nile Valley, neither the Tigris nor the Euphrates -is flanked by cliffs. The land on either side of the rivers stretches -out for miles and is not much rougher than a poor tennis court. - - -THE RIVERS MUST BE CONTROLLED - -The real trick in both Egypt and Mesopotamia is to make the rivers work -for you. In Egypt, this is a matter of building dikes and reservoirs -that will catch and hold the Nile flood. In this way, the water is held -and allowed to run off over the fields as it is needed. In Mesopotamia, -it is a matter of taking advantage of natural river channels and branch -channels, and of leading ditches from these onto the fields. - -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 -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. -In the second place, the patterns for such projects seem to have been -pretty well set by historic times. - - -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 -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 -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. - -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 -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. - - -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 -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 -way to live until they had set up the rules? I think that small groups -must have moved down along the mud-flats of the river banks quite -early, making use of naturally favorable spots, and that the rules grew -out of such cases. It would have been like the hand-in-hand growth of -automobiles and paved highways in the United States. - -Once the rules and the know-how did get going, there must have been a -constant interplay of the two. Thus, the more the crops yielded, the -richer and better-fed the people would have been, and the more the -population would have grown. As the population grew, more land would -have needed to be flooded or irrigated, and more complex systems of -dikes, reservoirs, canals, and ditches would have been built. The more -complex the system, the more necessity for work on new projects and for -the control of their use.... And so on.... - -What I have just put down for you is a guess at the manner of growth of -some of the formalized systems that go to make up a civilized society. -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. - -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 -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 -they worked. But without these other aspects of culture, I do not -think that urbanization and civilization itself could have come into -being. - - -THE ARCHEOLOGICAL SEQUENCE TO CIVILIZATION IN IRAQ - -We last spoke of the archeological materials of Iraq on page 130, -where I described the village-farming community of Hassunan type. The -Hassunan type villages appear in the hilly-flanks zone and in the -rolling land adjacent to the Tigris in northern Iraq. It is probable -that even before the Hassuna pattern of culture lived its course, a -new assemblage had been established in northern Iraq and Syria. This -assemblage is called Halaf, after a site high on a tributary of the -Euphrates, on the Syro-Turkish border. - -[Illustration: SKETCH OF SELECTED ITEMS OF HALAFIAN ASSEMBLAGE - - BEADS AND PENDANTS - POTTERY MOTIFS - POTTERY] - -The Halafian assemblage is incompletely known. The culture it -represents included a remarkably handsome painted pottery. -Archeologists have tended to be so fascinated with this pottery that -they have bothered little with the rest of the Halafian assemblage. We -do know that strange stone-founded houses, with plans like those of the -popular notion of an Eskimo igloo, were built. Like the pottery of the -Samarran style, which appears as part of the Hassunan assemblage (see -p. 131), the Halafian painted pottery implies great concentration and -excellence of draftsmanship on the part of the people who painted it. - -We must mention two very interesting sites adjacent to the mud-flats of -the rivers, half way down from northern Iraq to the classic alluvial -Mesopotamian area. One is Baghouz on the Euphrates; the other is -Samarra on the Tigris (see map, p. 125). Both these sites yield the -handsome painted pottery of the style called Samarran: in fact it -is Samarra which gives its name to the pottery. Neither Baghouz nor -Samarra have completely Hassunan types of assemblages, and at Samarra -there are a few pots of proper Halafian style. I suppose that Samarra -and Baghouz give us glimpses of those early farmers who had begun to -finger their way down the mud-flats of the river banks toward the -fertile but yet untilled southland. - - -CLASSIC SOUTHERN MESOPOTAMIA FIRST OCCUPIED - -Our next step is into the southland proper. Here, deep in the core of -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” -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 -do not have much evidence to go on. The idea does at least fit with my -interpretation of the meaning of Baghouz and Samarra as way-points on -the mud-flats of the rivers half way down from the north. - -My colleague, Robert Adams, believes that there were certainly -riverine-adapted food-collectors living in lower Mesopotamia. The -presence of such would explain why the Eridu assemblage is not simply -the sum of the Halafian and Samarran assemblages. But the domesticated -plants and animals and the basic ways of food-production must have -come from the hilly-flanks country in the north. - -Above the basal Eridu levels, and at a number of other sites in the -south, comes a full-fledged assemblage called Ubaid. Incidentally, -there is an aspect of the Ubaidian assemblage in the north as well. It -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 -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 -carefully drafted styles of Samarra and Halaf. The Ubaidian people made -other items of baked clay: sickles and axes of very hard-baked clay -are found. The northern Ubaidian sites have yielded tools of copper, -but metal tools of unquestionable Ubaidian find-spots are not yet -available from the south. Clay figurines of human beings with monstrous -turtle-like faces are another item in the southern Ubaidian assemblage. - -[Illustration: SKETCH OF SELECTED ITEMS OF 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 -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 -the peculiar riverine environment of classic southern Mesopotamia. For -example, hard-baked clay axes will chop bundles of reeds very well, or -help a mason dress his unbaked mud bricks, and there were only a few -soft and pithy species of trees available. The Ubaidian levels of Eridu -yield quantities of date pits; that excellent and characteristically -Iraqi fruit was already in use. The excavators also found the clay -model of a ship, with the stepping-point for a mast, so that Sinbad the -Sailor must have had his antecedents as early as the time of Ubaid. -The bones of fish, which must have flourished in the larger canals as -well as in the rivers, are common in the Ubaidian levels and thereafter. - - -THE UBAIDIAN ACHIEVEMENT - -On present evidence, my tendency is to see the Ubaidian assemblage -in southern Iraq as the trace of a new era. I wish there were more -evidence, but what we have suggests this to me. The culture of southern -Ubaid soon became a culture of towns--of centrally located towns with -some rural villages about them. The town had a temple and there must -have been priests. These priests probably had political and economic -functions as well as religious ones, if the somewhat later history of -Mesopotamia may suggest a pattern for us. Presently the temple and its -priesthood were possibly the focus of the market; the temple received -its due, and may already have had its own lands and herds and flocks. -The people of the town, undoubtedly at least in consultation with the -temple administration, planned and maintained the simple irrigation -ditches. As the system flourished, the community of rural farmers would -have produced more than sufficient food. The tendency for specialized -crafts to develop--tentative at best at the cultural level of the -earlier village-farming community era--would now have been achieved, -and probably many other specialists in temple administration, water -control, architecture, and trade would also have appeared, as the -surplus food-supply was assured. - -Southern Mesopotamia is not a land rich in natural resources other -than its fertile soil. Stone, good wood for construction, metal, and -innumerable other things would have had to be imported. Grain and -dates--although both are bulky and difficult to transport--and wool and -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 -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 -then we may watch with interest where the Ubaid pottery-painting -tradition spread. We have already mentioned that there is a substantial -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 -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 -Syria, and it may be faintly reflected in the painted style of a -site called Ghassul, on the east bank of the Jordan in the Dead Sea -Valley. Over this vast area--certainly in all of the great basin of -the Tigris-Euphrates drainage system and its natural extensions--I -believe we may lay our fingers on the traces of a peculiar way of -decorating pottery, which we call Ubaidian. This cursive and even -slap-dash decoration, it appears to me, was part of a new cultural -tradition which arose from the adjustments which immigrant northern -farmers first made to the new and challenging environment of southern -Mesopotamia. But exciting as the idea of the spread of influences of -the Ubaid tradition in space may be, I believe you will agree that the -consequences of the growth of that tradition in southern Mesopotamia -itself, as time passed, are even more important. - - -THE WARKA PHASE IN THE SOUTH - -So far, there are only two radiocarbon determinations for the Ubaidian -assemblage, one from Tepe Gawra in the north and one from Warka in the -south. My hunch would be to use the dates 4500 to 3750 B.C., with a -plus or more probably a minus factor of about two hundred years for -each, as the time duration of the Ubaidian assemblage in southern -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 -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 -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 -deal more to say about the Warkan assemblage, but someone will first -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 -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 -the somewhat shaky principle that writing, however early, means -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 -a consistent development from beginning to end. - -I shall, in fact, leave much of the description and the historic -implications of the Proto-Literate assemblage to the conventional -historians. Professor T. J. Jacobsen, reaching backward from the -legends he finds in the cuneiform writings of slightly later times, can -in fact tell you a more complete story of Proto-Literate culture than -I can. It should be enough here if I sum up briefly what the excavated -archeological evidence shows. - -We have yet to dig a Proto-Literate site in its entirety, but the -indications are that the sites cover areas the size of small cities. -In architecture, we know of large and monumental temple structures, -which were built on elaborate high terraces. The plans and decoration -of these temples follow the pattern set in the Ubaid phase: the chief -difference is one of size. The German excavators at the site of Warka -reckoned that the construction of only one of the Proto-Literate temple -complexes there must have taken 1,500 men, each working a ten-hour day, -five years to build. - - -ART AND WRITING - -If the architecture, even in its monumental forms, can be seen to -stem from Ubaidian developments, this is not so with our other -evidence of Proto-Literate artistic expression. In relief and applied -sculpture, in sculpture in the round, and on the engraved cylinder -seals--all of which now make their appearance--several completely -new artistic principles are apparent. These include the composition -of subject-matter in groups, commemorative scenes, and especially -the ability and apparent desire to render the human form and face. -Excellent as the animals of the Franco-Cantabrian art may have been -(see p. 85), and however handsome were the carefully drafted -geometric designs and conventionalized figures on the pottery of the -early farmers, there seems to have been, up to this time, a mental -block about the drawing of the human figure and especially the human -face. We do not yet know what caused this self-consciousness about -picturing themselves which seems characteristic of men before the -appearance of civilization. We do know that with civilization, the -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, -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 -Jacobsen sees writing as a means for overcoming space, time, and the -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.” - -[Illustration: RELIEF ON A PROTO-LITERATE STONE VASE, WARKA - -Unrolled drawing, with restoration suggested by figures from -contemporary cylinder seals] - -While the new principles in art and the idea of writing are not -foreshadowed in the Ubaid phase, or in what little we know of the -Warkan, I do not think we need to look outside southern Mesopotamia -for their beginnings. We do know something of the adjacent areas, -too, and these beginnings are not there. I think we must accept them -as completely new discoveries, made by the people who were developing -the whole new culture pattern of classic southern Mesopotamia. Full -description of the art, architecture, and writing of the Proto-Literate -phase would call for many details. Men like Professor Jacobsen and Dr. -Adams can give you these details much better than I can. Nor shall I do -more than tell you that the common pottery of the Proto-Literate phase -was so well standardized that it looks factory made. There was also -some handsome painted pottery, and there were stone bowls with inlaid -decoration. Well-made tools in metal had by now become fairly common, -and the metallurgist was experimenting with the casting process. Signs -for plows have been identified in the early pictographs, and a wheeled -chariot is shown on a cylinder seal engraving. But if I were forced to -a guess in the matter, I would say that the development of plows and -draft-animals probably began in the Ubaid period and was another of the -great innovations of that time. - -The Proto-Literate assemblage clearly suggests a highly developed and -sophisticated culture. While perhaps not yet fully urban, it is on -the threshold of urbanization. There seems to have been a very dense -settlement of Proto-Literate sites in classic southern Mesopotamia, -many of them newly founded on virgin soil where no earlier settlements -had been. When we think for a moment of what all this implies, of the -growth of an irrigation system which must have existed to allow the -flourish of this culture, and of the social and political organization -necessary to maintain the irrigation system, I think we will agree that -at last we are dealing with civilization proper. - - -FROM PREHISTORY TO HISTORY - -Now it is time for the conventional ancient historians to take over -the story from me. Remember this when you read what they write. Their -real base-line is with cultures ruled over by later kings and emperors, -whose writings describe military campaigns and the administration of -laws and fully organized trading ventures. To these historians, the -Proto-Literate phase is still a simple beginning for what is to follow. -If they mention the Ubaid assemblage at all--the one I was so lyrical -about--it will be as some dim and fumbling step on the path to the -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 -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 -historian are bound to be different. You will need both if you want to -understand all of the story of men, as they lived through time to the -present. - - - - -End of PREHISTORY - -[Illustration] - - -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 -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 -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, -presently to be replaced by Persia. - -A new culture, partaking of older west Asiatic and Egyptian elements, -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 -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 -to mention it, beyond recalling to you that it is to Greece we owe the -beginnings of rational or empirical science and thought in general. But -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 -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 -Africa. Remember, too, that prehistory in most of Europe runs on for -three thousand or more years _after_ conventional ancient history -begins in the Near East. Britain is a good example to use in showing -how prehistory ended in Europe. As we said earlier, it lies at the -opposite end of Europe from the area of highest cultural achievement in -those times, and should you care to read more of the story in detail, -you may do so in the English language. - - -METAL USERS REACH ENGLAND - -We left the story of Britain with the peoples who made three different -assemblages--the Windmill Hill, the megalith-builders, and the -Peterborough--making adjustments to their environments, to the original -inhabitants of the island, and to each other. They had first arrived -about 2500 B.C., and were simple pastoralists and hoe cultivators who -lived in little village communities. Some of them planted little if any -grain. By 2000 B.C., they were well settled in. Then, somewhere in the -range from about 1900 to 1800 B.C., the traces of the invasion of a new -series of peoples began to appear. - -The first newcomers are called the Beaker folk, after the name of a -peculiar form of pottery they made. The beaker type of pottery seems -oldest in Spain, where it occurs with great collective tombs of -megalithic construction and with copper tools. But the Beaker folk who -reached England seem already to have moved first from Spain(?) to the -Rhineland and Holland. While in the Rhineland, and before leaving for -England, the Beaker folk seem to have mixed with the local population -and also with incomers from northeastern Europe whose culture included -elements brought originally from the Near East by the eastern way -through the steppes. This last group has also been named for a peculiar -article in its assemblage; the group is called the Battle-axe folk. A -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 - archeological assemblage and human physical type. They speak - 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 - its usual meaning of standardized physical type. When I use - “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. - -The Beaker folk settled earliest in the agriculturally fertile south -and east. There seem to have been several phases of Beaker folk -invasions, and it is not clear whether these all came strictly from the -Rhineland or Holland. We do know that their copper daggers and awls -and armlets are more of Irish or Atlantic European than of Rhineland -origin. A few simple habitation sites and many burials of the Beaker -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 -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. - -[Illustration: BEAKER] - -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 -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 -the food-vessels seem to have been heavily involved in trade. It is -quite proper to wonder whether the food-vessel pottery itself was made -by local women who were married to traders who were middlemen in the -transmission of Irish metal objects to north Germany and Scandinavia. -The belt of high, relatively woodless country, from southwest to -northeast, was already established as a natural route for inland trade. - - -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 -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 -of amber to Mycenean Greece or even Egypt, as evidenced by glazed blue -beads. - -The great visual trace of Wessex achievement is the final form of -the spectacular sanctuary at Stonehenge. A wooden henge or circular -monument was first made several hundred years earlier, but the site -now received its great circles of stone uprights and lintels. The -diameter of the surrounding ditch at Stonehenge is about 350 feet, the -diameter of the inner circle of large stones is about 100 feet, and -the tallest stone of the innermost horseshoe-shaped enclosure is 29 -feet 8 inches high. One circle is made of blue stones which must have -been transported from Pembrokeshire, 145 miles away as the crow flies. -Recently, many carvings representing the profile of a standard type of -bronze axe of the time, and several profiles of bronze daggers--one of -which has been called Mycenean in type--have been found carved in the -stones. We cannot, of course, describe the details of the religious -ceremonies which must have been staged in Stonehenge, but we can -certainly imagine the well-integrated and smoothly working culture -which must have been necessary before such a great monument could have -been built. - - -“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” -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 -marked. The different aspects of the assemblages of the Beaker folk and -the clearly expressed activities of the Food-vessel folk and the Wessex -warriors show that Britain was already taking on her characteristic -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. -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 -encountered traders who traveled the great north-south European road, -by which Baltic amber moved southward to Greece and the Levant, and -ideas and things moved northward again. - -There was, however, the Channel between England and Europe, and this -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, -the far north, and the distant islands not yet fully touched by -food-production. The “personality of Britain” was being formed. - - -CREMATION BURIALS BEGIN - -Along with people of certain religious faiths, archeologists are -against cremation (for other people!). Individuals to be cremated seem -in past times to have been dressed in their trappings and put upon a -large pyre: it takes a lot of wood and a very hot fire for a thorough -cremation. When the burning had been completed, the few fragile scraps -of bone and such odd beads of stone or other rare items as had resisted -the great heat seem to have been whisked into a pot and the pot buried. -The archeologist is left with the pot and the unsatisfactory scraps in -it. - -Tentatively, after about 1400 B.C. and almost completely over the whole -island by 1200 B.C., Britain became the scene of cremation burials -in urns. We know very little of the people themselves. None of their -settlements have been identified, although there is evidence that they -grew barley and made enclosures for cattle. The urns used for the -burials seem to have antecedents in the pottery of the Food-vessel -folk, and there are some other links with earlier British traditions. -In Lancashire, a wooden circle seems to have been built about a grave -with cremated burials in urns. Even occasional instances of cremation -may be noticed earlier in Britain, and it is not clear what, if any, -connection the British cremation burials in urns have with the classic -_Urnfields_ which were now beginning in the east Mediterranean and -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 -until after 900 B.C. and are considered to be a specialized highland -“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. - -Granting that our knowledge of this phase of British prehistory is -very restricted because the cremations have left so little for the -archeologist, it does not appear that the cremation-burial-urn folk can -be sharply set off from their immediate predecessors. But change on a -grander scale was on the way. - - -REVERBERATIONS FROM CENTRAL EUROPE - -In the centuries immediately following 1000 B.C., we see with fair -clarity two phases of a cultural process which must have been going -on for some time. Certainly several of the invasions we have already -described in this chapter were due to earlier phases of the same -cultural process, but we could not see the details. - -[Illustration: SLASHING SWORD] - -Around 1200 B.C. central Europe was upset by the spread of the -so-called Urnfield folk, who practiced cremation burial in urns and -whom we also know to have been possessors of long, slashing swords and -the horse. I told you above that we have no idea that the Urnfield -folk proper were in any way connected with the people who made -cremation-burial-urn cemeteries a century or so earlier in Britain. It -has been supposed that the Urnfield folk themselves may have shared -ideas with the people who sacked Troy. We know that the Urnfield -pressure from central Europe displaced other people in northern France, -and perhaps in northwestern Germany, and that this reverberated into -Britain about 1000 B.C. - -Soon after 750 B.C., the same thing happened again. This time, the -pressure from central Europe came from the Hallstatt folk who were iron -tool makers: the reverberation brought people from the western Alpine -region across the Channel into Britain. - -At first it is possible to see the separate results of these folk -movements, but the developing cultures soon fused with each other and -with earlier British elements. Presently there were also strains of -other northern and western European pottery and traces of Urnfield -practices themselves which appeared in the finished British product. I -hope you will sense that I am vastly over-simplifying the details. - -The result seems to have been--among other things--a new kind of -agricultural system. The land was marked off by ditched divisions. -Rectangular fields imply the plow rather than hoe cultivation. We seem -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 -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.” - -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 -parts of Wales, Scotland, and the Hebrides. - - -THE COMING OF IRON - -Iron--once the know-how of reducing it from its ore in a very hot, -closed fire has been achieved--produces a far cheaper and much more -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 -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 -phase. - -A few iron users reached Britain a little before 400 B.C. Not long -after that, a number of allied groups appeared in southern and -southeastern England. They came over the Channel from France and must -have been Celts with dialects related to those already in England. A -second wave of Celts arrived from the Marne district in France about -250 B.C. Finally, in the second quarter of the first century B.C., -there were several groups of newcomers, some of whom were Belgae of -a mixed Teutonic-Celtic confederacy of tribes in northern France and -Belgium. The Belgae preceded the Romans by only a few years. - - -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 -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. - -At Little Woodbury, near the town of Salisbury, a farmstead has been -rather completely excavated. The rustic buildings were within a -palisade, the round house itself was built of wood, and there were -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. - -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 -restlessness being shown by remote relatives in France. - - -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 -type and also the two-wheeled chariot. Like the Wessex warriors of -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] - -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 -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 -middle of the first century B.C., which was a troubled time in Britain. -The circular houses were built on timber platforms surrounded with -palisades. The preservation of antiquities by the water-logged peat of -the swamp has yielded us a long catalogue of the materials of these -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” -(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. - - -THE THIRD PHASE - -For the time of about the middle of the first century B.C., we again -see traces of frantic hill-fort construction. This simple military -architecture now took some new forms. Its multiple ramparts must -reflect the use of slings as missiles, rather than spears. We probably -know the reason. In 56 B.C., Julius Caesar chastised the Veneti of -Brittany for outraging the dignity of Roman ambassadors. The Veneti -were famous slingers, and doubtless the reverberations of escaping -Veneti were felt across the Channel. The military architecture suggests -that some Veneti did escape to Britain. - -Also, through Caesar, we learn the names of newcomers who arrived in -two waves, about 75 B.C. and about 50 B.C. These were the Belgae. Now, -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. -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 -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 -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.” - -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 -his own is able to describe the ways of men in the past. - - -BRITAIN AS A SAMPLE OF THE GENERAL COURSE OF PREHISTORY IN EUROPE - -In giving this very brief outline of the later prehistory of Britain, -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 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. - -The whole course of later prehistory in Europe is, in fact, so very -complicated that there is no single good book to cover it all; -certainly there is none in English. There are some good regional -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, -cultures, and sub-cultures--which begin to appear as history -proceeds--were in themselves very complicated. - -Hence, I selected the case of Britain as a single example of how -prehistory ends in Europe. It could have been more complicated than we -found it to be. Even in the subject matter on Britain in the chapter -before the last, we did not see direct traces of the effect on Britain -of the very important developments which took place in the Danubian -way from the Near East. Apparently Britain was not affected. Britain -received the impulses which brought copper, bronze, and iron tools from -an original east Mediterranean homeland into Europe, almost at the ends -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 -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 -how the personality of Britain was formed. The principles that Fox -used, in trying to balance cultural and environmental factors and -interrelationships would not be greatly different for other lands. - - - - -Summary - -[Illustration] - - -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 -left only 1 per cent of the story for the historians to tell. - - -THE DRAMA OF THE PAST - -Men first became men when evolution had carried them to a certain -point. This was the point where the eye-hand-brain co-ordination was -good enough so that tools could be made. When tools began to be made -according to sets of lasting habits, we know that men had appeared. -This happened over a half million years ago. The stage for the play -may have been as broad as all of Europe, Africa, and Asia. At least, -it seems unlikely that it was only one little region that saw the -beginning of the drama. - -Glaciers and different climates came and went, to change the settings. -But the play went on in the same first act for a very long time. The -men who were the players had simple roles. They had to feed themselves -and protect themselves as best they could. They did this by hunting, -catching, and finding food wherever they could, and by taking such -protection as caves, fire, and their simple tools would give them. -Before the first act was over, the last of the glaciers was melting -away, and the players had added the New World to their stage. If -we want a special name for the first act, we could call it _The -Food-Gatherers_. - -There were not many climaxes in the first act, so far as we can see. -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 activity that were over and above pure food-getting. - - -THE SECOND ACT - -The second act began only about ten thousand years ago. A few of the -players started it by themselves near the center of the Old World part -of the stage, in the Near East. It began as a plant and animal act, but -it soon became much more complicated. - -But the players in this one part of the stage--in the Near East--were -not the only ones to start off on the second act by themselves. Other -players, possibly in several places in the Far East, and certainly in -the New World, also started second acts that began as plant and animal -acts, and then became complicated. We can call the whole second act -_The Food-Producers_. - - -THE FIRST GREAT CLIMAX OF THE SECOND ACT - -In the Near East, the first marked climax of the second act happened -in Mesopotamia and Egypt. The play and the players reached that great -climax that we call civilization. This seems to have come less than -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 -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 -some of these players. They seem perfectly happy to keep on with the -first act. - -The second act moved from climax to climax. The civilizations of -Mesopotamia and Egypt were only the earliest of these climaxes. The -players to the west caught the spirit of the thing, and climaxes -followed there. So also did climaxes come in the Far Eastern and New -World portions of the stage. - -The greater part of the second act should really be described to you -by a historian. Although it was a very short act when compared to the -first one, the climaxes complicate it a great deal. I, a prehistorian, -have told you about only the first act, and the very beginning of the -second. - - -THE THIRD ACT - -Also, as a prehistorian I probably should not even mention the third -act--it began so recently. The third act is _The Industrialization_. -It is the one in which we ourselves are players. If the pace of the -second act was so much faster than that of the first, the pace of the -third act is terrific. The danger is that it may wear down the players -completely. - -What sort of climaxes will the third act have, and are we already in -one? You have seen by now that the acts of my play are given in terms -of modes or basic patterns of human economy--ways in which people -get food and protection and safety. The climaxes involve more than -human economy. Economics and technological factors may be part of the -climaxes, but they are not all. The climaxes may be revolutions in -their own way, intellectual and social revolutions if you like. - -If the third act follows the pattern of the second act, a climax should -come soon after the act begins. We may be due for one soon if we are -not already in it. Remember the terrific pace of this third act. - - -WHY BOTHER WITH PREHISTORY? - -Why do we bother about prehistory? The main reason is that we think it -may point to useful ideas for the present. We are in the troublesome -beginnings of the third act of the play. The beginnings of the second -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. -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 -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 -another. - -By the time the written records of Mesopotamia and Egypt begin to turn -up in number, the climaxes there are well under way. To understand the -beginnings of the climaxes, and the real beginnings of the second act -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 -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, -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. - - - - -List of Books - - -BOOKS OF GENERAL INTEREST - -(Chosen from a variety of the increasingly useful list of cheap -paperbound books.) - - Childe, V. Gordon - _What Happened in History._ 1954. Penguin. - _Man Makes Himself._ 1955. Mentor. - _The Prehistory of European Society._ 1958. Penguin. - - Dunn, L. C., and Dobzhansky, Th. - _Heredity, Race, and Society._ 1952. Mentor. - - Frankfort, Henri, Frankfort, H. A., Jacobsen, Thorkild, and Wilson, - John A. - _Before Philosophy._ 1954. Penguin. - - Simpson, George G. - _The Meaning of Evolution._ 1955. Mentor. - - Wheeler, Sir Mortimer - _Archaeology from the Earth._ 1956. Penguin. - - -GEOCHRONOLOGY AND THE ICE AGE - -(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.) - - Flint, R. F. - _Glacial Geology and the Pleistocene Epoch._ 1947. John Wiley - and Sons. - - Zeuner, F. E. - _Dating the Past._ 1952 (3rd ed.). Methuen and Co. - - -FOSSIL MEN AND RACE - -(The points of view of physical anthropologists and human -paleontologists are changing very quickly. Two of the different points -of view are listed here.) - - Clark, W. E. Le Gros - _History of the Primates._ 1956 (5th ed.). British Museum - (Natural History). (Also in Phoenix edition, 1957.) - - Howells, W. W. - _Mankind So Far._ 1944. Doubleday, Doran. - - -GENERAL ANTHROPOLOGY - -(These are standard texts not absolutely up to date in every detail, or -interpretative essays concerned with cultural change through time as -well as in space.) - - Kroeber, A. L. - _Anthropology._ 1948. Harcourt, Brace. - - Linton, Ralph - _The Tree of Culture._ 1955. Alfred A. Knopf, Inc. - - Redfield, Robert - _The Primitive World and Its Transformations._ 1953. Cornell - University Press. - - Steward, Julian H. - _Theory of Culture Change._ 1955. University of Illinois Press. - - White, Leslie - _The Science of Culture._ 1949. Farrar, Strauss. - - -GENERAL PREHISTORY - -(A sampling of the more useful and current standard works in English.) - - Childe, V. Gordon - _The Dawn of European Civilization._ 1957. Kegan Paul, Trench, - Trubner. - _Prehistoric Migrations in Europe._ 1950. Instituttet for - Sammenlignende Kulturforskning. - - Clark, Grahame - _Archaeology and Society._ 1957. Harvard University Press. - - Clark, J. G. D. - _Prehistoric Europe: The Economic Basis._ 1952. Methuen and Co. - - Garrod, D. A. E. - _Environment, Tools, and Man._ 1946. Cambridge University - Press. - - Movius, Hallam L., Jr. - “Old World Prehistory: Paleolithic” in _Anthropology Today_. - Kroeber, A. L., ed. 1953. University of Chicago Press. - - Oakley, Kenneth P. - _Man the Tool-Maker._ 1956. British Museum (Natural History). - (Also in Phoenix edition, 1957.) - - Piggott, Stuart - _British Prehistory._ 1949. Oxford University Press. - - Pittioni, Richard - _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.) - - -THE NEAR EAST - - Adams, Robert M. - “Developmental Stages in Ancient Mesopotamia,” _in_ Steward, - Julian, _et al_, _Irrigation Civilizations: A Comparative - Study_. 1955. Pan American Union. - - Braidwood, Robert J. - _The Near East and the Foundations for Civilization._ 1952. - University of Oregon. - - Childe, V. Gordon - _New Light on the Most Ancient East._ 1952. Oriental Dept., - Routledge and Kegan Paul. - - Frankfort, Henri - _The Birth of Civilization in the Near East._ 1951. University - of Indiana Press. (Also in Anchor edition, 1956.) - - Pallis, Svend A. - _The Antiquity of Iraq._ 1956. Munksgaard. - - Wilson, John A. - _The Burden of Egypt._ 1951. University of Chicago Press. (Also - in Phoenix edition, called _The Culture of Ancient Egypt_, - 1956.) - - -HOW DIGGING IS DONE - - Braidwood, Linda - _Digging beyond the Tigris._ 1953. Schuman, New York. - - Wheeler, Sir Mortimer - _Archaeology from the Earth._ 1954. Oxford, London. - - - - -Index - - - Abbevillian, 48; - core-biface tool, 44, 48 - - Acheulean, 48, 60 - - Acheuleo-Levalloisian, 63 - - Acheuleo-Mousterian, 63 - - Adams, R. M., 106 - - Adzes, 45 - - Africa, east, 67, 89; - north, 70, 89; - south, 22, 25, 34, 40, 67 - - Agriculture, incipient, in England, 140; - in Near East, 123 - - Ain Hanech, 48 - - Amber, taken from Baltic to Greece, 167 - - American Indians, 90, 142 - - Anatolia, used as route to Europe, 138 - - Animals, in caves, 54, 64; - in cave art, 85 - - Antevs, Ernst, 19 - - Anyathian, 47 - - Archeological interpretation, 8 - - Archeology, defined, 8 - - Architecture, at Jarmo, 128; - at Jericho, 133 - - Arrow, points, 94; - shaft straightener, 83 - - Art, in caves, 84; - East Spanish, 85; - figurines, 84; - Franco-Cantabrian, 84, 85; - movable (engravings, modeling, scratchings), 83; - painting, 83; - sculpture, 83 - - Asia, western, 67 - - Assemblage, defined, 13, 14; - European, 94; - Jarmo, 129; - Maglemosian, 94; - Natufian, 113 - - Aterian, industry, 67; - point, 89 - - Australopithecinae, 24 - - Australopithecine, 25, 26 - - Awls, 77 - - Axes, 62, 94 - - Ax-heads, 15 - - Azilian, 97 - - Aztecs, 145 - - - Baghouz, 152 - - Bakun, 134 - - Baltic sea, 93 - - Banana, 107 - - Barley, wild, 108 - - Barrow, 141 - - Battle-axe folk, 164; - assemblage, 164 - - Beads, 80; - bone, 114 - - Beaker folk, 164; - assemblage, 164-165 - - Bear, in cave art, 85; - cult, 68 - - Belgium, 94 - - Belt cave, 126 - - Bering Strait, used as route to New World, 98 - - Bison, in cave art, 85 - - Blade, awl, 77; - backed, 75; - blade-core, 71; - end-scraper, 77; - stone, defined, 71; - strangulated (notched), 76; - tanged point, 76; - tools, 71, 75-80, 90; - tool tradition, 70 - - Boar, wild, in cave art, 85 - - Bogs, source of archeological materials, 94 - - Bolas, 54 - - Bordes, François, 62 - - Borer, 77 - - Boskop skull, 34 - - Boyd, William C., 35 - - Bracelets, 118 - - Brain, development of, 24 - - Breadfruit, 107 - - Breasted, James H., 107 - - Brick, at Jericho, 133 - - Britain, 94; - late prehistory, 163-175; - invaders, 173 - - Broch, 172 - - Buffalo, in China, 54; - killed by stampede, 86 - - Burials, 66, 86; - in “henges,” 164; - in urns, 168 - - Burins, 75 - - Burma, 90 - - Byblos, 134 - - - Camel, 54 - - Cannibalism, 55 - - Cattle, wild, 85, 112; - in cave art, 85; - domesticated, 15; - at Skara Brae, 142 - - Caucasoids, 34 - - Cave men, 29 - - Caves, 62; - art in, 84 - - Celts, 170 - - Chariot, 160 - - Chicken, domestication of, 107 - - Chiefs, in food-gathering groups, 68 - - Childe, V. Gordon, 8 - - China, 136 - - Choukoutien, 28, 35 - - Choukoutienian, 47 - - Civilization, beginnings, 144, 149, 157; - meaning of, 144 - - Clactonian, 45, 47 - - Clay, used in modeling, 128; - baked, used for tools, 153 - - Club-heads, 82, 94 - - Colonization, in America, 142; - in Europe, 142 - - Combe Capelle, 30 - - Combe Capelle-Brünn group, 34 - - Commont, Victor, 51 - - Coon, Carlton S., 73 - - Copper, 134 - - Corn, in America, 145 - - Corrals for cattle, 140 - - “Cradle of mankind,” 136 - - Cremation, 167 - - Crete, 162 - - Cro-Magnon, 30, 34 - - Cultivation, incipient, 105, 109, 111 - - Culture, change, 99; - characteristics, defined, 38, 49; - prehistoric, 39 - - - Danube Valley, used as route from Asia, 138 - - Dates, 153 - - Deer, 54, 96 - - Dog, domesticated, 96 - - Domestication, of animals, 100, 105, 107; - of plants, 100 - - “Dragon teeth” fossils in China, 28 - - Drill, 77 - - Dubois, Eugene, 26 - - - Early Dynastic Period, Mesopotamia, 147 - - East Spanish art, 72, 85 - - Egypt, 70, 126 - - Ehringsdorf, 31 - - Elephant, 54 - - Emiliani, Cesare, 18 - - Emiran flake point, 73 - - England, 163-168; - prehistoric, 19, 40; - farmers in, 140 - - Eoanthropus dawsoni, 29 - - Eoliths, 41 - - Erich, 152 - - Eridu, 152 - - Euphrates River, floods in, 148 - - Europe, cave dwellings, 58; - at end of Ice Age, 93; - early farmers, 140; - glaciers in, 40; - huts in, 86; - routes into, 137-140; - spread of food-production to, 136 - - - Far East, 69, 90 - - Farmers, 103 - - Fauresmith industry, 67 - - Fayum, 135; - radiocarbon date, 146 - - “Fertile Crescent,” 107, 146 - - Figurines, “Venus,” 84; - at Jarmo, 128; - at Ubaid, 153 - - Fire, used by Peking man, 54 - - First Dynasty, Egypt, 147 - - Fish-hooks, 80, 94 - - Fishing, 80; - by food-producers, 122 - - Fish-lines, 80 - - Fish spears, 94 - - Flint industry, 127 - - Fontéchevade, 32, 56, 58 - - Food-collecting, 104, 121; - end of, 104 - - Food-gatherers, 53, 176 - - Food-gathering, 99, 104; - in Old World, 104; - stages of, 104 - - Food-producers, 176 - - Food-producing economy, 122; - in America, 145; - in Asia, 105 - - Food-producing revolution, 99, 105; - causes of, 101; - preconditions for, 100 - - Food-production, beginnings of, 99; - carried to Europe, 110 - - Food-vessel folk, 164 - - “Forest folk,” 97, 98, 104, 110 - - Fox, Sir Cyril, 174 - - France, caves in, 56 - - - Galley Hill (fossil type), 29 - - Garrod, D. A., 73 - - Gazelle, 114 - - Germany, 94 - - Ghassul, 156 - - Glaciers, 18, 30; - destruction by, 40 - - Goat, wild, 108; - domesticated, 128 - - Grain, first planted, 20 - - Graves, passage, 141; - gallery, 141 - - Greece, civilization in, 163; - as route to western Europe, 138; - towns in, 162 - - Grimaldi skeletons, 34 - - - Hackberry seeds used as food, 55 - - Halaf, 151; - assemblage, 151 - - Hallstatt, tradition, 169 - - Hand, development of, 24, 25 - - Hand adzes, 46 - - Hand axes, 44 - - Harpoons, antler, 83, 94; - bone, 82, 94 - - Hassuna, 131; - assemblage, 131, 132 - - Heidelberg, fossil type, 28 - - Hill-forts, in England, 171; - in Scotland, 172 - - Hilly flanks of Near East, 107, 108, 125, 131, 146, 147 - - History, beginning of, 7, 17 - - Hoes, 112 - - Holland, 164 - - Homo sapiens, 32 - - Hooton, E. A., 34 - - Horse, 112; - wild, in cave art, 85; - in China, 54 - - Hotu cave, 126 - - Houses, 122; - at Jarmo, 128; - at Halaf, 151 - - Howe, Bruce, 116 - - Howell, F. Clark, 30 - - Hunting, 93 - - - Ice Age, in Asia, 99; - beginning of, 18; - glaciers in, 41; - last glaciation, 93 - - Incas, 145 - - India, 90, 136 - - Industrialization, 178 - - Industry, blade-tool, 88; - defined, 58; - ground stone, 94 - - Internationalism, 162 - - Iran, 107, 147 - - Iraq, 107, 124, 127, 136, 147 - - Iron, introduction of, 170 - - Irrigation, 123, 149, 155 - - Italy, 138 - - - Jacobsen, T. J., 157 - - Jarmo, 109, 126, 128, 130; - assemblage, 129 - - Java, 23, 29 - - Java man, 26, 27, 29 - - Jefferson, Thomas, 11 - - Jericho, 119, 133 - - Judaidah, 134 - - - Kafuan, 48 - - Kanam, 23, 36 - - Karim Shahir, 116-119, 124; - assemblage, 116, 117 - - Keith, Sir Arthur, 33 - - Kelley, Harper, 51 - - Kharga, 126 - - Khartoum, 136 - - Knives, 80 - - Krogman, W. M., 3, 25 - - - Lamps, 85 - - Land bridges in Mediterranean, 19 - - La Tène phase, 170 - - Laurel leaf point, 78, 89 - - Leakey, L. S. B., 40 - - Le Moustier, 57 - - Levalloisian, 47, 61, 62 - - Levalloiso-Mousterian, 47, 63 - - Little Woodbury, 170 - - - Magic, used by hunters, 123 - - Maglemosian, assemblage, 94, 95; - folk, 98 - - Makapan, 40 - - Mammoth, 93; - in cave art, 85 - - “Man-apes,” 26 - - Mango, 107 - - Mankind, age, 17 - - Maringer, J., 45 - - Markets, 155 - - Marston, A. T., 11 - - Mathiassen, T., 97 - - McCown, T. D., 33 - - Meganthropus, 26, 27, 36 - - Men, defined, 25; - modern, 32 - - Merimde, 135 - - Mersin, 133 - - Metal-workers, 160, 163, 167, 172 - - Micoquian, 48, 60 - - Microliths, 87; - at Jarmo, 130; - “lunates,” 87; - trapezoids, 87; - triangles, 87 - - Minerals used as coloring matter, 66 - - Mine-shafts, 140 - - M’lefaat, 126, 127 - - Mongoloids, 29, 90 - - Mortars, 114, 118, 127 - - Mounds, how formed, 12 - - Mount Carmel, 11, 33, 52, 59, 64, 69, 113, 114 - - “Mousterian man,” 64 - - “Mousterian” tools, 61, 62; - of Acheulean tradition, 62 - - Movius, H. L., 47 - - - Natufian, animals in, 114; - assemblage, 113, 114, 115; - burials, 114; - date of, 113 - - Neanderthal man, 29, 30, 31, 56 - - Near East, beginnings of civilization in, 20, 144; - cave sites, 58; - climate in Ice Age, 99; - “Fertile Crescent,” 107, 146; - food-production in, 99; - Natufian assemblage in, 113-115; - stone tools, 114 - - Needles, 80 - - Negroid, 34 - - New World, 90 - - Nile River valley, 102, 134; - floods in, 148 - - Nuclear area, 106, 110; - in Near East, 107 - - - Obsidian, used for blade tools, 71; - at Jarmo, 130 - - Ochre, red, with burials, 86 - - Oldowan, 48 - - Old World, 67, 70, 90; - continental phases in, 18 - - Olorgesailie, 40, 51 - - Ostrich, in China, 54 - - Ovens, 128 - - Oxygen isotopes, 18 - - - Paintings in caves, 83 - - Paleoanthropic man, 50 - - Palestine, burials, 56; - cave sites, 52; - types of man, 69 - - Parpallo, 89 - - Patjitanian, 45, 47 - - Pebble tools, 42 - - Peking cave, 54; - animals in, 54 - - Peking man, 27, 28, 29, 54, 58 - - Pendants, 80; - bone, 114 - - Pestle, 114 - - Peterborough, 141; - assemblage, 141 - - Pictographic signs, 158 - - Pig, wild, 108 - - “Piltdown man,” 29 - - Pins, 80 - - Pithecanthropus, 26, 27, 30, 36 - - Pleistocene, 18, 25 - - Plows developed, 123 - - Points, arrow, 76; - laurel leaf, 78; - shouldered, 78, 79; - split-based bone, 80, 82; - tanged, 76; - willow leaf, 78 - - Potatoes, in America, 145 - - Pottery, 122, 130, 156; - decorated, 142; - painted, 131, 151, 152; - Susa style, 156; - in tombs, 141 - - Prehistory, defined, 7; - range of, 18 - - Pre-neanderthaloids, 30, 31, 37 - - Pre-Solutrean point, 89 - - Pre-Stellenbosch, 48 - - Proto-Literate assemblage, 157-160 - - - Race, 35; - biological, 36; - “pure,” 16 - - Radioactivity, 9, 10 - - Radioactive carbon dates, 18, 92, 120, 130, 135, 156 - - Redfield, Robert, 38, 49 - - Reed, C. A., 128 - - Reindeer, 94 - - Rhinoceros, 93; - in cave art, 85 - - Rhodesian man, 32 - - Riss glaciation, 58 - - Rock-shelters, 58; - art in, 85 - - - Saccopastore, 31 - - Sahara Desert, 34, 102 - - Samarra, 152; - pottery, 131, 152 - - Sangoan industry, 67 - - Sauer, Carl, 136 - - Sbaikian point, 89 - - Schliemann, H., 11, 12 - - Scotland, 171 - - Scraper, flake, 79; - end-scraper on blade, 77, 78; - keel-shaped, 79, 80, 81 - - Sculpture in caves, 83 - - Sebilian III, 126 - - Shaheinab, 135 - - Sheep, wild, 108; - at Skara Brae, 142; - in China, 54 - - Shellfish, 142 - - Ship, Ubaidian, 153 - - Sialk, 126, 134; - assemblage, 134 - - Siberia, 88; - pathway to New World, 98 - - Sickle, 112, 153; - blade, 113, 130 - - Silo, 122 - - Sinanthropus, 27, 30, 35 - - Skara Brae, 142 - - Snails used as food, 128 - - Soan, 47 - - Solecki, R., 116 - - Solo (fossil type), 29, 32 - - Solutrean industry, 77 - - Spear, shaft, 78; - thrower, 82, 83 - - Speech, development of organs of, 25 - - Squash, in America, 145 - - Steinheim fossil skull, 28 - - Stillbay industry, 67 - - Stonehenge, 166 - - Stratification, in caves, 12, 57; - in sites, 12 - - Swanscombe (fossil type), 11, 28 - - Syria, 107 - - - Tabun, 60, 71 - - Tardenoisian, 97 - - Taro, 107 - - Tasa, 135 - - Tayacian, 47, 59 - - Teeth, pierced, in beads and pendants, 114 - - Temples, 123, 155 - - Tepe Gawra, 156 - - Ternafine, 29 - - Teshik Tash, 69 - - Textiles, 122 - - Thong-stropper, 80 - - Tigris River, floods in, 148 - - Toggle, 80 - - Tomatoes, in America, 145 - - Tombs, megalithic, 141 - - Tool-making, 42, 49 - - Tool-preparation traditions, 65 - - Tools, 62; - antler, 80; - blade, 70, 71, 75; - bone, 66; - chopper, 47; - core-biface, 43, 48, 60, 61; - flake, 44, 47, 51, 60, 64; - flint, 80, 127; - ground stone, 68, 127; - handles, 94; - pebble, 42, 43, 48, 53; - use of, 24 - - Touf (mud wall), 128 - - Toynbee, A. J., 101 - - Trade, 130, 155, 162 - - Traders, 167 - - Traditions, 15; - blade tool, 70; - definition of, 51; - interpretation of, 49; - tool-making, 42, 48; - chopper-tool, 47; - chopper-chopping tool, 45; - core-biface, 43, 48; - flake, 44, 47; - pebble tool, 42, 48 - - Tool-making, prehistory of, 42 - - Turkey, 107, 108 - - - Ubaid, 153; - assemblage, 153-155 - - Urnfields, 168, 169 - - - Village-farming community era, 105, 119 - - - Wad B, 72 - - Wadjak, 34 - - Warka phase, 156; - assemblage, 156 - - Washburn, Sherwood L., 36 - - Water buffalo, domestication of, 107 - - Weidenreich, F., 29, 34 - - Wessex, 166, 167 - - Wheat, wild, 108; - partially domesticated, 127 - - Willow leaf point, 78 - - Windmill Hill, 138; - assemblage, 138, 140 - - Witch doctors, 68 - - Wool, 112; - in garments, 167 - - Writing, 158; - cuneiform, 158 - - Würm I glaciation, 58 - - - Zebu cattle, domestication of, 107 - - Zeuner, F. E., 73 - - - - - * * * * * * - - - - -Transcriber’s note: - -Punctuation, hyphenation, and spelling were made consistent when a -predominant preference was found in this book; otherwise they were not -changed. - -Simple typographical errors were corrected; occasional unbalanced -quotation marks retained. - -Ambiguous hyphens at the ends of lines were retained. - -Index not checked for proper alphabetization or correct page references. - -In the original book, chapter headings were accompanied by -illustrations, sometimes above, sometimes below, and sometimes -adjacent. In this eBook those ilustrations always appear below the -headings. - - - -***END OF THE PROJECT GUTENBERG EBOOK PREHISTORIC MEN*** - - -******* This file should be named 52664-0.txt or 52664-0.zip ******* - - -This and all associated files of various formats will be found in: -http://www.gutenberg.org/dirs/5/2/6/6/52664 - - -Updated editions will replace the previous one--the old editions will -be renamed. - -Creating the works from print editions not protected by U.S. copyright -law means that no one owns a United States copyright in these works, -so the Foundation (and you!) can copy and distribute it in the United -States without permission and without paying copyright -royalties. Special rules, set forth in the General Terms of Use part -of this license, apply to copying and distributing Project -Gutenberg-tm electronic works to protect the PROJECT GUTENBERG-tm -concept and trademark. Project Gutenberg is a registered trademark, -and may not be used if you charge for the eBooks, unless you receive -specific permission. If you do not charge anything for copies of this -eBook, complying with the rules is very easy. You may use this eBook -for nearly any purpose such as creation of derivative works, reports, -performances and research. They may be modified and printed and given -away--you may do practically ANYTHING in the United States with eBooks -not protected by U.S. copyright law. Redistribution is subject to the -trademark license, especially commercial redistribution. - -START: FULL LICENSE - -THE FULL PROJECT GUTENBERG LICENSE -PLEASE READ THIS BEFORE YOU DISTRIBUTE OR USE THIS WORK - -To protect the Project Gutenberg-tm mission of promoting the free -distribution of electronic works, by using or distributing this work -(or any other work associated in any way with the phrase "Project -Gutenberg"), you agree to comply with all the terms of the Full -Project Gutenberg-tm License available with this file or online at -www.gutenberg.org/license. - -Section 1. General Terms of Use and Redistributing Project -Gutenberg-tm electronic works - -1.A. By reading or using any part of this Project Gutenberg-tm -electronic work, you indicate that you have read, understand, agree to -and accept all the terms of this license and intellectual property -(trademark/copyright) agreement. If you do not agree to abide by all -the terms of this agreement, you must cease using and return or -destroy all copies of Project Gutenberg-tm electronic works in your -possession. If you paid a fee for obtaining a copy of or access to a -Project Gutenberg-tm electronic work and you do not agree to be bound -by the terms of this agreement, you may obtain a refund from the -person or entity to whom you paid the fee as set forth in paragraph -1.E.8. - -1.B. "Project Gutenberg" is a registered trademark. It may only be -used on or associated in any way with an electronic work by people who -agree to be bound by the terms of this agreement. There are a few -things that you can do with most Project Gutenberg-tm electronic works -even without complying with the full terms of this agreement. See -paragraph 1.C below. There are a lot of things you can do with Project -Gutenberg-tm electronic works if you follow the terms of this -agreement and help preserve free future access to Project Gutenberg-tm -electronic works. See paragraph 1.E below. - -1.C. The Project Gutenberg Literary Archive Foundation ("the -Foundation" or PGLAF), owns a compilation copyright in the collection -of Project Gutenberg-tm electronic works. Nearly all the individual -works in the collection are in the public domain in the United -States. If an individual work is unprotected by copyright law in the -United States and you are located in the United States, we do not -claim a right to prevent you from copying, distributing, performing, -displaying or creating derivative works based on the work as long as -all references to Project Gutenberg are removed. Of course, we hope -that you will support the Project Gutenberg-tm mission of promoting -free access to electronic works by freely sharing Project Gutenberg-tm -works in compliance with the terms of this agreement for keeping the -Project Gutenberg-tm name associated with the work. You can easily -comply with the terms of this agreement by keeping this work in the -same format with its attached full Project Gutenberg-tm License when -you share it without charge with others. - -1.D. The copyright laws of the place where you are located also govern -what you can do with this work. Copyright laws in most countries are -in a constant state of change. If you are outside the United States, -check the laws of your country in addition to the terms of this -agreement before downloading, copying, displaying, performing, -distributing or creating derivative works based on this work or any -other Project Gutenberg-tm work. The Foundation makes no -representations concerning the copyright status of any work in any -country outside the United States. - -1.E. Unless you have removed all references to Project Gutenberg: - -1.E.1. The following sentence, with active links to, or other -immediate access to, the full Project Gutenberg-tm License must appear -prominently whenever any copy of a Project Gutenberg-tm work (any work -on which the phrase "Project Gutenberg" appears, or with which the -phrase "Project Gutenberg" is associated) is accessed, displayed, -performed, viewed, copied or distributed: - - 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 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 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. - -1.E.2. If an individual Project Gutenberg-tm electronic work is -derived from texts not protected by U.S. copyright law (does not -contain a notice indicating that it is posted with permission of the -copyright holder), the work can be copied and distributed to anyone in -the United States without paying any fees or charges. If you are -redistributing or providing access to a work with the phrase "Project -Gutenberg" associated with or appearing on the work, you must comply -either with the requirements of paragraphs 1.E.1 through 1.E.7 or -obtain permission for the use of the work and the Project Gutenberg-tm -trademark as set forth in paragraphs 1.E.8 or 1.E.9. - -1.E.3. If an individual Project Gutenberg-tm electronic work is posted -with the permission of the copyright holder, your use and distribution -must comply with both paragraphs 1.E.1 through 1.E.7 and any -additional terms imposed by the copyright holder. Additional terms -will be linked to the Project Gutenberg-tm License for all works -posted with the permission of the copyright holder found at the -beginning of this work. - -1.E.4. Do not unlink or detach or remove the full Project Gutenberg-tm -License terms from this work, or any files containing a part of this -work or any other work associated with Project Gutenberg-tm. - -1.E.5. Do not copy, display, perform, distribute or redistribute this -electronic work, or any part of this electronic work, without -prominently displaying the sentence set forth in paragraph 1.E.1 with -active links or immediate access to the full terms of the Project -Gutenberg-tm License. - -1.E.6. You may convert to and distribute this work in any binary, -compressed, marked up, nonproprietary or proprietary form, including -any word processing or hypertext form. However, if you provide access -to or distribute copies of a Project Gutenberg-tm work in a format -other than "Plain Vanilla ASCII" or other format used in the official -version posted on the official Project Gutenberg-tm web site -(www.gutenberg.org), you must, at no additional cost, fee or expense -to the user, provide a copy, a means of exporting a copy, or a means -of obtaining a copy upon request, of the work in its original "Plain -Vanilla ASCII" or other form. Any alternate format must include the -full Project Gutenberg-tm License as specified in paragraph 1.E.1. - -1.E.7. Do not charge a fee for access to, viewing, displaying, -performing, copying or distributing any Project Gutenberg-tm works -unless you comply with paragraph 1.E.8 or 1.E.9. - -1.E.8. You may charge a reasonable fee for copies of or providing -access to or distributing Project Gutenberg-tm electronic works -provided that - -* You pay a royalty fee of 20% of the gross profits you derive from - the use of Project Gutenberg-tm works calculated using the method - you already use to calculate your applicable taxes. The fee is owed - to the owner of the Project Gutenberg-tm trademark, but he has - agreed to donate royalties under this paragraph to the Project - Gutenberg Literary Archive Foundation. Royalty payments must be paid - within 60 days following each date on which you prepare (or are - legally required to prepare) your periodic tax returns. Royalty - payments should be clearly marked as such and sent to the Project - Gutenberg Literary Archive Foundation at the address specified in - Section 4, "Information about donations to the Project Gutenberg - Literary Archive Foundation." - -* You provide a full refund of any money paid by a user who notifies - you in writing (or by e-mail) within 30 days of receipt that s/he - does not agree to the terms of the full Project Gutenberg-tm - License. You must require such a user to return or destroy all - copies of the works possessed in a physical medium and discontinue - all use of and all access to other copies of Project Gutenberg-tm - works. - -* You provide, in accordance with paragraph 1.F.3, a full refund of - any money paid for a work or a replacement copy, if a defect in the - electronic work is discovered and reported to you within 90 days of - receipt of the work. - -* You comply with all other terms of this agreement for free - distribution of Project Gutenberg-tm works. - -1.E.9. If you wish to charge a fee or distribute a Project -Gutenberg-tm electronic work or group of works on different terms than -are set forth in this agreement, you must obtain permission in writing -from both the Project Gutenberg Literary Archive Foundation and The -Project Gutenberg Trademark LLC, the owner of the Project Gutenberg-tm -trademark. Contact the Foundation as set forth in Section 3 below. - -1.F. - -1.F.1. Project Gutenberg volunteers and employees expend considerable -effort to identify, do copyright research on, transcribe and proofread -works not protected by U.S. copyright law in creating the Project -Gutenberg-tm collection. Despite these efforts, Project Gutenberg-tm -electronic works, and the medium on which they may be stored, may -contain "Defects," such as, but not limited to, incomplete, inaccurate -or corrupt data, transcription errors, a copyright or other -intellectual property infringement, a defective or damaged disk or -other medium, a computer virus, or computer codes that damage or -cannot be read by your equipment. - -1.F.2. LIMITED WARRANTY, DISCLAIMER OF DAMAGES - Except for the "Right -of Replacement or Refund" described in paragraph 1.F.3, the Project -Gutenberg Literary Archive Foundation, the owner of the Project -Gutenberg-tm trademark, and any other party distributing a Project -Gutenberg-tm electronic work under this agreement, disclaim all -liability to you for damages, costs and expenses, including legal -fees. YOU AGREE THAT YOU HAVE NO REMEDIES FOR NEGLIGENCE, STRICT -LIABILITY, BREACH OF WARRANTY OR BREACH OF CONTRACT EXCEPT THOSE -PROVIDED IN PARAGRAPH 1.F.3. YOU AGREE THAT THE FOUNDATION, THE -TRADEMARK OWNER, AND ANY DISTRIBUTOR UNDER THIS AGREEMENT WILL NOT BE -LIABLE TO YOU FOR ACTUAL, DIRECT, INDIRECT, CONSEQUENTIAL, PUNITIVE OR -INCIDENTAL DAMAGES EVEN IF YOU GIVE NOTICE OF THE POSSIBILITY OF SUCH -DAMAGE. - -1.F.3. LIMITED RIGHT OF REPLACEMENT OR REFUND - If you discover a -defect in this electronic work within 90 days of receiving it, you can -receive a refund of the money (if any) you paid for it by sending a -written explanation to the person you received the work from. If you -received the work on a physical medium, you must return the medium -with your written explanation. The person or entity that provided you -with the defective work may elect to provide a replacement copy in -lieu of a refund. If you received the work electronically, the person -or entity providing it to you may choose to give you a second -opportunity to receive the work electronically in lieu of a refund. If -the second copy is also defective, you may demand a refund in writing -without further opportunities to fix the problem. - -1.F.4. Except for the limited right of replacement or refund set forth -in paragraph 1.F.3, this work is provided to you 'AS-IS', WITH NO -OTHER WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PURPOSE. - -1.F.5. Some states do not allow disclaimers of certain implied -warranties or the exclusion or limitation of certain types of -damages. If any disclaimer or limitation set forth in this agreement -violates the law of the state applicable to this agreement, the -agreement shall be interpreted to make the maximum disclaimer or -limitation permitted by the applicable state law. The invalidity or -unenforceability of any provision of this agreement shall not void the -remaining provisions. - -1.F.6. INDEMNITY - You agree to indemnify and hold the Foundation, the -trademark owner, any agent or employee of the Foundation, anyone -providing copies of Project Gutenberg-tm electronic works in -accordance with this agreement, and any volunteers associated with the -production, promotion and distribution of Project Gutenberg-tm -electronic works, harmless from all liability, costs and expenses, -including legal fees, that arise directly or indirectly from any of -the following which you do or cause to occur: (a) distribution of this -or any Project Gutenberg-tm work, (b) alteration, modification, or -additions or deletions to any Project Gutenberg-tm work, and (c) any -Defect you cause. - -Section 2. Information about the Mission of Project Gutenberg-tm - -Project Gutenberg-tm is synonymous with the free distribution of -electronic works in formats readable by the widest variety of -computers including obsolete, old, middle-aged and new computers. It -exists because of the efforts of hundreds of volunteers and donations -from people in all walks of life. - -Volunteers and financial support to provide volunteers with the -assistance they need are critical to reaching Project Gutenberg-tm's -goals and ensuring that the Project Gutenberg-tm collection will -remain freely available for generations to come. In 2001, the Project -Gutenberg Literary Archive Foundation was created to provide a secure -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 - -Section 3. Information about the Project Gutenberg Literary -Archive Foundation - -The Project Gutenberg Literary Archive Foundation is a non profit -501(c)(3) educational corporation organized under the laws of the -state of Mississippi and granted tax exempt status by the Internal -Revenue Service. The Foundation's EIN or federal tax identification -number is 64-6221541. Contributions to the Project Gutenberg Literary -Archive Foundation are tax deductible to the full extent permitted by -U.S. federal laws and your state's laws. - -The Foundation's principal office is in Fairbanks, Alaska, with the -mailing address: PO Box 750175, Fairbanks, AK 99775, but its -volunteers and employees are scattered throughout numerous -locations. Its business office is located at 809 North 1500 West, Salt -Lake City, UT 84116, (801) 596-1887. Email contact links and up to -date contact information can be found at the Foundation's web site and -official page at www.gutenberg.org/contact - -For additional contact information: - - Dr. Gregory B. Newby - Chief Executive and Director - gbnewby@pglaf.org - -Section 4. Information about Donations to the Project Gutenberg -Literary Archive Foundation - -Project Gutenberg-tm depends upon and cannot survive without wide -spread public support and donations to carry out its mission of -increasing the number of public domain and licensed works that can be -freely distributed in machine readable form accessible by the widest -array of equipment including outdated equipment. Many small donations -($1 to $5,000) are particularly important to maintaining tax exempt -status with the IRS. - -The Foundation is committed to complying with the laws regulating -charities and charitable donations in all 50 states of the United -States. Compliance requirements are not uniform and it takes a -considerable effort, much paperwork and many fees to meet and keep up -with these requirements. We do not solicit donations in locations -where we have not received written confirmation of compliance. To SEND -DONATIONS or determine the status of compliance for any particular -state visit www.gutenberg.org/donate - -While we cannot and do not solicit contributions from states where we -have not met the solicitation requirements, we know of no prohibition -against accepting unsolicited donations from donors in such states who -approach us with offers to donate. - -International donations are gratefully accepted, but we cannot make -any statements concerning tax treatment of donations received from -outside the United States. U.S. laws alone swamp our small staff. - -Please check the Project Gutenberg Web pages for current donation -methods and addresses. Donations are accepted in a number of other -ways including checks, online payments and credit card donations. To -donate, please visit: www.gutenberg.org/donate - -Section 5. General Information About Project Gutenberg-tm electronic works. - -Professor Michael S. Hart was the originator of the Project -Gutenberg-tm concept of a library of electronic works that could be -freely shared with anyone. For forty years, he produced and -distributed Project Gutenberg-tm eBooks with only a loose network of -volunteer support. - -Project Gutenberg-tm eBooks are often created from several printed -editions, all of which are confirmed as not protected by copyright in -the U.S. unless a copyright notice is included. Thus, we do not -necessarily keep eBooks in compliance with any particular paper -edition. - -Most people start at our Web site which has the main PG search -facility: www.gutenberg.org - -This Web site includes information about Project Gutenberg-tm, -including how to make donations to the Project Gutenberg Literary -Archive Foundation, how to help produce our new eBooks, and how to -subscribe to our email newsletter to hear about new eBooks. 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/XOR_cipher.py b/ciphers/XOR_cipher.py deleted file mode 100644 index 727fac3b0703..000000000000 --- a/ciphers/XOR_cipher.py +++ /dev/null @@ -1,209 +0,0 @@ -""" - 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 -""" -class XORCipher(object): - - def __init__(self, key = 0): - """ - simple constructor that receives a key or uses - default key = 0 - """ - - #private field - self.__key = key - - def encrypt(self, content, key): - """ - 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 - """ - - # precondition - assert (isinstance(key,int) and isinstance(content,str)) - - key = key or self.__key or 1 - - # make sure key can be any size - while (key > 255): - key -= 255 - - # This will be returned - ans = [] - - for ch in content: - ans.append(chr(ord(ch) ^ key)) - - return ans - - def decrypt(self,content,key): - """ - 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 - """ - - # precondition - assert (isinstance(key,int) and isinstance(content,list)) - - key = key or self.__key or 1 - - # make sure key can be any size - while (key > 255): - key -= 255 - - # This will be returned - ans = [] - - for ch in content: - ans.append(chr(ord(ch) ^ key)) - - return ans - - - def encrypt_string(self,content, key = 0): - """ - 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 - """ - - # precondition - assert (isinstance(key,int) and isinstance(content,str)) - - key = key or self.__key or 1 - - # make sure key can be any size - while (key > 255): - key -= 255 - - # This will be returned - ans = "" - - for ch in content: - ans += chr(ord(ch) ^ key) - - return ans - - def decrypt_string(self,content,key = 0): - """ - 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 - """ - - # precondition - assert (isinstance(key,int) and isinstance(content,str)) - - key = key or self.__key or 1 - - # make sure key can be any size - while (key > 255): - key -= 255 - - # This will be returned - ans = "" - - for ch in content: - ans += chr(ord(ch) ^ key) - - return ans - - - def encrypt_file(self, file, key = 0): - """ - 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) and isinstance(key,int)) - - try: - with open(file,"r") as fin: - with open("encrypt.out","w+") as fout: - - # actual encrypt-process - for line in fin: - fout.write(self.encrypt_string(line,key)) - - except: - return False - - return True - - - def decrypt_file(self,file, key): - """ - 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) and isinstance(key,int)) - - try: - with open(file,"r") as fin: - with open("decrypt.out","w+") as fout: - - # actual encrypt-process - for line in fin: - fout.write(self.decrypt_string(line,key)) - - except: - return False - - return True - - - - -# Tests -# crypt = XORCipher() -# key = 67 - -# # test enrcypt -# 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" \ No newline at end of file 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 6c1ba06f6850..10d16367cced 100644 --- a/ciphers/affine_cipher.py +++ b/ciphers/affine_cipher.py @@ -1,77 +1,109 @@ -from __future__ import print_function -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 deleted file mode 100644 index 3b0716442fc5..000000000000 --- a/ciphers/brute-force_caesar_cipher.py +++ /dev/null @@ -1,54 +0,0 @@ -from __future__ import print_function -def decrypt(message): - """ - >>> decrypt('TMDETUX PMDVU') - Decryption using Key #0: TMDETUX PMDVU - Decryption using Key #1: SLCDSTW OLCUT - Decryption using Key #2: RKBCRSV NKBTS - Decryption using Key #3: QJABQRU MJASR - Decryption using Key #4: PIZAPQT LIZRQ - Decryption using Key #5: OHYZOPS KHYQP - Decryption using Key #6: NGXYNOR JGXPO - Decryption using Key #7: MFWXMNQ IFWON - Decryption using Key #8: LEVWLMP HEVNM - Decryption using Key #9: KDUVKLO GDUML - Decryption using Key #10: JCTUJKN FCTLK - Decryption using Key #11: IBSTIJM EBSKJ - Decryption using Key #12: HARSHIL DARJI - Decryption using Key #13: GZQRGHK CZQIH - Decryption using Key #14: FYPQFGJ BYPHG - Decryption using Key #15: EXOPEFI AXOGF - Decryption using Key #16: DWNODEH ZWNFE - Decryption using Key #17: CVMNCDG YVMED - Decryption using Key #18: BULMBCF XULDC - Decryption using Key #19: ATKLABE WTKCB - Decryption using Key #20: ZSJKZAD VSJBA - Decryption using Key #21: YRIJYZC URIAZ - Decryption using Key #22: XQHIXYB TQHZY - Decryption using Key #23: WPGHWXA SPGYX - Decryption using Key #24: VOFGVWZ ROFXW - Decryption using Key #25: UNEFUVY QNEWV - """ - LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - for key in range(len(LETTERS)): - translated = "" - for symbol in message: - if symbol in LETTERS: - num = LETTERS.find(symbol) - num = num - key - if num < 0: - num = num + len(LETTERS) - translated = translated + LETTERS[num] - else: - translated = translated + symbol - print("Decryption using Key #%s: %s" % (key, translated)) - -def main(): - message = input("Encrypted message: ") - message = message.upper() - decrypt(message) - -if __name__ == '__main__': - import doctest - doctest.testmod() - main() diff --git a/ciphers/brute_force_caesar_cipher.py b/ciphers/brute_force_caesar_cipher.py new file mode 100644 index 000000000000..458d08db2628 --- /dev/null +++ b/ciphers/brute_force_caesar_cipher.py @@ -0,0 +1,58 @@ +import string + + +def decrypt(message: str) -> None: + """ + >>> decrypt('TMDETUX PMDVU') + Decryption using Key #0: TMDETUX PMDVU + Decryption using Key #1: SLCDSTW OLCUT + Decryption using Key #2: RKBCRSV NKBTS + Decryption using Key #3: QJABQRU MJASR + Decryption using Key #4: PIZAPQT LIZRQ + Decryption using Key #5: OHYZOPS KHYQP + Decryption using Key #6: NGXYNOR JGXPO + Decryption using Key #7: MFWXMNQ IFWON + Decryption using Key #8: LEVWLMP HEVNM + Decryption using Key #9: KDUVKLO GDUML + Decryption using Key #10: JCTUJKN FCTLK + Decryption using Key #11: IBSTIJM EBSKJ + Decryption using Key #12: HARSHIL DARJI + Decryption using Key #13: GZQRGHK CZQIH + Decryption using Key #14: FYPQFGJ BYPHG + Decryption using Key #15: EXOPEFI AXOGF + Decryption using Key #16: DWNODEH ZWNFE + Decryption using Key #17: CVMNCDG YVMED + Decryption using Key #18: BULMBCF XULDC + Decryption using Key #19: ATKLABE WTKCB + Decryption using Key #20: ZSJKZAD VSJBA + Decryption using Key #21: YRIJYZC URIAZ + Decryption using Key #22: XQHIXYB TQHZY + Decryption using Key #23: WPGHWXA SPGYX + Decryption using Key #24: VOFGVWZ ROFXW + Decryption using Key #25: UNEFUVY QNEWV + """ + for key in range(len(string.ascii_uppercase)): + translated = "" + for symbol in message: + if symbol in string.ascii_uppercase: + num = string.ascii_uppercase.find(symbol) + num = num - key + if num < 0: + num = num + len(string.ascii_uppercase) + translated = translated + string.ascii_uppercase[num] + else: + translated = translated + symbol + print(f"Decryption using Key #{key}: {translated}") + + +def main() -> None: + message = input("Encrypted message: ") + message = message.upper() + decrypt(message) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/ciphers/caesar_cipher.py b/ciphers/caesar_cipher.py index 6cd35e73db0d..ef5f49313ee7 100644 --- a/ciphers/caesar_cipher.py +++ b/ciphers/caesar_cipher.py @@ -1,68 +1,256 @@ -import sys -def encrypt(strng, key): - encrypted = '' - for x in strng: - indx = (ord(x) + key) % 256 - if indx > 126: - indx = indx - 95 - encrypted = encrypted + chr(indx) - return encrypted - - -def decrypt(strng, key): - decrypted = '' - for x in strng: - indx = (ord(x) - key) % 256 - if indx < 32: - indx = indx + 95 - decrypted = decrypted + chr(indx) - return decrypted - -def brute_force(strng): - key = 1 - decrypted = '' - while key <= 94: - for x in strng: - indx = (ord(x) - key) % 256 - if indx < 32: - indx = indx + 95 - decrypted = decrypted + chr(indx) - print("Key: {}\t| Message: {}".format(key, decrypted)) - decrypted = '' - key += 1 - return None - - -def main(): - print('-' * 10 + "\n**Menu**\n" + '-' * 10) - print("1.Encrpyt") - print("2.Decrypt") - print("3.BruteForce") - print("4.Quit") +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: + # 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: - choice = input("What would you like to do?: ") - if choice not in ['1', '2', '3', '4']: - print ("Invalid choice") - elif choice == '1': - strng = input("Please enter the string to be ecrypted: ") - while True: - key = int(input("Please enter off-set between 1-94: ")) - if key in range(1, 95): - print (encrypt(strng, key)) - main() - elif choice == '2': - strng = input("Please enter the string to be decrypted: ") - while True: - key = int(input("Please enter off-set between 1-94: ")) - if key > 0 and key <= 94: - print(decrypt(strng, key)) - main() - elif choice == '3': - strng = input("Please enter the string to be decrypted: ") - brute_force(strng) - main() - elif choice == '4': - print ("Goodbye.") - sys.exit() - -main() + 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}") + + 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 index 6a8751f69524..17ba55c0d013 100644 --- a/ciphers/elgamal_key_generator.py +++ b/ciphers/elgamal_key_generator.py @@ -1,25 +1,22 @@ import os import random import sys -import rabin_miller as rabinMiller, cryptomath_module as cryptoMath - -min_primitive_root = 3 +from . import cryptomath_module as cryptomath +from . import rabin_miller -def main(): - print('Making key files...') - makeKeyFiles('elgamal', 2048) - print('Key files generation successful') +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) +# 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 primitiveRoot(p_val): +def primitive_root(p_val: int) -> int: print("Generating primitive root of p") while True: - g = random.randrange(3,p_val) + g = random.randrange(3, p_val) if pow(g, 2, p_val) == 1: continue if pow(g, p_val, p_val) == 1: @@ -27,37 +24,43 @@ def primitiveRoot(p_val): return g -def generateKey(keySize): - print('Generating prime p...') - p = rabinMiller.generateLargePrime(keySize) # select large prime number. - e_1 = primitiveRoot(p) # one primitive root on modulo p. +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.findModInverse(pow(e_1, d, p), p) + e_2 = cryptomath.find_mod_inverse(pow(e_1, d, p), p) - publicKey = (keySize, e_1, e_2, p) - privateKey = (keySize, d) + public_key = (key_size, e_1, e_2, p) + private_key = (key_size, d) - return publicKey, privateKey + 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. \n' - 'Use 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('%d,%d,%d,%d' % (publicKey[0], publicKey[1], publicKey[2], publicKey[3])) + 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]}") + - print('Writing private key to file %s_privkey.txt...' % name) - with open('%s_privkey.txt' % name, 'w') as fo: - fo.write('%d,%d' % (privateKey[0], privateKey[1])) +def main() -> None: + print("Making key files...") + make_key_files("elgamal", 2048) + print("Key files generation successful") -if __name__ == '__main__': +if __name__ == "__main__": main() - \ No newline at end of file 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 20449b161963..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 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): +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 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 new file mode 100644 index 000000000000..8d1b2bd8c8d1 --- /dev/null +++ b/ciphers/prehistoric_men.txt @@ -0,0 +1,7193 @@ +The Project Gutenberg eBook, Prehistoric Men, by Robert J. (Robert John) +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 +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 +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. + + +Title: Prehistoric Men +Author: Robert J. (Robert John) Braidwood +Release Date: July 28, 2016 [eBook #52664] +Language: English +Character set encoding: UTF-8 + + +***START OF THE PROJECT GUTENBERG EBOOK PREHISTORIC MEN*** + + +E-text prepared by Stephen Hutcheson, Dave Morgan, Charlie Howard, and the +Online Distributed Proofreading Team (http://www.pgdp.net) + + + +Note: Project Gutenberg also has an HTML version of this + file which includes the original illustrations. + See 52664-h.htm or 52664-h.zip: + (http://www.gutenberg.org/files/52664/52664-h/52664-h.htm) + or + (http://www.gutenberg.org/files/52664/52664-h.zip) + + +Transcriber's note: + + Some characters might not display in this UTF-8 text + version. If so, the reader should consult the HTML + 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�. + Devices that cannot display that symbol may substitute + a question mark, a square, or other symbol. + + +PREHISTORIC MEN + +by + +ROBERT J. BRAIDWOOD + +Research Associate, Old World Prehistory + +Professor +Oriental Institute and Department of Anthropology +University of Chicago + +Drawings by Susan T. Richert + + +[Illustration] + +Chicago Natural History Museum +Popular Series +Anthropology, Number 37 + +Third Edition Issued in Co-operation with +The Oriental Institute, The University of Chicago + +Edited by Lillian A. Ross + +Printed in the United States of America +by Chicago Natural History Museum Press + +Copyright 1948, 1951, and 1957 by Chicago Natural History Museum + +First edition 1948 +Second edition 1951 +Third edition 1957 +Fourth edition 1959 + + +Preface + +[Illustration] + + +Like the writing of most professional archeologists, mine has been +confined to so-called learned papers. Good, bad, or indifferent, these +papers were in a jargon that only my colleagues and a few advanced +students could understand. Hence, when I was asked to do this little +book, I soon found it extremely difficult to say what I meant in simple +fashion. The style is new to me, but I hope the reader will not find it +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 +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 +of Pennsylvania), and also Mrs. Linda Braidwood, Associate of the +Oriental Institute, and Professors Fay-Cooper Cole and Sol Tax, of +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 +the story. I am deeply indebted to all these friends. + +While I was preparing the second edition, I had the great fortune +to be able to rework the third chapter with Professor Sherwood L. +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 +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 +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 +and better dating systems appear. + +I wish to thank Dr. Clifford C. Gregg, Director of Chicago Natural +History Museum, for the opportunity to publish this book. My old +friend, Dr. Paul S. Martin, Chief Curator in the Department of +Anthropology, asked me to undertake the job and inspired me to complete +it. I am also indebted to Miss Lillian A. Ross, Associate Editor of +Scientific Publications, and to Mr. George I. Quimby, Curator of +Exhibits in Anthropology, for all the time they have given me in +getting the manuscript into proper shape. + + ROBERT J. BRAIDWOOD + _June 15, 1950_ + + + + +Preface to the Third Edition + + +In preparing the enlarged third edition, many of the above mentioned +friends have again helped me. I have picked the brains of Professor F. +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 +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. + + ROBERT J. BRAIDWOOD + _March 1, 1959_ + + + + +Contents + + + PAGE + How We Learn about Prehistoric Men 7 + + The Changing World in Which Prehistoric Men Lived 17 + + Prehistoric Men Themselves 22 + + Cultural Beginnings 38 + + More Evidence of Culture 56 + + Early Moderns 70 + + End and Prelude 92 + + The First Revolution 121 + + The Conquest of Civilization 144 + + End of Prehistory 162 + + Summary 176 + + List of Books 180 + + Index 184 + + + + +HOW WE LEARN about Prehistoric Men + +[Illustration] + + +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 +million years old, but he did not begin to write history (or to write +anything) until about 5,000 years ago. + +The men who lived in prehistoric times left us no history books, but +they did unintentionally leave a record of their presence and their way +of life. This record is studied and interpreted by different kinds of +scientists. + + +SCIENTISTS WHO FIND OUT ABOUT PREHISTORIC MEN + +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 +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 +prehistoric men work with fossils, so they are sometimes called _human +paleontologists_. + + +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 +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 +history of human activity, provided always that the actions have +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 + ancient man, and + + 2. Only a few objects may be found, for most of these were probably + 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 + graves that have escaped destruction. + +What I mean is this: Let us say you are walking through a dump yard, +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 +sixty years old. + +When you think about the old spark plug in this way you have +just been making the beginnings of what we call an archeological +_interpretation_; you have been making the spark plug tell a story. +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. + + +SOME OTHER SCIENTISTS + +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 +_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 +the kinds of animals and plants that were living in prehistoric times +and so could be used for food by ancient man; what the weather was +like; and whether there were glaciers. Also, when I tell you that +prehistoric men did not appear until long after the great dinosaurs had +disappeared, I go on the say-so of the paleontologists. They know that +fossils of men and of dinosaurs are not found in the same geological +period. The dinosaur fossils come in early periods, the fossils of men +much later. + +Since World War II even the atomic scientists have been helping the +archeologists. By testing the amount of radioactivity left in charcoal, +wood, or other vegetable matter obtained from archeological sites, they +have been able to date the sites. Shell has been used also, and even +the hair of Egyptian mummies. The dates of geological and climatic +events have also been discovered. Some of this work has been done from +drillings taken from the bottom of the sea. + +This dating by radioactivity has considerably shortened the dates which +the archeologists used to give. If you find that some of the dates +I give here are more recent than the dates you see in other books +on prehistory, it is because I am using one of the new lower dating +systems. + +[Illustration: RADIOCARBON CHART + +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 + 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 + 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 + 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. + + +HOW THE SCIENTISTS FIND OUT + +So far, this chapter has been mainly about the people who find out +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 +the strange stones came to earth as bolts of lightning. We know now +that these strange stones were prehistoric stone tools. + +Many important finds still come to us by accident. In 1935, a British +dentist, A. T. Marston, found the first of two fragments of a very +important fossil human skull, in a gravel pit at Swanscombe, on the +River Thames, England. He had to wait nine months, until the face of +the gravel pit had been dug eight yards farther back, before the second +fragment appeared. They fitted! Then, twenty years later, still another +piece appeared. In 1928 workmen who were blasting out rock for the +breakwater in the port of Haifa began to notice flint tools. Thus the +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, +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 +an old idea of his. He believed that the heroes of ancient Troy and +Mycenae were once real Trojans and Greeks. He proved it by going to +Turkey and Greece and digging up the remains of both cities. + +Schliemann had the great good fortune to find rich and spectacular +treasures, and he also had the common sense to keep notes and make +descriptions of what he found. He proved beyond doubt that many ancient +city mounds can be _stratified_. This means that there may be the +remains of many towns in a mound, one above another, like layers in a +cake. + +You might like to have an idea of how mounds come to be in layers. +The original settlers may have chosen the spot because it had a good +spring and there were good fertile lands nearby, or perhaps because +it was close to some road or river or harbor. These settlers probably +built their town of stone and mud-brick. Finally, something would have +happened to the town--a flood, or a burning, or a raid by enemies--and +the walls of the houses would have fallen in or would have melted down +as mud in the rain. Nothing would have remained but the mud and debris +of a low mound of _one_ layer. + +The second settlers would have wanted the spot for the same reasons +the first settlers did--good water, land, and roads. Also, the second +settlers would have found a nice low mound to build their houses on, +a protection from floods. But again, something would finally have +happened to the second town, and the walls of _its_ houses would have +come tumbling down. This makes the _second_ layer. And so on.... + +In Syria I once had the good fortune to dig on a large mound that had +no less than fifteen layers. Also, most of the layers were thick, and +there were signs of rebuilding and repairs within each layer. The mound +was more than a hundred feet high. In each layer, the building material +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 +that their lowest layer of rock was oldest or earliest, and that the +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 +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 +soon set out to dig on ancient sites, from Egypt to Central America. + + +ARCHEOLOGICAL INFORMATION + +As time went on, the study of archeological materials--found either +by accident or by digging on purpose--began to show certain things. +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 +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. + +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 +our 1900 mail-order catalogue is from the one of today. + +The archeologists who came after Schliemann began to notice other +things and to compare them with occurrences in modern times. The +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 +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 +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 +has to date at about A.D. 1936-56. + +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. + + +EVERYTHING BEGINS TO FIT TOGETHER + +Now we need to pull these ideas all together and see the complicated +structure the archeologists can build with their materials. + +Even the earliest archeologists soon found that there was a very long +range of prehistoric time which would yield only very simple things. +For this very long early part of prehistory, there was little to be +found but the flint tools which wandering, hunting and gathering +people made, and the bones of the wild animals they ate. Toward the +end of prehistoric time there was a general settling down with the +coming of agriculture, and all sorts of new things began to be made. +Archeologists soon got a general notion of what ought to appear with +what. Thus, it would upset a French prehistorian digging at the bottom +of a very early cave if he found a fine bronze sword, just as much as +it would upset him if he found a beer bottle. The people of his very +early cave layer simply could not have made bronze swords, which came +later, just as do beer bottles. Some accidental disturbance of the +layers of his cave must have happened. + +With any luck, archeologists do their digging in a layered, stratified +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 +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 +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 +facts begin to stand out. We begin to be more sure of which people +lived at the same time, which earlier and which later. We begin to +know who traded with whom, and which peoples seemed to live off by +themselves. We begin to find enough skeletons in burials so that the +physical anthropologists can tell us what the people looked like. We +get animal bones, and a paleontologist may tell us they are all bones +of wild animals; or he may tell us that some or most of the bones are +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 +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? +How long did they last? We watch the different tools and the traces +of the way things were done--how the burials were arranged, what +the living-places were like, and so on. We wonder about the people +themselves, for the traces of habits and traditions are useful to us +only as clues to the men who once had them. So we ask the physical +anthropologists about the skeletons that we found in the burials. The +physical anthropologists tell us about the anatomy and the similarities +and differences which the skeletons show when compared with other +skeletons. The physical anthropologists are even working on a +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. + +We become particularly interested in any signs of change--when new +materials and tool types and ways of doing things replace old ones. We +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 +happened before history began. + + + + +THE CHANGING WORLD in which Prehistoric Men Lived + +[Illustration] + + +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 +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 +prehistoric time. + +Or maybe we can grasp the length of time better in terms of +generations. As you know, primitive peoples tend to marry and have +children rather early in life. So suppose we say that twenty years +will make an average generation. At this rate there would be 25,000 +generations in a half-million years. But our United States is much less +than ten generations old, twenty-five generations take us back before +the time of Columbus, Julius Caesar was alive just 100 generations ago, +David was king of Israel less than 150 generations ago, 250 generations +take us back to the beginning of written history. And there were 24,750 +generations of men before written history began! + +I should probably tell you that there is a new method of prehistoric +dating which would cut the earliest dates in my reckoning almost +in half. Dr. Cesare Emiliani, combining radioactive (C14) and +chemical (oxygen isotope) methods in the study of deep-sea borings, +has developed a system which would lower the total range of human +prehistory to about 300,000 years. The system is still too new to have +had general examination and testing. Hence, I have not used it in this +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 +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. + +In earlier times the earth looked very different. Geologists call the +last great geological period the _Pleistocene_. It began somewhere +between a half million and a million years ago, and was a time of great +changes. Sometimes we call it the Ice Age, for in the Pleistocene +there were at least three or four times when large areas of earth +were covered with glaciers. The reason for my uncertainty is that +while there seem to have been four major mountain or alpine phases of +glaciation, there may only have been three general continental phases +in the Old World.[2] + + [2] This is a complicated affair and I do not want to bother you + with its details. Both the alpine and the continental ice sheets + seem to have had minor fluctuations during their _main_ phases, + and the advances of the later phases destroyed many of the + traces of the earlier phases. The general textbooks have tended + to follow the names and numbers established for the Alps early + in this century by two German geologists. I will not bother you + with the names, but there were _four_ major phases. It is the + second of these alpine phases which seems to fit the traces of + the earliest of the great continental glaciations. In this book, + I will use the four-part system, since it is the most familiar, + but will add the word _alpine_ so you may remember to make the + transition to the continental system if you wish to do so. + +Glaciers are great sheets of ice, sometimes over a thousand feet +thick, which are now known only in Greenland and Antarctica and in +high mountains. During several of the glacial periods in the Ice Age, +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. + +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 +ice. Because so much water was frozen and contained in the glaciers, +the water level of lakes and oceans was lowered. Flooded areas were +drained and appeared as dry land. There were times in the Ice Age when +there was no English Channel, so that England was not an island, and a +land bridge at the Dardanelles probably divided the Mediterranean from +the Black Sea. + +A very important thing for people living during the time of a +glaciation was the region adjacent to the glacier. They could not, of +course, live on the ice itself. The questions would be how close could +they live to it, and how would they have had to change their way of +life to do so. + + +GLACIERS CHANGE THE WEATHER + +Great sheets of ice change the weather. When the front of a glacier +stood at Milwaukee, the weather must have been bitterly cold in +Chicago. The climate of the whole world would have been different, and +you can see how animals and men would have been forced to move from one +place to another in search of food and warmth. + +On the other hand, it looks as if only a minor proportion of the whole +Ice Age was really taken up by times of glaciation. In between came +the _interglacial_ periods. During these times the climate around +Chicago was as warm as it is now, and sometimes even warmer. It may +interest you to know that the last great glacier melted away less than +10,000 years ago. Professor Ernst Antevs thinks we may be living in an +interglacial period and that the Ice Age may not be over yet. So if you +want to make a killing in real estate for your several hundred times +great-grandchildren, you might buy some land in the Arizona desert or +the Sahara. + +We do not yet know just why the glaciers appeared and disappeared, as +they did. It surely had something to do with an increase in rainfall +and a fall in temperature. It probably also had to do with a general +tendency for the land to rise at the beginning of the Pleistocene. We +know there was some mountain-building at that time. Hence, rain-bearing +winds nourished the rising and cooler uplands with snow. An increase +in all three of these factors--if they came together--would only have +needed to be slight. But exactly why this happened we do not know. + +The reason I tell you about the glaciers is simply to remind you of the +changing world in which prehistoric men lived. Their surroundings--the +animals and plants they used for food, and the weather they had to +protect themselves from--were always changing. On the other hand, this +change happened over so long a period of time and was so slow that +individual people could not have noticed it. Glaciers, about which they +probably knew nothing, moved in hundreds of miles to the north of them. +The people must simply have wandered ever more southward in search +of the plants and animals on which they lived. Or some men may have +stayed where they were and learned to hunt different animals and eat +different foods. Prehistoric men had to keep adapting themselves to new +environments and those who were most adaptive were most successful. + + +OTHER CHANGES + +Changes took place in the men themselves as well as in the ways they +lived. As time went on, they made better tools and weapons. Then, too, +we begin to find signs of how they started thinking of other things +than food and the tools to get it with. We find that they painted on +the walls of caves, and decorated their tools; we find that they buried +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.� +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 +impossible for men to go beyond that point if they only hunted and +fished and gathered wild foods. Once the basic change was made--once +the food-producing revolution became effective--technology leaped ahead +and civilization and written history soon began. + + + + +Prehistoric Men THEMSELVES + +[Illustration] + + +DO WE KNOW WHERE MAN ORIGINATED? + +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 +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 +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 +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.� + +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, +we have not yet found the tools which we suppose these earliest men in +the Far East must have made. + +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� +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 +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. + +Very soon after the earliest Javanese come the bones of slightly more +developed people in Java, and the jawbone of a man who once lived in +what is now Germany. The same general glacial beds which yielded the +later Javanese bones and the German jawbone also include tools. These +finds come from the time of the second alpine glaciation. + +So this is the situation. By the time of the end of the second alpine +or first continental glaciation (say 400,000 years ago) we have traces +of men from the extremes of the more southerly portions of the Old +World--South Africa, eastern Asia, and western Europe. There are also +some traces of men in the middle ground. In fact, Professor Franz +Weidenreich believed that creatures who were the immediate ancestors +of men had already spread over Europe, Africa, and Asia by the time +the Ice Age began. We certainly have no reason to disbelieve this, but +fortunate accidents of discovery have not yet given us the evidence to +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 +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 +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, +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 +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 +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. + + +BRAINS, HANDS, AND TOOLS + +Whether the australopithecines were our ancestors or not, the proper +ancestors of men must have been able to stand erect and to walk on +their two feet. Three further important things probably were involved, +next, before they could become men proper. These are: + + 1. The increasing size and development of the brain. + + 2. The increasing usefulness (specialization) of the thumb and hand. + + 3. The use of tools. + +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 +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 +_co-ordination_ of these things that counts. + +Many other things must have been happening to the bodies of the +creatures who were the ancestors of men. Our ancestors had to develop +organs of speech. More than that, they had to get the idea of letting +_certain sounds_ made with these speech organs have _certain meanings_. + +All this must have gone very slowly. Probably everything was developing +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 +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 +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 +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 +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 +with the australopithecine fossils. But there are three varieties +of the australopithecines and they last on until a time equal to +that of the second alpine glaciation. They are the best suggestion +we have yet as to what the ancestors of men _may_ have looked like. +They were certainly closer to men than to apes. Although their brain +size was no larger than the brains of modern apes their body size and +stature were quite small; hence, relative to their small size, their +brains were large. We have not been able to prove without doubt that +the australopithecines were _tool-making_ creatures, even though the +recent news has it that tools have been found with australopithecine +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.� + + +THE EARLIEST MEN WE KNOW + +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 +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 +australopithecines and _Pithecanthropus_? It is still too early to say. +We shall need more finds before we can be definite one way or the other. + +Java man, _Pithecanthropus_, comes from geological beds equal in age +to the latter part of the second alpine glaciation; the _Meganthropus_ +finds refer to beds of the beginning of this glaciation. The first +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 +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. + +No tools were found in the geological deposits where bones of Java man +appeared. There are some tools in the same general area, but they come +a bit later in time. One reason we accept the Java man as man--aside +from his general anatomical appearance--is that these tools probably +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 +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 +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,� +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 +these bones are now. The Red Chinese accuse us of having stolen them. +They were last seen on a dock-side at a Chinese port. But should you +catch a Marine with a sack of old bones, perhaps we could achieve peace +in Asia by returning them! Fortunately, there is a complete set of +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 +in their drug stores and grind them into powder for medicine. The +�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 +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. + +Peking man dates from early in the interglacial period following the +second alpine glaciation. He probably lived close to 350,000 years +ago. There are several finds to account for in Europe by about this +time, and one from northwest Africa. The very large jawbone found +near Heidelberg in Germany is doubtless even earlier than Peking man. +The beds where it was found are of second alpine glacial times, and +recently some tools have been said to have come from the same beds. +There is not much I need tell you about the Heidelberg jaw save that it +seems certainly to have belonged to an early man, and that it is very +big. + +Another find in Germany was made at Steinheim. It consists of the +fragmentary skull of a man. It is very important because of its +relative completeness, but it has not yet been fully studied. The bone +is thick, but the back of the head is neither very low nor primitive, +and the face is also not primitive. The forehead does, however, have +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 +necessarily so. This is important because it bears on the question of +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 +found with them. Since no jaws have yet been found at Steinheim or +Swanscombe, but the time is the same, one wonders if these people had +jaws like those of Ternafine. + + +WHAT HAPPENED TO JAVA AND PEKING MEN + +Professor Weidenreich thought that there were at least a dozen ways in +which the Peking man resembled the modern Mongoloids. This would seem +to indicate that Peking man was really just a very early Chinese. + +Several later fossil men have been found in the Java-Australian area. +The best known of these is the so-called Solo man. There are some finds +from Australia itself which we now know to be quite late. But it looks +as if we may assume a line of evolution from Java man down to the +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 + +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 +him were part of an elaborate fake! + + +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 +human fossil to be recognized as such. + +[Illustration: PRINCIPAL KNOWN TYPES OF FOSSIL MEN + + CRO-MAGNON + NEANDERTHAL + MODERN SKULL + COMBE-CAPELLE + SINANTHROPUS + 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 +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 +Howell sees it this way. In Europe, the earliest trace of men we +now know is the Heidelberg jaw. Evolution continued in Europe, from +Heidelberg through the Swanscombe and Steinheim types to a group of +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 +the neanderthaloids proper--about half way between Steinheim and the +classic Neanderthal people. + +Professor Howell believes that the pre-neanderthaloids who happened to +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� +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 +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 +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. + +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 +something about how they lived, and about some of the tools they made. + + +OTHER MEN CONTEMPORARY WITH THE NEANDERTHALOIDS + +We have seen that the neanderthaloids seem to be a specialization +in a corner of Europe. What was going on elsewhere? We think that +the pre-neanderthaloid type was a generally widespread form of men. +From this type evolved other more or less extreme although generally +related men. The Solo finds in Java form one such case. Another was the +Rhodesian man of Africa, and the more recent Hopefield finds show more +of the general Rhodesian type. It is more confusing than it needs to be +if these cases outside western Europe are called neanderthaloids. They +lived during the same approximate time range but they were all somewhat +different-looking people. + + +EARLY MODERN MEN + +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 +early modern men. Now that Piltdown has been liquidated and Galley Hill +is known to be very late, what is left of the idea? + +The backs of the skulls of the Swanscombe and Steinheim finds look +rather modern. Unless you pay attention to the face and forehead of the +Steinheim find--which not many people have--and perhaps also consider +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 +interglacial, as did the pre-neanderthaloids. The crowns 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 +fossils, as modern type men, is little stronger than that for +Swanscombe, although Professor Vallois believes it a good case. + +It seems to add up to the fact that there were people living in +Europe--before the classic neanderthaloids--who looked more modern, +in some features, than the classic western neanderthaloids did. Our +best suggestion of what men looked like--just before they became fully +modern--comes from a cave on Mount Carmel in Palestine. + + +THE FIRST MODERNS + +Professor T. D. McCown and the late Sir Arthur Keith, who studied the +Mount Carmel bones, figured out that one of the two groups involved +was as much as 70 per cent modern. There were, in fact, two groups or +varieties of men in the Mount Carmel caves and in at least two other +Palestinian caves of about the same time. The time would be about that +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 +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 +group and the pre-neanderthaloid group came from exactly the same time. +The conclusion was quite natural: here was a population of men in the +act of evolving in two different directions. But the time may not be +exactly the same. It is very difficult to be precise, within say 10,000 +years, for a time some 75,000 years ago. If the Skhul men are in fact +later than the pre-neanderthaloid group of Palestine, as some of us +think, then they show how relatively modern some men were--men who +lived at the same time as the classic Neanderthalers of the European +pocket. + +Soon after the first extremely cold phase of the last glaciation, we +begin to get a number of bones of completely modern men in Europe. +We also get great numbers of the tools they made, and their living +places in caves. Completely modern skeletons begin turning up in caves +dating back to toward 40,000 years ago. The time is about that of the +beginning of the second phase of the last glaciation. These skeletons +belonged to people no different from many people we see today. Like +people today, not everybody looked alike. (The positions of the more +important fossil men of later Europe are shown in the chart on page +72.) + + +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 +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�). + +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 +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 +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 +not proper here. It is also not clear whether the features of the bones +claimed to be �Negroid� are really so at all. + +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 +the Bushmen seem to have once been very widespread south of the Sahara +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.� + +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. + + +WHAT THE DIFFERENCES MEAN + +What does all this difference mean? It means that, at one moment in +time, within each different area, men tended to look somewhat alike. +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 +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�). +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 +self-righteous nonsense when we try to kid ourselves into thinking that +they do. + +I am not going to struggle with the whole business of modern stocks and +races. This is a book about prehistoric men, not recent historic or +modern men. My physical anthropologist friends have been very patient +in helping me to write and rewrite this chapter--I am not going to +break their patience completely. Races are their business, not mine, +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 + population which differs significantly from other human populations + in regard to the frequency of one or more of the genes it + possesses.� + + Professor Sherwood L. Washburn, professor of Physical Anthropology, + 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.� + +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 +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 +race proper, which is a matter of the biology of groups. + +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 +it. Some unfortunate genetic trait soon turns up, so this has just as +carefully to be bred out again, and so on. + + +SUMMARY OF PRESENT KNOWLEDGE OF FOSSIL MEN + +The earliest bones of men we now have--upon which all the experts +would probably agree--are those of _Meganthropus_, from Java, of about +450,000 years ago. The earlier australopithecines of Africa were +possibly not tool-users and may not have been ancestral to men at all. +But there is an alternate and evidently increasingly stronger chance +that some of them may have been. The Kanam jaw from Kenya, another +early possibility, is not only very incomplete but its find-spot is +very questionable. + +Java man proper, _Pithecanthropus_, comes next, at about 400,000 years +ago, and the big Heidelberg jaw in Germany must be of about the same +date. Next comes Swanscombe in England, Steinheim in Germany, the +Ternafine jaws in Algeria, and Peking man, _Sinanthropus_. They all +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 +Heidelberg to the Neanderthalers where it became extinct, and (2) a +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 +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. +But on Mount Carmel, the Skhul cave-find with its 70 per cent modern +features shows what could happen elsewhere at the same time. + +Lastly, from about 40,000 or 35,000 years ago--the time of the onset +of the second phase of the last glaciation--we begin to find the fully +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. +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 +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 +little Cro-Magnon boy bragging to his friends about his tough, strong, +Neanderthaler great-great-great-great-grandfather! + + + + +Cultural BEGINNINGS + +[Illustration] + + +Men, unlike the lower animals, are made up of much more than flesh and +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 +whether or not he eats peas with his knife. + +The anthropologists use the word too, and argue heatedly over its finer +meanings; but they all agree that every human being is part of or has +some kind of culture. Each particular human group has a particular +culture; that is one of the ways in which we can tell one group of +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, +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 +generation to another. They are what really characterize different +human groups. + + +SOME CHARACTERISTICS OF CULTURE + +A culture lasts, although individual men in the group die off. On +the other hand, a culture changes as the different conventions and +understandings change. You could almost say that a culture lives in the +minds of the men who have it. But people are not born with it; they +get it as they grow up. Suppose a day-old Hungarian baby is adopted by +a family in Oshkosh, Wisconsin, and the child is not told that he is +Hungarian. He will grow up with no more idea of Hungarian culture than +anyone else in Oshkosh. + +So when I speak of ancient Egyptian culture, I mean the whole body +of understandings and beliefs and knowledge possessed by the ancient +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. + + +PREHISTORIC CULTURES + +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 +tell us something about the ways these men lived and the things they +believed. But the story we learn of the very early cultures must be +only a very small part of the whole, for we find so few things. The +rest of the story is gone forever. We have to do what we can with what +we find. + +For all of the time up to about 75,000 years ago, which was the time +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 +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 +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. + + +WHY LITTLE HAS LASTED FROM EARLY TIMES + +Except for the rare find-spots mentioned above, all our very early +finds come from geological deposits, or from the wind-blown surfaces +of deserts. Here is what the business of geological deposits really +means. Let us say that a group of people was living in England about +300,000 years ago. They made the tools they needed, lived in some sort +of camp, almost certainly built fires, and perhaps buried their dead. +While the climate was still warm, many generations may have lived in +the same place, hunting, and gathering nuts and berries; but after some +few thousand years, the weather began very gradually to grow colder. +These early Englishmen would not have known that a glacier was forming +over northern Europe. They would only have noticed that the animals +they hunted seemed to be moving south, and that the berries grew larger +toward the south. So they would have moved south, too. + +The camp site they left is the place we archeologists would really have +liked to find. All of the different tools the people used would have +been there together--many broken, some whole. The graves, and traces +of fire, and the tools would have been there. But the glacier got +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 +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 +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 +materials were dropped by the rushing waters as gravels, miles from +the front of the glacier. At last the glacier reached its greatest +extent; then it melted backward toward the north. Debris held in the +ice was dropped where the ice melted, or was flushed off by more melt +water. When the glacier, leaving the land, had withdrawn to the sea, +great hunks of ice were broken off as icebergs. These icebergs probably +dropped the materials held in their ice wherever they floated and +melted. There must be many tools and fragmentary bones of prehistoric +men on the bottom of the Atlantic Ocean and the North Sea. + +Remember, too, that these glaciers came and went at least three or four +times during the Ice Age. Then you will realize why the earlier things +we find are all mixed up. Stone tools from one camp site got mixed up +with stone tools from many other camp sites--tools which may have been +made tens of thousands or more years apart. The glaciers mixed them +all up, and so we cannot say which particular sets of tools belonged +together in the first place. + + +�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 +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 +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� +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.� + + +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 +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 +_standardization_. Here, men began to make tools according to certain +set traditions. Counting the better-made pebble tools, there are four +such traditions or sets of habits for the production of stone tools in +earliest prehistoric times. Toward the end of the Pleistocene, a fifth +tradition appears. + + +PEBBLE TOOLS + +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 +come from Africa. They date back to the time of the first great +alpine glaciation and are at least 500,000 years old. The earliest +ones are made of split pebbles, about the size of your fist or a bit +bigger. They go under the name of pebble tools. There are many natural +exposures of early Pleistocene geological beds in Africa, and the +prehistoric archeologists of south and central Africa have concentrated +on searching for early tools. Other finds of early pebble tools have +recently been made in Algeria and Morocco. + +[Illustration: SOUTH AFRICAN PEBBLE TOOL] + +There are probably early pebble tools to be found in areas of the +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� +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. + + +CORE-BIFACE 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. +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 +trimmed to a thin tip. The real purpose in trimming down the two faces +was to get a good cutting edge all around. You can see all this in the +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 +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 +be pointed, it seems likely that they were used for hitting, picking, +and prying. But they have rough cutting edges, so they could have been +used for chopping, scraping, and cutting. + + +FLAKE TOOLS + +The third tradition is the _flake_ tradition. The idea was to get a +tool with a good cutting edge by simply knocking a nice large flake off +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. + +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 +to have been to produce a broad, sharp, cutting edge. + +[Illustration: CLACTONIAN FLAKE] + +The core-biface and the flake traditions were spread, from earliest +times, over much of Europe, Africa, and western Asia. The map on page +52 shows the general area. Over much of this great region there was +flint. Both of these traditions seem well adapted to flint, although +good core-bifaces and flakes were made from other kinds of stone, +especially in Africa south of the Sahara. + + +CHOPPERS AND ADZE-LIKE TOOLS + +The fourth early tradition is found in southern and eastern Asia, from +northwestern India through Java and Burma into China. Father Maringer +recently reported an early group of tools in Japan, which most resemble +those of Java, called Patjitanian. The prehistoric men in this general +area mostly used quartz and tuff and even petrified wood for their +stone tools (see illustration, p. 46). + +This fourth early tradition is called the _chopper-chopping tool_ +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 +an ax is < shaped. + +[Illustration: ANYATHIAN ADZE-LIKE TOOL] + +There are also pointed pebble tools. Thus the tool kit of these early +south and east Asiatic peoples seems to have included tools for doing +as many different jobs as did the tools of the Western traditions. + +Dr. H. L. Movius has emphasized that the tools which were found in the +Peking cave with Peking man belong to the chopper-tool tradition. This +is the only case as yet where the tools and the man have been found +together from very earliest times--if we except Sterkfontein. + + +DIFFERENCES WITHIN THE TOOL-MAKING TRADITIONS + +The latter three great traditions in the manufacture of stone +tools--and the less clear-cut pebble tools before them--are all we have +to show of the cultures of the men of those times. Changes happened in +each of the traditions. As time went on, the tools in each tradition +were better made. There could also be slight regional differences in +the tools within one tradition. Thus, tools with small differences, but +all belonging to one tradition, can be given special group (facies) +names. + +This naming of special groups has been going on for some time. Here are +some of these names, since you may see them used in museum displays +of flint tools, or in books. Within each tradition of tool-making +(save the chopper tools), the earliest tool type is at the bottom +of the list, just as it appears in the lowest beds of a geological +stratification.[3] + + [3] Archeologists usually make their charts and lists with the + earliest materials at the bottom and the latest on top, since + this is the way they find them in the ground. + + Chopper tool (all about equally early): + Anyathian (Burma) + Choukoutienian (China) + Patjitanian (Java) + Soan (India) + + Flake: + �Typical Mousterian� + Levalloiso-Mousterian + Levalloisian + Tayacian + Clactonian (localized in England) + + Core-biface: + Some blended elements in �Mousterian� + Micoquian (= Acheulean 6 and 7) + Acheulean + Abbevillian (once called �Chellean�) + + Pebble tool: + Oldowan + Ain Hanech + pre-Stellenbosch + Kafuan + +The core-biface and the flake traditions appear in the chart (p. 65). + +The early archeologists had many of the tool groups named before they +ever realized that there were broader tool preparation traditions. This +was understandable, for in dealing with the mixture of things that come +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 +of the early traditions the various early groups belong. + + +THE MEANING OF THE DIFFERENT TRADITIONS + +What do the traditions really mean? I see them as the standardization +of ways to make tools for particular jobs. We may not know exactly what +job the maker of a particular core-biface or flake tool had in mind. We +can easily see, however, that he already enjoyed a know-how, a set of +persistent habits of tool preparation, which would always give him the +same type of tool when he wanted to make it. Therefore, the traditions +show us that persistent habits already existed for the preparation of +one type of tool or another. + +This tells us that one of the characteristic aspects of human culture +was already present. There must have been, in the minds of these +early men, a notion of the ideal type of tool for a particular job. +Furthermore, since we find so many thousands upon thousands of tools +of one type or another, the notion of the ideal types of tools _and_ +the know-how for the making of each type must have been held in common +by many men. The notions of the ideal types and the know-how for their +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 +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 +suppose that it is not impossible. + +There may, of course, be a real question whether tool-utilizing +creatures--our first step, on page 42--were actually men. Other +animals utilize things at hand as tools. The tool-fashioning creature +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 +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 +no longer preserved for the archeologist to find--I believe there would +be clear signs of further conventional understandings. The men may have +been physically primitive and pretty shaggy in appearance, but I think +we must surely call them men. + + +AN OLDER INTERPRETATION OF THE WESTERN TRADITIONS + +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 +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 +said to include Heidelberg, the proto-neanderthaloids and classic +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 +Piltdown liquidated, Galley Hill known to be very late, and Swanscombe +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 +archeological evidence was looked at in one short-sighted way, the +archeological materials _seemed_ to fit the parallel phyla theory. It +was simply necessary to believe that the flake tools were made only +by the paleoanthropic Neanderthaler line, and that the more handsome +core-biface tools were the product of the neanthropic modern-man line. + +Remember that _almost_ all of the early prehistoric European tools +came only from the redeposited gravel beds. This means that the tools +were not normally found in the remains of camp sites or work shops +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. +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 +think that the groups stand for something _on their own_. + +In 1906, M. Victor Commont actually made a rare find of what seems +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 +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 +ran heavily to flake tools. All in all, the parallel phyla theory +flourished because it seemed so neat and easy to understand. + + +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 +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 +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 +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. + +[Illustration: DISTRIBUTION OF TOOL-PREPARATION TRADITIONS + +Time approximately 100,000 years ago] + +There are certainly a few cases in which flake tools did appear with +few or no core-bifaces. The flake-tool group called Clactonian in +England is such a case. Another good, but certainly later case is +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 +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 +circumstances. + + +LIFE IN EARLIEST TIMES + +What do we actually know of life in these earliest times? In the +glacial gravels, or in the terrace gravels of rivers once swollen by +floods of melt water or heavy rains, or on the windswept deserts, we +find stone tools. The earliest and coarsest of these are the pebble +tools. We do not yet know what the men who made them looked like, +although the Sterkfontein australopithecines probably give us a good +hint. Then begin the more formal tool preparation traditions of the +west--the core-bifaces and the flake tools--and the chopper-chopping +tool series of the farther east. There is an occasional roughly worked +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� +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 +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 +mixture within the geological contexts. + +The stone tools of each of the earlier traditions are the simplest +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. + +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 +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 +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 +groups of three round stones on the site which suggest that the people +used bolas. The South American Indians used three-ball bolas, with the +stones in separate leather bags connected by thongs. These were whirled +and then thrown through the air so as to entangle the feet of a fleeing +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 +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 +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. + + +WHAT THE PEKING CAVE-FINDS TELL US + +The one early cave-dwelling we have found is that of Peking man, in +China. Peking man had fire. He probably cooked his meat, or used +the fire to keep dangerous animals away from his den. In the cave +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 +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 +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 +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 +eat the bone marrow, but he may have used some of them as tools. + +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 +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. + + * * * * * + +This rounds out about all we can say of the life and times of early +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, +and that these early people were really _men_. + + + + +MORE EVIDENCE of Culture + +[Illustration] + + +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 +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 +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. + + +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, +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 +thicker. Then there might have been a break in the occupation of the +cave for a while. Perhaps the game animals got scarce and the people +moved away; or maybe the cave became flooded. Later on, other people +moved in and began making a new layer of their own on top of the first +layer. Perhaps this process of layering went on in the same cave for a +hundred thousand years; you can see what happened. The drawing on this +page shows a section through such a cave. The earliest layer is on the +bottom, the latest one on top. They go in order from bottom to top, +earliest to latest. This is the _stratification_ we talked about (p. +12). + +[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 +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 +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 +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, +as a place in which people actually lived. + +The most important European cave sites are in Spain, France, and +central Europe; there are also sites in England and Italy. A few caves +are known in the Near East and Africa, and no doubt more sites will be +found when the out-of-the-way parts of Europe, Africa, and Asia are +studied. + + +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 +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 +(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.� +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. +Archeological context means finding things just as men left them. 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 +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 +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 +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 +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 +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, +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 +a European cave. There is so little of this material available that +we do not know which is really typical and which is not. You would +probably find it hard to see much difference between this industry and +a collection of tools of the type called Clactonian, taken from the +glacial gravels, especially if the Clactonian tools were small-sized. + +The stone industry of the bottommost layer of the Mount Carmel cave, +in Palestine, where somewhat similar tools were found, has also been +called Tayacian. + +I shall have to bring in many unfamiliar words for the names of the +industries. The industries are usually named after the places where +they were first found, and since these were in most cases in France, +most of the names which follow will be of French origin. However, +the names have simply become handles and are in use far beyond the +boundaries of France. It would be better if we had a non-place-name +terminology, but archeologists have not yet been able to agree on such +a terminology. + + +THE ACHEULEAN INDUSTRY + +Both in France and in Palestine, as well as in some African cave +sites, the next layers in the deep caves have an industry in both the +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 +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. + +The Acheulean core-biface type of tool is worked on two faces so as +to give a cutting edge all around. The outline of its front view may +be oval, or egg-shaped, or a quite pointed pear shape. The large +chip-scars of the Acheulean core-bifaces are shallow and flat. It is +suspected that this resulted from the removal of the chips with a +wooden club; the deep chip-scars of the earlier Abbevillian core-biface +came from beating the tool against a stone anvil. These tools are +really the best and also the final products of the core-biface +tradition. We first noticed the tradition in the early glacial gravels +(p. 43); now we see its end, but also its finest examples, in the +deeper cave levels. + +The flake tools, which really make up the greater bulk of this +industry, are simple scrapers and chips with sharp cutting edges. The +habits used to prepare them must have been pretty much the same as +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 +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 +animal bones are those of beasts that live in a warm climate. They +belonged in the last interglacial period. We have not yet found the +bones of fossil men definitely in place with this industry. + +[Illustration: ACHEULEAN BIFACE] + + +FLAKE INDUSTRIES FROM THE CAVES + +Two more stone industries--the _Levalloisian_ and the +�_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� +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 +there may be some kinds of Acheulean industries that are later than +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 +of the two traditions--core-biface or flake--or on combinations of +the habits used in the preparation techniques of both traditions. All +were popular at about the same time. So we find that people who made +one kind of stone tool industry lived in a cave for a while. Then they +gave up the cave for some reason, and people with another industry +moved in. Then the first people came back--or at least somebody with +the same tool-making habits as the first people. Or maybe a third group +of tool-makers moved in. The people who had these different habits for +making their stone tools seem to have moved around a good deal. They no +doubt borrowed and exchanged tricks of the trade with each other. There +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 +method of great importance for understanding these tool preparation +habits. + + +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 +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 +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 +points and side scrapers are illustrated (pp. 64 and 66). + +[Illustration: LEVALLOIS FLAKE] + + +THE MIXING OF TRADITIONS + +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� +systematic work is beginning to clear up some of our confusion. + +The time of these late Acheuleo-Levalloiso-Mousterioid industries +is from perhaps as early as 100,000 years ago. It may have lasted +until well past 50,000 years ago. This was the time of the first +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 +of industries with tools of both core-biface and flake habits, and +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). + +[Illustration: MOUSTERIAN POINT] + + +OTHER SUGGESTIONS OF LIFE IN THE EARLY CAVE LAYERS + +Except for the stone tools, what do we know of the way men lived in the +time range after 100,000 to perhaps 40,000 years ago or even later? +We know that in the area from Europe to Palestine, at least some of +the people (some of the time) lived in the fronts of caves and warmed +themselves over fires. In Europe, in the cave layers of these times, +we find the bones of different animals; the bones in the lowest layers +belong to animals that lived in a warm climate; above them are the +bones of those who could stand the cold, like the reindeer and mammoth. +Thus, the meat diet must have been changing, as the glacier crept +farther south. Shells and possibly fish bones have lasted in these +cave layers, but there is not a trace of the vegetable foods and the +nuts and berries and other wild fruits that must have been eaten when +they could be found. + +[Illustration: CHART SHOWING PRESENT UNDERSTANDING OF RELATIONSHIPS AND +SUCCESSION OF TOOL-PREPARATION TRADITIONS, INDUSTRIES, AND ASSEMBLAGES +OF WEST-CENTRAL EUROPE + +Wavy lines indicate transitions in industrial habits. These transitions +are not yet understood in detail. The glacial and climatic scheme shown +is the alpine one.] + +Bone tools have also been found from this period. Some are called +scrapers, and there are also long chisel-like leg-bone fragments +believed to have been used for skinning animals. Larger hunks of bone, +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. + +[Illustration: MOUSTERIAN SIDE SCRAPER] + +There is a small but certain number of cases of intentional burials. +These burials have been found on the floors of the caves; in other +words, the people dug graves in the places where they lived. The holes +made for the graves were small. For this reason (or perhaps for some +other?) the bodies were in a curled-up or contracted position. Flint or +bone tools or pieces of meat seem to have been put in with some of the +bodies. In several cases, flat stones had been laid over the graves. + + +TOOLS FROM AFRICA AND ASIA ABOUT 100,000 YEARS AGO + +Professor Movius characterizes early prehistoric Africa as a continent +showing a variety of stone industries. Some of these industries were +purely local developments and some were practically identical with +industries found in Europe at the same time. From northwest Africa +to Capetown--excepting the tropical rain forest region of the west +center--tools of developed Acheulean, Levalloisian, and Mousterian +types have been recognized. Often they are named after African place +names. + +In east and south Africa lived people whose industries show a +development of the Levalloisian technique. Such industries are +called Stillbay. Another industry, developed on the basis of the +Acheulean technique, is called Fauresmith. From the northwest comes +an industry with tanged points and flake-blades; this is called the +Aterian. The tropical rain forest region contained people whose stone +tools apparently show adjustment to this peculiar environment; the +so-called Sangoan industry includes stone picks, adzes, core-bifaces +of specialized Acheulean type, and bifacial points which were probably +spearheads. + +In western Asia, even as far as the east coast of India, the tools of +the Eurafrican core-biface and flake tool traditions continued to be +used. But in the Far East, as we noted in the last chapter, men had +developed characteristic stone chopper and chopping tools. This tool +preparation tradition--basically a pebble tool tradition--lasted to the +very end of the Ice Age. + +When more intact open air sites such as that of an earlier time at +Olorgesailie, and more stratified cave sites are found and excavated +in Asia and Africa, we shall be able to get a more complete picture. +So far, our picture of the general cultural level of the Old World at +about 100,000 years ago--and soon afterwards--is best from Europe, but +it is still far from complete there, too. + + +CULTURE AT THE BEGINNING OF THE LAST GREAT GLACIAL PERIOD + +The few things we have found must indicate only a very small part +of the total activities of the people who lived at the time. All of +the things they made of wood and bark, of skins, of anything soft, +are gone. The fact that burials were made, at least in Europe and +Palestine, is pretty clear proof that the people had some notion of a +life after death. But what this notion really was, or what gods (if +any) men believed in, we cannot know. Dr. Movius has also reminded me +of the so-called bear cults--cases in which caves have been found which +contain the skulls of bears in apparently purposeful arrangement. This +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 +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 +growing, and how there may have been witch doctors who made beginnings +in medicine or in art, in the materials they gathered for their trade. + +The stone tools help us most. They have lasted, and we can find +them. As they come to us, from this cave or that, and from this +layer or that, the tool industries show a variety of combinations +of the different basic habits or traditions of tool preparation. +This seems only natural, as the groups of people must have been very +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. +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 +of men to adapt themselves to a variety of conditions. We shall see +this adaptive ability even more clearly as time goes on and the record +becomes more complete. + +Over how great an area did these loosely related cultures reach in +the time 75,000 to 45,000 or even as late as 35,000 years ago? We +have described stone tools made in one or another of the flake and +core-biface habits, for an enormous area. It covers all of Europe, all +of Africa, the Near East, and parts of India. It is perfectly possible +that the flake and core-biface habits lasted on after 35,000 years ago, +in some places outside of Europe. In northern Africa, for example, we +are certain that they did (see chart, p. 72). + +On the other hand, in the Far East (China, Burma, Java) and in northern +India, the tools of the old chopper-tool tradition were still being +made. Out there, we must assume, there was a different set of loosely +related cultures. At least, there was a different set of loosely +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 +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 +Teshik Tash, in Trans-caspian Russia. It is still too soon to know +whether certain combinations of tools within industries were made +only by certain physical types of men. But since tools of both the +core-biface and the flake traditions, and their blends, turn up from +South Africa to England to India, it is most unlikely that only one +type of man used only one particular habit in the preparation of tools. +What seems perfectly clear is that men in Africa and men in India were +making just as good tools as the men who lived in western Europe. + + + + +EARLY MODERNS + +[Illustration] + + +From some time during the first inter-stadial of the last great +glaciation (say some time after about 40,000 years ago), we have +more accurate dates for the European-Mediterranean area and less +accurate ones for the rest of the Old World. This is probably +because the effects of the last glaciation have been studied in the +European-Mediterranean area more than they have been elsewhere. + + +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 +for dates is very inaccurate for the areas outside of Europe and the +Mediterranean. + +We can at least make a guess. In Egypt and north Africa, men were still +using the old methods of making stone tools. This was especially true +of flake tools of the Levalloisian type, save that they were growing +smaller and smaller as time went on. But at the same time, a new +tradition was becoming popular in westernmost Asia and in Europe. This +was the blade-tool tradition. + + +BLADE TOOLS + +A stone blade is really just a long parallel-sided flake, as the +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 +which to whack off blades. + +[Illustration: PLAIN BLADE] + +You whack with a hammer stone against a bone or antler punch which is +directed at the proper place on the blade-core. The blade-core has to +be well supported or gripped while this is going on. To get a good +flint blade tool takes a great deal of know-how. + +Remember that a tradition in stone tools means no more than that some +particular way of making the tools got started and lasted a long time. +Men who made some tools in one tradition or set of habits would also +make other tools for different purposes by means of another tradition +or set of habits. It was even possible for the two sets of habits to +become combined. + + +THE EARLIEST BLADE TOOLS + +The oldest blade tools we have found were deep down in the layers of +the Mount Carmel caves, in Tabun Eb and Ea. Similar tools have been +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. +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. + +[Illustration: SUCCESSION OF ICE AGE FLINT TYPES, INDUSTRIES, AND +ASSEMBLAGES, AND OF FOSSIL MEN, IN NORTHWESTERN EURAFRASIA] + +For some reason, the habit of making blades in Palestine and Syria was +interrupted. Blades only reappeared there at about the same time they +were first made in Europe, some time after 45,000 years ago; that is, +after the first phase of the last glaciation was ended. + +[Illustration: BACKED BLADE] + +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 +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 +the blade tools reappear in the Syro-Palestinian area, they do so in +industries which also include Levalloiso-Mousterian flake tools. From +the point of view of form and workmanship, the blade tools themselves +are not so fine as those which seem to be making their appearance +in western Europe about the same time. There is a characteristic +Syro-Palestinian flake point, possibly a projectile tip, called the +Emiran, which is not known from Europe. The appearance of blade tools, +together with Levalloiso-Mousterian flakes, continues even after the +Emiran point has gone out of use. + +It seems clear that the production of blade tools did not immediately +swamp the set of older habits in Europe, too; the use of flake +tools also continued there. This was not so apparent to the older +archeologists, whose attention was focused on individual tool types. It +is not, in fact, impossible--although it is certainly not proved--that +the technique developed in the preparation of the Levalloisian tortoise +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 +having enough individual character to be classified as distinct ... +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 +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. + +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 +complication in what we find. The chart (p. 72) tries to simplify +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 +simplified. + +You will realize that all this complication comes not only from +the fact that we are finding more material. It is due also to the +increasing ability of men to adapt themselves to a great variety of +situations. Their tools indicate this adaptiveness. We know there was +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 +increasing ability to adapt themselves. + + +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 + 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 + 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 + and bone; that is, to make handles or shafts for other tools. + They must also be the tools with which much of the engraving on + bone (see p. 83) was done. There is a bewildering variety of + different kinds of burins. + +[Illustration: TANGED POINT] + + 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 + either one or two shoulders. + +[Illustration: NOTCHED BLADE] + + 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. + Our spoke-shaves have sharp straight cutting blades and really + �shave.� Notched blades of flint probably scraped rather than cut. + + 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 + 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). + +There is one very special type of flint tool, which is best known from +western Europe in an industry called the Solutrean. These tools were +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 + dagger-like, and must have been used as knives or daggers. Others + 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 + types are illustrated (see above and p. 79). + +[Illustration: END-SCRAPER ON A BLADE] + +[Illustration: LAUREL LEAF POINT] + +The industries characterized by tools in the blade tradition also +yield some flake and core tools. We will end this list with two types +of tools that appear at this time. The first is made of a flake; the +second is a core tool. + +[Illustration: SHOULDERED POINT] + + 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) + 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 + 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 + large enough to be easily grasped, and probably were used like + push planes (see illustration, p. 81). + +[Illustration: KEEL-SHAPED ROUND SCRAPER] + +[Illustration: KEEL-SHAPED NOSED SCRAPER] + +The stone tools (usually made of flint) we have just listed are among +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 +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 +layer. + + +OTHER KINDS OF TOOLS + +By this time in Europe--say from about 40,000 to about 10,000 years +ago--we begin to find other kinds of material too. Bone tools begin +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. + +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 +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. + +[Illustration: SPLIT-BASED BONE POINT] + +[Illustration: SPEAR-THROWER] + +[Illustration: BONE HARPOON] + +Antler came into use for tools, especially in central and western +Europe. We do not know the use of one particular antler tool that +has a large hole bored in one end. One suggestion is that it was +a thong-stropper used to strop or work up hide thongs (see +illustration, below); another suggestion is that it was an arrow-shaft +straightener. + +Another interesting tool, usually of antler, is the spear-thrower, +which is little more than a stick with a notch or hook on one end. +The hook fits into the butt end of the spear, and the length of the +spear-thrower allows you to put much more power into the throw (p. +82). It works on pretty much the same principle as the sling. + +Very fancy harpoons of antler were also made in the latter half of +the period in western Europe. These harpoons had barbs on one or both +sides and a base which would slip out of the shaft (p. 82). Some have +engraved decoration. + + +THE BEGINNING OF ART + +[Illustration: THONG-STROPPER] + +In western Europe, at least, the period saw the beginning of several +kinds of art work. It is handy to break the art down into two great +groups: the movable art, and the cave paintings and sculpture. The +movable art group includes the scratchings, engravings, and modeling +which decorate tools and weapons. Knives, stroppers, spear-throwers, +harpoons, and sometimes just plain fragments of bone or antler are +often carved. There is also a group of large flat pebbles which seem +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] + +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 +meant to show the great forces of nature--fertility and the birth of +life. + + +CAVE PAINTINGS + +In the paintings on walls and ceilings of caves we have some examples +that compare with the best art of any time. The subjects were usually +animals, the great cold-weather beasts of the end of the Ice Age: the +mammoth, the wooly rhinoceros, the bison, the reindeer, the wild horse, +the bear, the wild boar, and wild cattle. As in the movable art, there +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 +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 +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 +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. + +The cave art of the Franco-Cantabrian style is one of the great +artistic achievements of all time. The subjects drawn are almost always +the larger animals of the time: the bison, wild cattle and horses, the +wooly rhinoceros, the mammoth, the wild boar, and the bear. In some of +the best examples, the beasts are drawn in full color and the paintings +are remarkably alive and charged with energy. They come from the hands +of men who knew the great animals well--knew the feel of their fur, the +tremendous drive of their muscles, and the danger one faced when he +hunted them. + +Another artistic style has been found in eastern Spain. It includes +lively drawings, often of people hunting with bow and arrow. The East +Spanish art is found on open rock faces and in rock-shelters. It is +less spectacular and apparently more recent than the Franco-Cantabrian +cave art. + + +LIFE AT THE END OF THE ICE AGE IN EUROPE + +Life in these times was probably as good as a hunter could expect it +to be. Game and fish seem to have been plentiful; berries and wild +fruits probably were, too. From France to Russia, great pits or +piles of animal bones have been found. Some of this killing was done +as our Plains Indians killed the buffalo--by stampeding them over +steep river banks or cliffs. There were also good tools for hunting, +however. In western Europe, people lived in the openings of caves and +under overhanging rocks. On the great plains of eastern Europe, very +crude huts were being built, half underground. The first part of this +time must have been cold, for it was the middle and end phases of the +last great glaciation. Northern Europe from Scotland to Scandinavia, +northern Germany and Russia, and also the higher mountains to the +south, were certainly covered with ice. But people had fire, and the +needles and tools that were used for scraping hides must mean that they +wore clothing. + +It is clear that men were thinking of a great variety of things beside +the tools that helped them get food and shelter. Such burials as we +find have more grave-gifts than before. Beads and ornaments and often +flint, bone, or antler tools are included in the grave, and sometimes +the body is sprinkled with red ochre. Red is the color of blood, which +means life, and of fire, which means heat. Professor Childe wonders if +the red ochre was a pathetic attempt at magic--to give back to the body +the heat that had gone from it. But pathetic or not, it is sure proof +that these people were already moved by death as men still are moved by +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 +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 +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, +and were provided with shell beads and ornaments and with some blade +tools of flint. Regardless of the different names once given them by +the human paleontologists, each of these groups seems to have shared +equally in the cultural achievements of the times, for all that the +archeologists can say. + + +MICROLITHS + +One peculiar set of tools seems to serve as a marker for the very last +phase of the Ice Age in southwestern Europe. This tool-making habit is +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, +and several other geometric forms. These little tools are called +�microliths.� They are so small that most of them must have been fixed +in handles or shafts. + +[Illustration: MICROLITHS + + BLADE FRAGMENT + BURIN + LUNATE + TRAPEZOID + SCALENE TRIANGLE + ARROWHEAD] + +We have found several examples of microliths mounted in shafts. In +northern Europe, where their use soon spread, the microlithic triangles +or lunates were set in rows down each side of a bone or wood point. +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 +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 +fleet-footed animals like the gazelle, which might run for miles after +being shot with an arrow, it was an advantage to cause as much bleeding +as possible, for the animal would drop sooner. + +We are not really sure where the microliths were first invented. There +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. + +Remember that the microliths we are talking about here were made from +carefully prepared little blades, and are often geometric in outline. +Each microlithic industry proper was made up, in good part, of such +tiny blade tools. But there were also some normal-sized blade tools and +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 +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. + + +LATER BLADE-TOOL INDUSTRIES OF THE NEAR EAST AND AFRICA + +The blade-tool industries of normal size we talked about earlier spread +from Europe to central Siberia. We noted that blade tools were made +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 +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 +example; its industry includes a long spike-like blade point which has +no western European counterpart. + +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 +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 +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. + + +NETHER AFRICA + +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.� +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 +time. + + +THE FAR EAST + +India and the Far East still seem to be going their own way. In India, +some blade tools have been found. These are not well dated, save that +we believe they must be post-Pleistocene. In the Far East it looks as +if the old chopper-tool tradition was still continuing. For Burma, +Dr. Movius feels this is fairly certain; for China he feels even more +certain. Actually, we know very little about the Far East at about the +time of the last glaciation. This is a shame, too, as you will soon +agree. + + +THE NEW WORLD BECOMES INHABITED + +At some time toward the end of the last great glaciation--almost +certainly after 20,000 years ago--people began to move over Bering +Strait, from Asia into America. As you know, the American Indians have +been assumed to be basically Mongoloids. New studies of blood group +types make this somewhat uncertain, but there is no doubt that the +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 +found here. + +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 +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 +spreading up that far. + + +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 +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 +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 +begun. We shall see further intensifications of it in the next chapter. + + + + +End and PRELUDE + +[Illustration] + + +Up to the end of the last glaciation, we prehistorians have a +relatively comfortable time schedule. The farther back we go the less +exact we can be about time and details. Elbow-room of five, ten, +even fifty or more thousands of years becomes available for us to +maneuver in as we work backward in time. But now our story has come +forward to the point where more exact methods of dating are at hand. +The radioactive carbon method reaches back into the span of the last +glaciation. There are other methods, developed by the geologists and +paleobotanists, which supplement and extend the usefulness of the +radioactive carbon dates. And, happily, as our means of being more +exact increases, our story grows more exciting. There are also more +details of culture for us to deal with, which add to the interest. + + +CHANGES AT THE END OF THE ICE AGE + +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 +always changing for the better; but there was at least one time when +European weather was even better than it is now. + +The melting back of the glaciers and the weather fluctuations caused +other changes, too. We know a fair amount about these changes in +Europe. In an earlier chapter, we said that the whole Ice Age was a +matter of continual change over long periods of time. As the last +glaciers began to melt back some interesting things happened to mankind. + +In Europe, along with the melting of the last glaciers, geography +itself was changing. Britain and Ireland had certainly become islands +by 5000 B.C. The Baltic was sometimes a salt sea, sometimes a large +fresh-water lake. Forests began to grow where the glaciers had been, +and in what had once been the cold tundra areas in front of the +glaciers. The great cold-weather animals--the mammoth and the wooly +rhinoceros--retreated northward and finally died out. It is probable +that the efficient hunting of the earlier people of 20,000 or 25,000 +to about 12,000 years ago had helped this process along (see p. 86). +Europeans, especially those of the post-glacial period, had to keep +changing to keep up with the times. + +The archeological materials for the time from 10,000 to 6000 B.C. seem +simpler than those of the previous five thousand years. The great cave +art of France and Spain had gone; so had the fine carving in bone and +antler. Smaller, speedier animals were moving into the new forests. New +ways of hunting them, or ways of getting other food, had to be found. +Hence, new tools and weapons were necessary. Some of the people who +moved into northern Germany were successful reindeer hunters. Then the +reindeer moved off to the north, and again new sources of food had to +be found. + + +THE READJUSTMENTS COMPLETED IN EUROPE + +After a few thousand years, things began to look better. Or at least +we can say this: By about 6000 B.C. we again get hotter archeological +materials. The best of these come from the north European area: +Britain, Belgium, Holland, Denmark, north Germany, southern Norway and +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 + archeological materials belonging together, from one area and of + 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 + 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, + canals, dams, irrigation ditches, and any other traces we might + leave of ourselves, from graves to garbage dumps. Not everything + would last, so that an archeologist digging us up--say 2,000 + years from now--would find only the most durable items in our + assemblage. + +The best known of these assemblages is the _Maglemosian_, named after a +great Danish peat-swamp where much has been found. + +[Illustration: SKETCH OF MAGLEMOSIAN ASSEMBLAGE + + CHIPPED STONE + HEMP + GROUND STONE + BONE AND ANTLER + WOOD] + +In the Maglemosian assemblage the flint industry was still very +important. Blade tools, tanged arrow points, and burins were still +made, but there were also axes for cutting the trees in the new +forests. Moreover, the tiny microlithic blades, in a variety of +geometric forms, are also found. Thus, a specialized tradition that +possibly began east of the Mediterranean had reached northern Europe. +There was also a ground stone industry; some axes and club-heads were +made by grinding and polishing rather than by chipping. The industries +in bone and antler show a great variety of tools: axes, fish-hooks, +fish spears, handles and hafts for other tools, harpoons, and clubs. +A remarkable industry in wood has been preserved. Paddles, sled +runners, handles for tools, and bark floats for fish-nets have been +found. There are even fish-nets made of plant fibers. Canoes of some +kind were no doubt made. Bone and antler tools were decorated with +simple patterns, and amber was collected. Wooden bows and arrows are +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 +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, +their assemblage shows us again that remarkable ability of men to adapt +themselves to change. They had succeeded in domesticating the dog; he +was still a very wolf-like dog, but his long association with mankind +had now begun. Professor Coon believes that these people were direct +descendants of the men of the glacial age and that they had much the +same appearance. He believes that most of the Ice Age survivors still +extant are living today in the northwestern European area. + + +SOUTH AND CENTRAL EUROPE PERHAPS AS READJUSTED AS THE NORTH + +There is always one trouble with things that come from areas where +preservation is exceptionally good: The very quantity of materials in +such an assemblage tends to make things from other areas look poor +and simple, although they may not have been so originally at all. The +assemblages of the people who lived to the south of the Maglemosian +area may also have been quite large and varied; but, unfortunately, +relatively little of the southern assemblages has lasted. The +water-logged sites of the Maglemosian area preserved a great deal +more. Hence the Maglemosian itself _looks_ quite advanced to us, when +we compare it with the few things that have happened to last in other +areas. If we could go back and wander over the Europe of eight thousand +years ago, we would probably find that the peoples of France, central +Europe, and south central Russia were just as advanced as those of the +north European-Baltic belt. + +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 +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. + + +HOW MUCH REAL CHANGE WAS THERE? + +You can see that no really _basic_ change in the way of life has yet +been described. Childe sees the problem that faced the Europeans of +10,000 to 3000 B.C. as a problem in readaptation to the post-glacial +forest environment. By 6000 B.C. some quite successful solutions of +the problem--like the Maglemosian--had been made. The upsets that came +with the melting of the last ice gradually brought about all sorts of +changes in the tools and food-getting habits, but the people themselves +were still just as much simple hunters, fishers, and food-collectors as +they had been in 25,000 B.C. It could be said that they changed just +enough so that they would not have to change. But there is a bit more +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 +were able to adapt themselves quickly when the next change did come. +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 +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, +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 +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 +the microliths but I do not know of one. Remember, too, that as yet +we can only use the microliths as a marker in Europe and about the +Mediterranean. + + +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 +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 +greater part of Africa and Asia, we do not know exactly what was +happening. Some day, we shall no doubt find out; today we are without +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 +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 +collecting in new ways to fit the post-glacial environment, something +completely new was being made ready in western Asia. + +Unfortunately, we do not have as much understanding of the climate and +environment of the late Ice Age in western Asia as we have for most +of Europe. Probably the weather was never so violent or life quite +so rugged as it was in northern Europe. We know that the microliths +made their appearance in western Asia at least by 10,000 B.C. and +possibly earlier, marking the beginning of the terminal phase of +food-collecting. Then, gradually, we begin to see the build-up towards +the first _basic change_ in human life. + +This change amounted to a revolution just as important as the +Industrial Revolution. In it, men first learned to domesticate +plants and animals. They began _producing_ their food instead of +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 +indeed a revolution. + + +QUESTIONS ABOUT CAUSE + +We do not yet know _how_ and _why_ this great revolution took place. We +are only just beginning to put the questions properly. I suspect the +answers will concern some delicate and subtle interplay between man and +nature. Clearly, both the level of culture and the natural condition of +the environment must have been ready for the great change, before the +change itself could come about. + +It is going to take years of co-operative field work by both +archeologists and the natural scientists who are most helpful to them +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 +environmental changes with the coming of post-glacial times. + +In western Asia, we archeologists can prove that the food-producing +revolution actually took place. We can see _the_ important consequence +of effective domestication of plants and animals in the appearance of +the settled village-farming community. And within the village-farming +community was the seed of civilization. The way in which effective +domestication of plants and animals came about, however, must also be +linked closely with the natural environment. Thus the archeologists +will not solve the _how_ and _why_ questions alone--they will need the +help of interested natural scientists in the field itself. + + +PRECONDITIONS FOR THE REVOLUTION + +Especially at this point in our story, we must remember how culture and +environment go hand in hand. Neither plants nor animals domesticate +themselves; men domesticate them. Furthermore, men usually domesticate +only those plants and animals which are useful. There is a good +question here: What is cultural usefulness? But I shall side-step it to +save time. Men cannot domesticate plants and animals that do not exist +in the environment where the men live. Also, there are certainly some +animals and probably some plants that resist domestication, although +they might be useful. + +This brings me back again to the point that _both_ the level of culture +and the natural condition of the environment--with the proper plants +and animals in it--must have been ready before domestication could +have happened. But this is precondition, not cause. Why did effective +food-production happen first in the Near East? Why did it happen +independently in the New World slightly later? Why also in the Far +East? Why did it happen at all? Why are all human beings not still +living as the Maglemosians did? These are the questions we still have +to face. + + +CULTURAL �RECEPTIVENESS� AND PROMISING ENVIRONMENTS + +Until the archeologists and the natural scientists--botanists, +geologists, zoologists, and general ecologists--have spent many more +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, +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. + +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 +natural conditions most promising? Did this promise include plants and +animals which could be domesticated, or did it only offer new ways of +food-collecting? There is much work to do on this problem, but we are +beginning to get some general hints. + +Before I begin to detail the hints we now have from western Asia, I +want to do two things. First, I shall tell you of an old theory as to +how food-production might have appeared. Second, I will bother you with +some definitions which should help us in our thinking as the story goes +on. + + +AN OLD THEORY AS TO THE CAUSE OF THE REVOLUTION + +The idea that change would result, if the balance between nature +and culture became upset, is of course not a new one. For at least +twenty-five years, there has been a general theory as to _how_ the +food-producing revolution happened. This theory depends directly on the +idea of natural change in the environment. + +The five thousand years following about 10,000 B.C. must have been +very difficult ones, the theory begins. These were the years when +the most marked melting of the last glaciers was going on. While the +glaciers were in place, the climate to the south of them must have been +different from the climate in those areas today. You have no doubt read +that people once lived in regions now covered by the Sahara Desert. +This is true; just when is not entirely clear. The theory is that +during the time of the glaciers, there was a broad belt of rain winds +south of the glaciers. These rain winds would have kept north Africa, +the Nile Valley, and the Middle East green and fertile. But when the +glaciers melted back to the north, the belt of rain winds is supposed +to have moved north too. Then the people living south and east of the +Mediterranean would have found that their water supply was drying up, +that the animals they hunted were dying or moving away, and that the +plant foods they collected were dried up and scarce. + +According to the theory, all this would have been true except in the +valleys of rivers and in oases in the growing deserts. Here, in the +only places where water was left, the men and animals and plants would +have clustered. They would have been forced to live close to one +another, in order to live at all. Presently the men would have seen +that some animals were more useful or made better food than others, +and so they would have begun to protect these animals from their +natural enemies. The men would also have been forced to try new plant +foods--foods which possibly had to be prepared before they could be +eaten. Thus, with trials and errors, but by being forced to live close +to plants and animals, men would have learned to domesticate them. + + +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. +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 +began melting after 12,000 years ago was merely the last sub-phase of +the last great glaciation. There had also been three earlier periods +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 +earlier, then? + +Furthermore, it does not seem to be in the oases and river valleys +that we have our first or only traces of either food-production +or the earliest farming villages. These traces are also in the +hill-flanks of the mountains of western Asia. Our earliest sites of the +village-farmers do not seem to indicate a greatly different climate +from that which the same region now shows. In fact, everything we now +know suggests that the old theory was just too simple an explanation to +have been the true one. The only reason I mention it--beyond correcting +the ideas you may get in the general texts--is that it illustrates the +kind of thinking we shall have to do, even if it is doubtless wrong in +detail. + +We archeologists shall have to depend much more than we ever have on +the natural scientists who can really help us. I can tell you this from +experience. I had the great good fortune to have on my expedition staff +in Iraq in 1954-55, a geologist, a botanist, and a zoologist. Their +studies added whole new bands of color to my spectrum of thinking about +_how_ and _why_ the revolution took place and how the village-farming +community began. But it was only a beginning; as I said earlier, we are +just now learning to ask the proper questions. + + +ABOUT STAGES AND ERAS + +Now come some definitions, so I may describe my material more easily. +Archeologists have always loved to make divisions and subdivisions +within the long range of materials which they have found. They often +disagree violently about which particular assemblage of material +goes into which subdivision, about what the subdivisions should be +named, about what the subdivisions really mean culturally. Some +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. + +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 +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. +91). + +Now, while I think we could make several possible subdivisions of the +food-gathering stage--I call my subdivisions of stages _eras_[5]--I +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 +post-glacial environment in northwestern Europe. We talked about +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 + 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 + _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 + allow it. + +The food-producing revolution ushers in the _food-producing stage_. +This stage began to be replaced by the _industrial stage_ only about +two hundred years ago. Now notice that my stage divisions are in terms +of technology and economics. We must think sharply to be sure that the +subdivisions of the stages, the eras, are in the same terms. This does +not mean that I think technology and economics are the only important +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 +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 +a universal historian. + + +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 +of these sites themselves, and the remarkable growth in human +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 +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� +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 +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 +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 +dog. In this sense, the whole era of food-collecting was becoming ready +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 +place in one of those natural environments of promise, where a variety +of plants and animals, each possible of domestication, was available in +the wild state. Let me spell this out. Really effective food-production +must include a variety of items to make up a reasonably well-rounded +diet. The food-supply so produced must be trustworthy, even though +the food-producing peoples themselves might be happy to supplement +it with fish and wild strawberries, just as we do when such things +are available. So, as we said earlier, part of our problem is that +of finding a region with a natural environment which includes--and +did include, some ten thousand years ago--a variety of possibly +domesticable wild plants and animals. + + +NUCLEAR AREAS + +Now comes the last of my definitions. A region with a natural +environment which included a variety of wild plants and animals, +both possible and ready for domestication, would be a central +or core or _nuclear area_, that is, it would be when and _if_ +food-production took place within it. It is pretty hard for me to +imagine food-production having ever made an independent start outside +such a nuclear area, although there may be some possible nuclear areas +in which food-production never took place (possibly in parts of Africa, +for example). + +We know of several such nuclear areas. In the New World, Middle America +and the Andean highlands make up one or two; it is my understanding +that the evidence is not yet clear as to which. There seems to have +been a nuclear area somewhere in southeastern Asia, in the Malay +peninsula or Burma perhaps, connected with the early cultivation of +taro, breadfruit, the banana and the mango. Possibly the cultivation +of rice and the domestication of the chicken and of zebu cattle and +the water buffalo belong to this southeast Asiatic nuclear area. We +know relatively little about it archeologically, as yet. The nuclear +area which was the scene of the earliest experiment in effective +food-production was in western Asia. Since I know it best, I shall use +it as my example. + + +THE NUCLEAR NEAR EAST + +The nuclear area of western Asia is naturally the one of greatest +interest to people of the western cultural tradition. Our cultural +heritage began within it. The area itself is the region of the hilly +flanks of rain-watered grass-land which build up to the high mountain +ridges of Iran, Iraq, Turkey, Syria, and Palestine. The map on page +125 indicates the region. If you have a good atlas, try to locate the +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 +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 +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 +to Turkey, the Taurus Mountains of southern Turkey, and the Zagros +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 +rain to the foothills of the Taurus and the Zagros. + +The picture I hope you will have from this description is that of an +intermediate hilly-flanks zone lying between two regions of extremes. +The lower Tigris-Euphrates basin land is low and far too dry and hot +for agriculture based on rainfall alone; to the south and southwest, it +merges directly into the great desert of Arabia. The mountains which +lie above the hilly-flanks zone are much too high and rugged to have +encouraged farmers. + + +THE NATURAL ENVIRONMENT OF THE NUCLEAR NEAR EAST + +The more we learn of this hilly-flanks zone that I describe, the +more it seems surely to have been a nuclear area. This is where we +archeologists need, and are beginning to get, the help of natural +scientists. They are coming to the conclusion that the natural +environment of the hilly-flanks zone today is much as it was some eight +to ten thousand years ago. There are still two kinds of wild wheat and +a wild barley, and the wild sheep, goat, and pig. We have discovered +traces of each of these at about nine thousand years ago, also traces +of wild ox, horse, and dog, each of which appears to be the probable +ancestor of the domesticated form. In fact, at about nine thousand +years ago, the two wheats, the barley, and at least the goat, were +already well on the road to domestication. + +The wild wheats give us an interesting clue. They are only available +together with the wild barley within the hilly-flanks zone. While the +wild barley grows in a variety of elevations and beyond the zone, +at least one of the wild wheats does not seem to grow below the hill +country. As things look at the moment, the domestication of both the +wheats together could _only_ have taken place within the hilly-flanks +zone. Barley seems to have first come into cultivation due to its +presence as a weed in already cultivated wheat fields. There is also +a suggestion--there is still much more to learn in the matter--that +the animals which were first domesticated were most at home up in the +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 +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 +stage, that of incipient cultivation and animal domestication. + + +THE ERA OF INCIPIENT CULTIVATION AND ANIMAL DOMESTICATION + +I said above (p. 105) that my era of incipient cultivation and animal +domestication is mainly set up by playing a hunch. Although we cannot +really demonstrate it--and certainly not in the Near East--it would +be very strange for food-collectors not to have known a great deal +about the plants and animals most useful to them. They do seem to have +domesticated the dog. We can easily imagine them remembering to go +back, season after season, to a particular patch of ground where seeds +or acorns or berries grew particularly well. Most human beings, unless +they are extremely hungry, are attracted to baby animals, and many wild +pups or fawns or piglets must have been brought back alive by hunting +parties. + +In this last sense, man has probably always been an incipient +cultivator and domesticator. But I believe that Adams is right in +suggesting that this would be doubly true with the experimenters of +the terminal era of food-collecting. We noticed that they also seem +to have had a tendency to settle down. Now my hunch goes that _when_ +this experimentation and settling down took place within a potential +nuclear area--where a whole constellation of plants and animals +possible of domestication was available--the change was easily made. +Professor Charles A. Reed, our field colleague in zoology, agrees that +year-round settlement with plant domestication probably came before +there were important animal domestications. + + +INCIPIENT ERAS AND NUCLEAR AREAS + +I have put this scheme into a simple chart (p. 111) with the names +of a few of the sites we are going to talk about. You will see that my +hunch means that there are eras of incipient cultivation _only_ within +nuclear areas. In a nuclear area, the terminal era of food-collecting +would probably have been quite short. I do not know for how long a time +the era of incipient cultivation and domestication would have lasted, +but perhaps for several thousand years. Then it passed on into the era +of the primary village-farming community. + +Outside a nuclear area, the terminal era of food-collecting would last +for a long time; in a few out-of-the-way parts of the world, it still +hangs on. It would end in any particular place through contact with +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 +many) who had reached at least the level of the primary village-farming +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. + + +ARCHEOLOGICAL DIFFICULTIES IN SEEING THE INCIPIENT ERA + +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 +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 +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 +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! + +[Illustration: POSSIBLE RELATIONSHIPS OF STAGES AND ERAS IN WESTERN +ASIA AND NORTHEASTERN AFRICA] + +Now since this was an era of incipience, of the birth of new ideas, +and of experimentation, it is very difficult to see its traces +archeologically. New tools having to do with the new ways of getting +and, in fact, producing food would have taken some time to develop. +It need not surprise us too much if we cannot find hoes for planting +and sickles for reaping grain at the very beginning. We might expect +a time of making-do with some of the older tools, or with make-shift +tools, for some of the new jobs. The present-day wild cousin of the +domesticated sheep still lives in the mountains of western Asia. It has +no wool, only a fine down under hair like that of a deer, so it need +not surprise us to find neither the whorls used for spinning nor traces +of woolen cloth. It must have taken some time for a wool-bearing sheep +to develop and also time for the invention of the new tools which go +with weaving. It would have been the same with other kinds of tools for +the new way of life. + +It is difficult even for an experienced comparative zoologist to tell +which are the bones of domesticated animals and which are those of +their wild cousins. This is especially so because the animal bones the +archeologists find are usually fragmentary. Furthermore, we do not have +a sort of library collection of the skeletons of the animals or an +herbarium of the plants of those times, against which the traces which +the archeologists find may be checked. We are only beginning to get +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 +from some slightly later sites. + + +SITES WITH ANTIQUITIES OF THE INCIPIENT ERA + +So far, we know only a very few sites which would suit my notion of the +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 +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 +they are already used to. I would suspect that a great deal of this +making-do went on almost to the end of this era. + + +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 +guess that it probably falls within five hundred years either way of +about 5000 B.C. + +Until recently, the people who produced the Natufian assemblage were +thought to have been only cave dwellers, but now at least three open +air Natufian sites have been briefly described. In their best-known +dwelling place, on Mount Carmel, the Natufian folk lived in the open +mouth of a large rock-shelter and on the terrace in front of it. On the +terrace, they had set at least two short curving lines of stones; but +these were hardly architecture; they seem more like benches or perhaps +the low walls of open pens. There were also one or two small clusters +of stones laid like paving, and a ring of stones around a hearth or +fireplace. One very round and regular basin-shaped depression had been +cut into the rocky floor of the terrace, and there were other less +regular basin-like depressions. In the newly reported open air sites, +there seem to have been huts with rounded corners. + +Most of the finds in the Natufian layer of the Mount Carmel cave were +flints. About 80 per cent of these flint tools were microliths made +by the regular working of tiny blades into various tools, some having +geometric forms. The larger flint tools included backed blades, burins, +scrapers, a few arrow points, some larger hacking or picking tools, and +one special type. This last was the sickle blade. + +We know a sickle blade of flint when we see one, because of a strange +polish or sheen which seems to develop on the cutting edge when the +blade has been used to cut grasses or grain, or--perhaps--reeds. In +the Natufian, we have even found the straight bone handles in which a +number of flint sickle blades were set in a line. + +There was a small industry in ground or pecked stone (that is, abraded +not chipped) in the Natufian. This included some pestle and mortar +fragments. The mortars are said to have a deep and narrow hole, +and some of the pestles show traces of red ochre. We are not sure +that these mortars and pestles were also used for grinding food. In +addition, there were one or two bits of carving in stone. + + +NATUFIAN ANTIQUITIES IN OTHER MATERIALS; BURIALS AND PEOPLE + +The Natufian industry in bone was quite rich. It included, beside the +sickle hafts mentioned above, points and harpoons, straight and curved +types of fish-hooks, awls, pins and needles, and a variety of beads and +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 +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, +and several of the best collections of beads come from the burials. On +two of the skulls there were traces of elaborate head-dresses of shell +beads. + +[Illustration: SKETCH OF NATUFIAN ASSEMBLAGE + + MICROLITHS + ARCHITECTURE? + BURIAL + CHIPPED STONE + GROUND STONE + BONE] + +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 +the same climate that it has today. Some of the animal bones were those +of large or dangerous beasts: the hyena, the bear, the wild boar, +and the leopard. But the Natufian people may have had the help of a +large domesticated dog. If our guess at a date for the Natufian is +right (about 7750 B.C.), this is an earlier dog than was that in the +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.� + + +THE KARIM SHAHIR ASSEMBLAGE + +Karim Shahir differs from the Natufian sites in that it shows traces +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 +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. + +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 +on it spread out over about three acres of area. In spots, the single +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. + +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 +were microlithic bladelets and geometric forms. The flint sickle blade +was almost non-existent, being far scarcer than in the Natufian. The +people of Karim Shahir did a modest amount of work in the grinding of +stone; there were milling stone fragments of both the mortar and the +quern type, and stone hoes or axes with polished bits. Beads, pendants, +rings, and bracelets were made of finer quality stone. We found a few +simple points and needles of bone, and even two rather formless unbaked +clay figurines which seemed to be of animal form. + +[Illustration: SKETCH OF KARIM SHAHIR ASSEMBLAGE + + CHIPPED STONE + GROUND STONE + UNBAKED CLAY + SHELL + BONE + �ARCHITECTURE�] + +Karim Shahir did not yield direct evidence of the kind of vegetable +food its people ate. The animal bones showed a considerable +increase in the proportion of the bones of the species capable of +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 +at the moment that will tell us from the bones alone. + + +WERE THE NATUFIAN AND KARIM SHAHIR PEOPLES FOOD-PRODUCERS? + +It is clear that a great part of the food of the Natufian people +must have been hunted or collected. Shells of land, fresh-water, and +sea animals occur in their cave layers. The same is true as regards +Karim Shahir, save for sea shells. But on the other hand, we have +the sickles, the milling stones, the possible Natufian dog, and the +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 +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 +food-production--that the hints we seek are there. But in the sense +that the peoples of the era of the primary village-farming community, +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 +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. + + +SUMMARY + +I am sorry that this chapter has had to be so much more about ideas +than about the archeological traces of prehistoric men themselves. +But the antiquities of the incipient era of cultivation and animal +domestication will not be spectacular, even when we do have them +excavated in quantity. Few museums will be interested in these +antiquities for exhibition purposes. The charred bits or impressions +of plants, the fragments of animal bone and shell, and the varied +clues to climate and environment will be as important as the artifacts +themselves. It will be the ideas to which these traces lead us that +will be important. I am sure that this unspectacular material--when we +have much more of it, and learn how to understand what it says--will +lead us to how and why answers about the first great change in human +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 +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 +in existence. Without effective food-production and the settled +village-farming communities, civilization never could have followed. +How effective food-production came into being by the end of the +incipient era, is, I believe, one of the most fascinating questions any +archeologist could face. + +It now seems probable--from possibly two of the Palestinian sites with +varieties of the Natufian (Jericho and Nahal Oren)--that there were +one or more local Palestinian developments out of the Natufian into +later times. In the same way, what followed after the Karim Shahir type +of assemblage in northeastern Iraq was in some ways a reflection of +beginnings made at Karim Shahir and Zawi Chemi. + + + + +THE First Revolution + +[Illustration] + + +As the incipient era of cultivation and animal domestication passed +onward into the era of the primary village-farming community, the first +basic change in human economy was fully achieved. In southwestern Asia, +this seems to have taken place about nine thousand years ago. I am +going to restrict my description to this earliest Near Eastern case--I +do not know enough about the later comparable experiments in the Far +East and in the New World. Let us first, once again, think of the +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 +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 +moved. Not many people can be fed in this way in one area, and small +children and old folks are a burden. There is not enough food to store, +and it is not the kind that can be stored for long. + +Do you see how this all fits into a picture? Small groups of people +living now in this cave, now in that--or out in the open--as they moved +after the animals they hunted; no permanent villages, a few half-buried +huts at best; no breakable utensils; no pottery; no signs of anything +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. +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. + + +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,� +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 +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 +already of a fair size, and it was growing, too. Everybody had more to +eat; they were presumably all stronger, and there were more children. +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 +empty-handed. + +There was more time to do different things, too. They began to modify +nature. They made pottery out of raw clay, and textiles out of hair +or fiber. People who became good at pottery-making traded their pots +for food and spent all of their time on pottery alone. Other people +were learning to weave cloth or to make new tools. There were already +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 +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 +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�? + +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 +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 +been established, however, things did begin to move very fast. By +six thousand years ago, the descendants of the first villagers had +developed irrigation and plow agriculture in the relatively rainless +Mesopotamian alluvium and were living in towns with temples. Relative +to the half million years of food-gathering which lay behind, this had +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 +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 +to the intelligent co-operation given foreign excavators by the +Iraq Directorate General of Antiquities, our understanding of the +sequence in Iraq is growing more complete. I shall use Iraq as my main +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.� + +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 +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 +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 +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 +on such a dig holds--halcyon days for the body and acute pleasurable +stimulation for the mind. Old things coming newly out of the good dirt, +and the pieces of the human puzzle fitting into place! I think I am +an honest man; I cannot tell you that I am sorry the job is not yet +finished and that there are still gaps in this part of the Near Eastern +archeological sequence. + + +EARLIEST SITES OF THE VILLAGE FARMERS + +So far, the Karim Shahir type of assemblage, which we looked at in the +last chapter, is the earliest material available in what I take to +be the nuclear area. We do not believe that Karim Shahir was a village +site proper: it looks more like the traces of a temporary encampment. +Two caves, called Belt and Hotu, which are outside the nuclear area +and down on the foreshore of the Caspian Sea, have been excavated +by Professor Coon. These probably belong in the later extension of +the terminal era of food-gathering; in their upper layers are traits +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, +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 +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 +sites and assemblages which lie along the hilly flanks of the crescent +and just below it. I am going to describe and illustrate some of these +for you. + +Since not very much archeological excavation has yet been done on sites +of this range of time, I shall have to mention the names of certain +single sites which now alone stand for an assemblage. This does not +mean that I think the individual sites I mention were unique. In the +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 +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 +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). + + +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 +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 +uncertain about this two thousand years--I do not think it can have +been so long. + +This suggests that we still have much work to do in Iraq. You can +imagine how earnestly we await the return of political stability in the +Republic of Iraq. + + +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 +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 +east of the modern oil town of Kirkuk. The Iraq Directorate General of +Antiquities suggested that we look at it in 1948, and we have had three +seasons of digging on it since. + +The people of Jarmo grew the barley plant and two different kinds of +wheat. They made flint sickles with which to reap their grain, mortars +or querns on which to crack it, ovens in which it might be parched, and +stone bowls out of which they might eat their porridge. We are sure +that they had the domesticated goat, but Professor Reed (the staff +zoologist) is not convinced that the bones of the other potentially +domesticable animals of Jarmo--sheep, cattle, pig, horse, dog--show +sure signs of domestication. We had first thought that all of these +animals were domesticated ones, but Reed feels he must find out much +more before he can be sure. As well as their grain and the meat from +their animals, the people of Jarmo consumed great quantities of land +snails. Botanically, the Jarmo wheat stands about half way between +fully bred wheat and the wild forms. + + +ARCHITECTURE: HALL-MARK OF THE VILLAGE + +The sure sign of the village proper is in its traces of architectural +permanence. The houses of Jarmo were only the size of a small cottage +by our standards, but each was provided with several rectangular rooms. +The walls of the houses were made of puddled mud, often set on crude +foundations of stone. (The puddled mud wall, which the Arabs call +_touf_, is built by laying a three to six inch course of soft mud, +letting this sun-dry for a day or two, then adding the next course, +etc.) The village probably looked much like the simple Kurdish farming +village of today, with its mud-walled houses and low mud-on-brush +roofs. I doubt that the Jarmo village had more than twenty houses at +any one moment of its existence. Today, an average of about seven +people live in a comparable Kurdish house; probably the population of +Jarmo was about 150 people. + +[Illustration: SKETCH OF JARMO ASSEMBLAGE + + CHIPPED STONE + UNBAKED CLAY + GROUND STONE + POTTERY _UPPER THIRD OF SITE ONLY._ + REED MATTING + BONE + ARCHITECTURE] + +It is interesting that portable pottery does not appear until the +last third of the life of the Jarmo village. Throughout the duration +of the village, however, its people had experimented with the plastic +qualities of clay. They modeled little figurines of animals and of +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 +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 +art. + +On the other hand, the old tradition of making flint blades and +microlithic tools was still very strong at Jarmo. The sickle-blade was +made in quantities, but so also were many of the much older tool types. +Strangely enough, it is within this age-old category of chipped stone +tools that we see one of the clearest pointers to a newer age. Many of +the Jarmo chipped stone tools--microliths--were made of obsidian, a +black volcanic natural glass. The obsidian beds nearest to Jarmo are +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� +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 +achieved almost nine thousand years ago. + + +HASSUNA, IN UPPER MESOPOTAMIAN IRAQ + +We are not sure just how soon after Jarmo the next assemblage of Iraqi +material is to be placed. I do not think the time was long, and there +are a few hints that detailed habits in the making of pottery and +ground stone tools were actually continued from Jarmo times into the +time of the next full assemblage. This is called after a site named +Hassuna, a few miles to the south and west of modern Mosul. We also +have Hassunan type materials from several other sites in the same +general region. It is probably too soon to make generalizations about +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 +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 +different ways of making and decorating pottery vessels. One style of +pottery painting, called the Samarran style, is an extremely handsome +one and must have required a great deal of concentration and excellence +of draftsmanship. On the other hand, the old habits for the preparation +of good chipped stone tools--still apparent at Jarmo--seem to have +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 +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. + + +OTHER EARLY VILLAGE SITES IN THE NUCLEAR AREA + +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, +roughly comparable to that of Hassuna, for example, must reflect a +culture which lived at just about the same time as that of Hassuna. We +do this guessing on the basis of the general similarity and degree of +complexity of the Sears Roebuck catalogues of the particular assemblage +and that of Hassuna. We suppose that for sites near at hand and of a +comparable cultural level, as indicated by their generally similar +assemblages, the dating must be about the same. We may also know that +in a general stratigraphic sense, the sites in question may both appear +at the bottom of the ascending village sequence in their respective +areas. Without a number of consistent radioactive carbon dates, we +cannot be precise about priorities. + +[Illustration: SKETCH OF HASSUNA ASSEMBLAGE + + POTTERY + POTTERY OBJECTS + CHIPPED STONE + BONE + GROUND STONE + ARCHITECTURE + REED MATTING + BURIAL] + +The ancient mound at Jericho, in the Dead Sea valley in Palestine, +yields some very interesting material. Its catalogue somewhat resembles +that of Jarmo, especially in the sense that there is a fair depth +of deposit without portable pottery vessels. On the other hand, the +architecture of Jericho is surprisingly complex, with traces of massive +stone fortification walls and the general use of formed sun-dried +mud brick. Jericho lies in a somewhat strange and tropically lush +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 +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. + +The mound at Jericho (Tell es-Sultan) contains a remarkably +fine sequence, which perhaps does not have the gap we noted in +Iraqi-Kurdistan between the Karim Shahir group and Jarmo. While I am +not sure that the Jericho sequence will prove valid for those parts +of Palestine outside the special Dead Sea environmental niche, the +sequence does appear to proceed from the local variety of Natufian into +that of a very well settled community. So far, we have little direct +evidence for the food-production basis upon which the Jericho people +subsisted. + +There is an early village assemblage with strong characteristics of its +own in the land bordering the northeast corner of the Mediterranean +Sea, where Syria and the Cilician province of Turkey join. This early +Syro-Cilician assemblage must represent a general cultural pattern +which was at least in part contemporary with that of the Hassuna +assemblage. These materials from the bases of the mounds at Mersin, and +from Judaidah in the Amouq plain, as well as from a few other sites, +represent the remains of true villages. The walls of their houses were +built of puddled mud, but some of the house foundations were of stone. +Several different kinds of pottery were made by the people of these +villages. None of it resembles the pottery from Hassuna or from the +upper levels of Jarmo or Jericho. The Syro-Cilician people had not +lost their touch at working flint. An important southern variation of +the Syro-Cilician assemblage has been cleared recently at Byblos, a +port town famous in later Phoenician times. There are three radiocarbon +determinations which suggest that the time range for these developments +was in the sixth or early fifth millennium B.C. + +It would be fascinating to search for traces of even earlier +village-farming communities and for the remains of the incipient +cultivation era, in the Syro-Cilician region. + + +THE IRANIAN PLATEAU AND THE NILE VALLEY + +The map on page 125 shows some sites which lie either outside or in +an extension of the hilly-flanks zone proper. From the base of the +great mound at Sialk on the Iranian plateau came an assemblage of +early village material, generally similar, in the kinds of things it +contained, to the catalogues of Hassuna and Judaidah. The details of +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 +appearance. + +There is at least one very early Iranian site on the inward slopes +of the hilly-flanks zone. It is the earlier of two mounds at a place +called Bakun, in southwestern Iran; the results of the excavations +there are not yet published and we only know of its coarse and +primitive pottery. I only mention Bakun because it helps us to plot the +extent of the hilly-flanks zone villages on the map. + +The Nile Valley lies beyond the peculiar environmental zone of the +hilly flanks of the crescent, and it is probable that the earliest +village-farming communities in Egypt were established by a few people +who wandered into the Nile delta area from the nuclear area. The +assemblage which is most closely comparable to the catalogue of Hassuna +or Judaidah, for example, is that from little settlements along the +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 +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 +generalized cultural pattern of village-farming community life to +spread from the nuclear area down into Egypt, but as yet we have no way +of testing these matters. + +In this same vein, we have two radioactive carbon dates for an +assemblage from sites near Khartoum in the Sudan, best represented by +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. + +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� +may be mainly an artificial selection of poor examples of objects which +belong in the following range of time. + + +SPREAD IN TIME AND SPACE + +There are now two things we can do; in fact, we have already begun to +do them. We can watch the spread of the new way of life upward through +time in the nuclear area. We can also see how the new way of life +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 +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 +into Africa, to reach Khartoum in the Sudan some thirty-five hundred +years later than we had seen it at Jarmo or Jericho. We caught glimpses +of it in the Fayum and perhaps at Tasa along the way. + +For the remainder of this chapter, I shall try to suggest briefly for +you the directions taken by the spread of the new way of life from the +nuclear area in the Near East. First, let me make clear again that +I _do not_ believe that the village-farming community way of life +was invented only once and in the Near East. It seems to me that the +evidence is very clear that a separate experiment arose in the New +World. For China, the question of independence or borrowing--in the +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 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 +China. However, the appearance of the village-farming community in the +northwest of India, at least, seems to have depended on the earlier +development in the Near East. It is also probable that ideas of the new +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 +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 +just as complicated. + +This much is clear for Europe, as far as the spread of the +village-community way of life is concerned. The general idea and much +of the know-how and the basic tools of food-production moved from the +Near East to Europe. So did the plants and animals which had been +domesticated; they were not naturally at home in Europe, as they were +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 +the groups that moved. + +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 +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 +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.� + + +THE WAYS TO EUROPE + +Suppose we want to follow the traces of those earliest village-farmers +who did travel from western Asia into Europe. Let us start from +Syro-Cilicia, that part of the hilly-flanks zone proper which lies in +the very northeastern corner of the Mediterranean. Three ways would be +open to us (of course we could not be worried about permission from the +Soviet authorities!). We would go north, or north and slightly east, +across Anatolian Turkey, and skirt along either shore of the Black Sea +or even to the east of the Caucasus Mountains along the Caspian Sea, +to reach the plains of Ukrainian Russia. From here, we could march +across eastern Europe to the Baltic and Scandinavia, or even hook back +southwestward to Atlantic Europe. + +Our second way from Syro-Cilicia would also lie over Anatolia, to the +northwest, where we would have to swim or raft ourselves over the +Dardanelles or the Bosphorus to the European shore. Then we would bear +left toward Greece, but some of us might turn right again in Macedonia, +going up the valley of the Vardar River to its divide and on down +the valley of the Morava beyond, to reach the Danube near Belgrade +in Jugoslavia. Here we would turn left, following the great river +valley of the Danube up into central Europe. We would have a number of +tributary valleys to explore, or we could cross the divide and go down +the valley of the Rhine to the North Sea. + +Our third way from Syro-Cilicia would be by sea. We would coast along +southern Anatolia and visit Cyprus, Crete, and the Aegean islands on +our way to Greece, where, in the north, we might meet some of those who +had taken the second route. From Greece, we would sail on to Italy and +the western isles, to reach southern France and the coasts of Spain. +Eventually a few of us would sail up the Atlantic coast of Europe, to +reach western Britain and even Ireland. + +[Illustration: PROBABLE ROUTES AND TIMING IN THE SPREAD OF THE +VILLAGE-FARMING COMMUNITY WAY OF LIFE FROM THE NEAR EAST TO EUROPE] + +Of course none of us could ever take these journeys as the first +farmers took them, since the whole course of each journey must have +lasted many lifetimes. The date given to the assemblage called Windmill +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 +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.� + +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 +(p. 139). + + +THE EARLIEST FARMERS OF ENGLAND + +To describe the later prehistory of all Europe for you would take +another book and a much larger one than this is. Therefore, I have +decided to give you only a few impressions of the later prehistory of +Britain. Of course the British Isles lie at the other end of Europe +from our base-line in western Asia. Also, they received influences +along at least two of the three ways in which the new way of life +moved into Europe. We will look at more of their late prehistory in a +following chapter: here, I shall speak only of the first farmers. + +The assemblage called Windmill Hill, which appears in the south of +England, exhibits three different kinds of structures, evidence of +grain-growing and of stock-breeding, and some distinctive types of +pottery and stone implements. The most remarkable type of structure +is the earthwork enclosures which seem to have served as seasonal +cattle corrals. These enclosures were roughly circular, reached over +a thousand feet in diameter, and sometimes included two or three +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 +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 +convinced, however, that important elements of Windmill Hill stem from +northern Germany and Denmark--products of the first way into Europe +from the east. + +The archeological traces of a second early culture are to be found +in the west of England, western and northern Scotland, and most of +Ireland. The bearers of this culture had come up the Atlantic coast +by sea from southern France and Spain. The evidence they have left us +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 +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 +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. + +The cultures of the people who produced the Windmill Hill assemblage +and of those who made the megalithic tombs flourished, at least in +part, at the same time. Although the distributions of the two different +types of archeological traces are in quite different parts of the +country, there is Windmill Hill pottery in some of the megalithic +tombs. But the tombs also contain pottery which seems to have arrived +with the tomb builders themselves. + +The third early British group of antiquities of this general time +(following 2500 B.C.) comes from sites in southern and eastern England. +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 +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 +finds extends into eastern Britain, where the other cultures have left +no trace. The Peterborough people had villages with semi-subterranean +huts, and the bones of oxen, pigs, and sheep have been found in a few +of these. On the whole, however, hunting and fishing seem to have been +their vital occupations. They also established trade routes especially +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 +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 +bedsteads, the shelves, the chests for clothes and oddments--all built +of thin stone-slabs--may still be seen in place. But the Skara Brae +people lived entirely by sheep- and cattle-breeding, and by catching +shellfish. Neither grain nor the instruments of agriculture appeared at +Skara Brae. + + +THE EUROPEAN ACHIEVEMENT + +The above is only a very brief description of what went on in Britain +with the arrival of the first farmers. There are many interesting +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 +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� +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_ +superior. The ideas and things and the people had to prove themselves +in their adaptation to each new environment. Once this was done another +link to the chain would be added, and then the forest-dwellers and +other indigenous folk of Europe along the way might accept the new +ideas and things. It is quite reasonable to expect that there must have +been much mixture of the migrants and the indigenes along the way; the +Peterborough and Skara Brae assemblages we mentioned above would seem +to be clear traces of such fused cultures. Sometimes, especially if the +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 +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 +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. + +What we know of the ways of men suggests to us that when the details +of the later prehistory of further Asia and Africa are learned, their +stories will be just as exciting. + + + + +THE Conquest of Civilization + +[Illustration] + + +Now we must return to the Near East again. We are coming to the point +where history is about to begin. I am going to stick pretty close +to Iraq and Egypt in this chapter. These countries will perhaps be +the most interesting to most of us, for the foundations of western +civilization were laid in the river lands of the Tigris and Euphrates +and of the Nile. I shall probably stick closest of all to Iraq, because +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 +traditions we live by today are based, ultimately, on those ancient +beginnings in food-production and civilization in the Near East. + + +WHAT �CIVILIZATION� MEANS + +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 +people have set up. It means formal laws--rules of conduct--which the +government (if not the people) believes are necessary. It probably +means that there are formalized projects--roads, harbors, irrigation +canals, and the like--and also some sort of army or police force +to protect them. It means quite new and different art forms. It +also usually means there is writing. (The people of the Andes--the +Incas--had everything which goes to make up a civilization but formal +writing. I can see no reason to say they were not civilized.) Finally, +as the late Professor Redfield reminded us, civilization seems to bring +with it the dawn of a new kind of moral order. + +In different civilizations, there may be important differences in the +way such things as the above are managed. In early civilizations, it is +usual to find religion very closely tied in with government, law, and +so forth. The king may also be a high priest, or he may even be thought +of as a god. The laws are usually thought to have been given to the +people by the gods. The temples are protected just as carefully as the +other projects. + + +CIVILIZATION IMPOSSIBLE WITHOUT FOOD-PRODUCTION + +Civilizations have to be made up of many people. Some of the people +live in the country; some live in very large towns or cities. Classes +of society have begun. There are officials and government people; there +are priests or religious officials; there are merchants and traders; +there are craftsmen, metal-workers, potters, builders, and so on; there +are also farmers, and these are the people who produce the food for the +whole population. It must be obvious that civilization cannot exist +without food-production and that food-production must also be at a +pretty efficient level of village-farming before civilization can even +begin. + +But people can be food-producing without being civilized. In many +parts of the world this is still the case. When the white men first +came to America, the Indians in most parts of this hemisphere were +food-producers. They grew corn, potatoes, tomatoes, squash, and many +other things the white men had never eaten before. But only the Aztecs +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? + +Once you have food-production, even at the well-advanced level of +the village-farming community, what else has to happen before you +get civilization? Many men have asked this question and have failed +to give a full and satisfactory answer. There is probably no _one_ +answer. I shall give you my own idea about how civilization _may_ have +come about in the Near East alone. Remember, it is only a guess--a +putting together of hunches from incomplete evidence. It is _not_ meant +to explain how civilization began in any of the other areas--China, +southeast Asia, the Americas--where other early experiments in +civilization went on. The details in those areas are quite different. +Whether certain general principles hold, for the appearance of any +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 +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). + +Now notice that this hilly-flanks zone does not include southern +Mesopotamia, the alluvial land of the lower Tigris and Euphrates in +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 +river lands, however, that the immediate beginnings of civilization +were made. + +We know that by about 3200 B.C. the Early Dynastic period had begun +in southern Mesopotamia. The beginnings of writing go back several +hundred years earlier, but we can safely say that civilization had +begun in Mesopotamia by 3200 B.C. In Egypt, the beginning of the First +Dynasty is slightly later, at about 3100 B.C., and writing probably +did not appear much earlier. There is no question but that history and +civilization were well under way in both Mesopotamia and Egypt by 3000 +B.C.--about five thousand years ago. + + +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 +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. + +The land in the hilly flanks is of a sort which people can farm without +too much trouble. There is a fairly fertile coastal strip in Palestine +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 +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 +domestic animals had been successful, a pleasant living could be +made--and without too much trouble. + +I should add here again, that our evidence points increasingly to a +climate for those times which is very little different from that for +the area today. Now look at Egypt and southern Mesopotamia. Both are +lands without rain, for all intents and purposes. Both are lands with +rivers that have laid down very fertile soil--soil perhaps superior to +that in the hilly flanks. But in both lands, the rivers are of no great +aid without some control. + +The Nile floods its banks once a year, in late September or early +October. It not only soaks the narrow fertile strip of land on either +side; it lays down a fresh layer of new soil each year. Beyond the +fertile strip on either side rise great cliffs, and behind them is the +desert. In its natural, uncontrolled state, the yearly flood of the +Nile must have caused short-lived swamps that were full of crocodiles. +After a short time, the flood level would have dropped, the water and +the crocodiles would have run back into the river, and the swamp plants +would have become parched and dry. + +The Tigris and the Euphrates of Mesopotamia are less likely to flood +regularly than the Nile. The Tigris has a shorter and straighter course +than the Euphrates; it is also the more violent river. Its banks are +high, and when the snows melt and flow into all of its tributary rivers +it is swift and dangerous. The Euphrates has a much longer and more +curving course and few important tributaries. Its banks are lower and +it is less likely to flood dangerously. The land on either side and +between the two rivers is very fertile, south of the modern city of +Baghdad. Unlike the Nile Valley, neither the Tigris nor the Euphrates +is flanked by cliffs. The land on either side of the rivers stretches +out for miles and is not much rougher than a poor tennis court. + + +THE RIVERS MUST BE CONTROLLED + +The real trick in both Egypt and Mesopotamia is to make the rivers work +for you. In Egypt, this is a matter of building dikes and reservoirs +that will catch and hold the Nile flood. In this way, the water is held +and allowed to run off over the fields as it is needed. In Mesopotamia, +it is a matter of taking advantage of natural river channels and branch +channels, and of leading ditches from these onto the fields. + +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 +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. +In the second place, the patterns for such projects seem to have been +pretty well set by historic times. + + +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 +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 +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. + +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 +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. + + +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 +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 +way to live until they had set up the rules? I think that small groups +must have moved down along the mud-flats of the river banks quite +early, making use of naturally favorable spots, and that the rules grew +out of such cases. It would have been like the hand-in-hand growth of +automobiles and paved highways in the United States. + +Once the rules and the know-how did get going, there must have been a +constant interplay of the two. Thus, the more the crops yielded, the +richer and better-fed the people would have been, and the more the +population would have grown. As the population grew, more land would +have needed to be flooded or irrigated, and more complex systems of +dikes, reservoirs, canals, and ditches would have been built. The more +complex the system, the more necessity for work on new projects and for +the control of their use.... And so on.... + +What I have just put down for you is a guess at the manner of growth of +some of the formalized systems that go to make up a civilized society. +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. + +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 +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 +they worked. But without these other aspects of culture, I do not +think that urbanization and civilization itself could have come into +being. + + +THE ARCHEOLOGICAL SEQUENCE TO CIVILIZATION IN IRAQ + +We last spoke of the archeological materials of Iraq on page 130, +where I described the village-farming community of Hassunan type. The +Hassunan type villages appear in the hilly-flanks zone and in the +rolling land adjacent to the Tigris in northern Iraq. It is probable +that even before the Hassuna pattern of culture lived its course, a +new assemblage had been established in northern Iraq and Syria. This +assemblage is called Halaf, after a site high on a tributary of the +Euphrates, on the Syro-Turkish border. + +[Illustration: SKETCH OF SELECTED ITEMS OF HALAFIAN ASSEMBLAGE + + BEADS AND PENDANTS + POTTERY MOTIFS + POTTERY] + +The Halafian assemblage is incompletely known. The culture it +represents included a remarkably handsome painted pottery. +Archeologists have tended to be so fascinated with this pottery that +they have bothered little with the rest of the Halafian assemblage. We +do know that strange stone-founded houses, with plans like those of the +popular notion of an Eskimo igloo, were built. Like the pottery of the +Samarran style, which appears as part of the Hassunan assemblage (see +p. 131), the Halafian painted pottery implies great concentration and +excellence of draftsmanship on the part of the people who painted it. + +We must mention two very interesting sites adjacent to the mud-flats of +the rivers, half way down from northern Iraq to the classic alluvial +Mesopotamian area. One is Baghouz on the Euphrates; the other is +Samarra on the Tigris (see map, p. 125). Both these sites yield the +handsome painted pottery of the style called Samarran: in fact it +is Samarra which gives its name to the pottery. Neither Baghouz nor +Samarra have completely Hassunan types of assemblages, and at Samarra +there are a few pots of proper Halafian style. I suppose that Samarra +and Baghouz give us glimpses of those early farmers who had begun to +finger their way down the mud-flats of the river banks toward the +fertile but yet untilled southland. + + +CLASSIC SOUTHERN MESOPOTAMIA FIRST OCCUPIED + +Our next step is into the southland proper. Here, deep in the core of +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� +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 +do not have much evidence to go on. The idea does at least fit with my +interpretation of the meaning of Baghouz and Samarra as way-points on +the mud-flats of the rivers half way down from the north. + +My colleague, Robert Adams, believes that there were certainly +riverine-adapted food-collectors living in lower Mesopotamia. The +presence of such would explain why the Eridu assemblage is not simply +the sum of the Halafian and Samarran assemblages. But the domesticated +plants and animals and the basic ways of food-production must have +come from the hilly-flanks country in the north. + +Above the basal Eridu levels, and at a number of other sites in the +south, comes a full-fledged assemblage called Ubaid. Incidentally, +there is an aspect of the Ubaidian assemblage in the north as well. It +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 +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 +carefully drafted styles of Samarra and Halaf. The Ubaidian people made +other items of baked clay: sickles and axes of very hard-baked clay +are found. The northern Ubaidian sites have yielded tools of copper, +but metal tools of unquestionable Ubaidian find-spots are not yet +available from the south. Clay figurines of human beings with monstrous +turtle-like faces are another item in the southern Ubaidian assemblage. + +[Illustration: SKETCH OF SELECTED ITEMS OF 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 +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 +the peculiar riverine environment of classic southern Mesopotamia. For +example, hard-baked clay axes will chop bundles of reeds very well, or +help a mason dress his unbaked mud bricks, and there were only a few +soft and pithy species of trees available. The Ubaidian levels of Eridu +yield quantities of date pits; that excellent and characteristically +Iraqi fruit was already in use. The excavators also found the clay +model of a ship, with the stepping-point for a mast, so that Sinbad the +Sailor must have had his antecedents as early as the time of Ubaid. +The bones of fish, which must have flourished in the larger canals as +well as in the rivers, are common in the Ubaidian levels and thereafter. + + +THE UBAIDIAN ACHIEVEMENT + +On present evidence, my tendency is to see the Ubaidian assemblage +in southern Iraq as the trace of a new era. I wish there were more +evidence, but what we have suggests this to me. The culture of southern +Ubaid soon became a culture of towns--of centrally located towns with +some rural villages about them. The town had a temple and there must +have been priests. These priests probably had political and economic +functions as well as religious ones, if the somewhat later history of +Mesopotamia may suggest a pattern for us. Presently the temple and its +priesthood were possibly the focus of the market; the temple received +its due, and may already have had its own lands and herds and flocks. +The people of the town, undoubtedly at least in consultation with the +temple administration, planned and maintained the simple irrigation +ditches. As the system flourished, the community of rural farmers would +have produced more than sufficient food. The tendency for specialized +crafts to develop--tentative at best at the cultural level of the +earlier village-farming community era--would now have been achieved, +and probably many other specialists in temple administration, water +control, architecture, and trade would also have appeared, as the +surplus food-supply was assured. + +Southern Mesopotamia is not a land rich in natural resources other +than its fertile soil. Stone, good wood for construction, metal, and +innumerable other things would have had to be imported. Grain and +dates--although both are bulky and difficult to transport--and wool and +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 +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 +then we may watch with interest where the Ubaid pottery-painting +tradition spread. We have already mentioned that there is a substantial +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 +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 +Syria, and it may be faintly reflected in the painted style of a +site called Ghassul, on the east bank of the Jordan in the Dead Sea +Valley. Over this vast area--certainly in all of the great basin of +the Tigris-Euphrates drainage system and its natural extensions--I +believe we may lay our fingers on the traces of a peculiar way of +decorating pottery, which we call Ubaidian. This cursive and even +slap-dash decoration, it appears to me, was part of a new cultural +tradition which arose from the adjustments which immigrant northern +farmers first made to the new and challenging environment of southern +Mesopotamia. But exciting as the idea of the spread of influences of +the Ubaid tradition in space may be, I believe you will agree that the +consequences of the growth of that tradition in southern Mesopotamia +itself, as time passed, are even more important. + + +THE WARKA PHASE IN THE SOUTH + +So far, there are only two radiocarbon determinations for the Ubaidian +assemblage, one from Tepe Gawra in the north and one from Warka in the +south. My hunch would be to use the dates 4500 to 3750 B.C., with a +plus or more probably a minus factor of about two hundred years for +each, as the time duration of the Ubaidian assemblage in southern +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 +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 +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 +deal more to say about the Warkan assemblage, but someone will first +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 +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 +the somewhat shaky principle that writing, however early, means +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 +a consistent development from beginning to end. + +I shall, in fact, leave much of the description and the historic +implications of the Proto-Literate assemblage to the conventional +historians. Professor T. J. Jacobsen, reaching backward from the +legends he finds in the cuneiform writings of slightly later times, can +in fact tell you a more complete story of Proto-Literate culture than +I can. It should be enough here if I sum up briefly what the excavated +archeological evidence shows. + +We have yet to dig a Proto-Literate site in its entirety, but the +indications are that the sites cover areas the size of small cities. +In architecture, we know of large and monumental temple structures, +which were built on elaborate high terraces. The plans and decoration +of these temples follow the pattern set in the Ubaid phase: the chief +difference is one of size. The German excavators at the site of Warka +reckoned that the construction of only one of the Proto-Literate temple +complexes there must have taken 1,500 men, each working a ten-hour day, +five years to build. + + +ART AND WRITING + +If the architecture, even in its monumental forms, can be seen to +stem from Ubaidian developments, this is not so with our other +evidence of Proto-Literate artistic expression. In relief and applied +sculpture, in sculpture in the round, and on the engraved cylinder +seals--all of which now make their appearance--several completely +new artistic principles are apparent. These include the composition +of subject-matter in groups, commemorative scenes, and especially +the ability and apparent desire to render the human form and face. +Excellent as the animals of the Franco-Cantabrian art may have been +(see p. 85), and however handsome were the carefully drafted +geometric designs and conventionalized figures on the pottery of the +early farmers, there seems to have been, up to this time, a mental +block about the drawing of the human figure and especially the human +face. We do not yet know what caused this self-consciousness about +picturing themselves which seems characteristic of men before the +appearance of civilization. We do know that with civilization, the +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, +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 +Jacobsen sees writing as a means for overcoming space, time, and the +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.� + +[Illustration: RELIEF ON A PROTO-LITERATE STONE VASE, WARKA + +Unrolled drawing, with restoration suggested by figures from +contemporary cylinder seals] + +While the new principles in art and the idea of writing are not +foreshadowed in the Ubaid phase, or in what little we know of the +Warkan, I do not think we need to look outside southern Mesopotamia +for their beginnings. We do know something of the adjacent areas, +too, and these beginnings are not there. I think we must accept them +as completely new discoveries, made by the people who were developing +the whole new culture pattern of classic southern Mesopotamia. Full +description of the art, architecture, and writing of the Proto-Literate +phase would call for many details. Men like Professor Jacobsen and Dr. +Adams can give you these details much better than I can. Nor shall I do +more than tell you that the common pottery of the Proto-Literate phase +was so well standardized that it looks factory made. There was also +some handsome painted pottery, and there were stone bowls with inlaid +decoration. Well-made tools in metal had by now become fairly common, +and the metallurgist was experimenting with the casting process. Signs +for plows have been identified in the early pictographs, and a wheeled +chariot is shown on a cylinder seal engraving. But if I were forced to +a guess in the matter, I would say that the development of plows and +draft-animals probably began in the Ubaid period and was another of the +great innovations of that time. + +The Proto-Literate assemblage clearly suggests a highly developed and +sophisticated culture. While perhaps not yet fully urban, it is on +the threshold of urbanization. There seems to have been a very dense +settlement of Proto-Literate sites in classic southern Mesopotamia, +many of them newly founded on virgin soil where no earlier settlements +had been. When we think for a moment of what all this implies, of the +growth of an irrigation system which must have existed to allow the +flourish of this culture, and of the social and political organization +necessary to maintain the irrigation system, I think we will agree that +at last we are dealing with civilization proper. + + +FROM PREHISTORY TO HISTORY + +Now it is time for the conventional ancient historians to take over +the story from me. Remember this when you read what they write. Their +real base-line is with cultures ruled over by later kings and emperors, +whose writings describe military campaigns and the administration of +laws and fully organized trading ventures. To these historians, the +Proto-Literate phase is still a simple beginning for what is to follow. +If they mention the Ubaid assemblage at all--the one I was so lyrical +about--it will be as some dim and fumbling step on the path to the +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 +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 +historian are bound to be different. You will need both if you want to +understand all of the story of men, as they lived through time to the +present. + + + + +End of PREHISTORY + +[Illustration] + + +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 +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 +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, +presently to be replaced by Persia. + +A new culture, partaking of older west Asiatic and Egyptian elements, +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 +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 +to mention it, beyond recalling to you that it is to Greece we owe the +beginnings of rational or empirical science and thought in general. But +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 +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 +Africa. Remember, too, that prehistory in most of Europe runs on for +three thousand or more years _after_ conventional ancient history +begins in the Near East. Britain is a good example to use in showing +how prehistory ended in Europe. As we said earlier, it lies at the +opposite end of Europe from the area of highest cultural achievement in +those times, and should you care to read more of the story in detail, +you may do so in the English language. + + +METAL USERS REACH ENGLAND + +We left the story of Britain with the peoples who made three different +assemblages--the Windmill Hill, the megalith-builders, and the +Peterborough--making adjustments to their environments, to the original +inhabitants of the island, and to each other. They had first arrived +about 2500 B.C., and were simple pastoralists and hoe cultivators who +lived in little village communities. Some of them planted little if any +grain. By 2000 B.C., they were well settled in. Then, somewhere in the +range from about 1900 to 1800 B.C., the traces of the invasion of a new +series of peoples began to appear. + +The first newcomers are called the Beaker folk, after the name of a +peculiar form of pottery they made. The beaker type of pottery seems +oldest in Spain, where it occurs with great collective tombs of +megalithic construction and with copper tools. But the Beaker folk who +reached England seem already to have moved first from Spain(?) to the +Rhineland and Holland. While in the Rhineland, and before leaving for +England, the Beaker folk seem to have mixed with the local population +and also with incomers from northeastern Europe whose culture included +elements brought originally from the Near East by the eastern way +through the steppes. This last group has also been named for a peculiar +article in its assemblage; the group is called the Battle-axe folk. A +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 + archeological assemblage and human physical type. They speak + 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 + its usual meaning of standardized physical type. When I use + �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. + +The Beaker folk settled earliest in the agriculturally fertile south +and east. There seem to have been several phases of Beaker folk +invasions, and it is not clear whether these all came strictly from the +Rhineland or Holland. We do know that their copper daggers and awls +and armlets are more of Irish or Atlantic European than of Rhineland +origin. A few simple habitation sites and many burials of the Beaker +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 +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. + +[Illustration: BEAKER] + +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 +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 +the food-vessels seem to have been heavily involved in trade. It is +quite proper to wonder whether the food-vessel pottery itself was made +by local women who were married to traders who were middlemen in the +transmission of Irish metal objects to north Germany and Scandinavia. +The belt of high, relatively woodless country, from southwest to +northeast, was already established as a natural route for inland trade. + + +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 +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 +of amber to Mycenean Greece or even Egypt, as evidenced by glazed blue +beads. + +The great visual trace of Wessex achievement is the final form of +the spectacular sanctuary at Stonehenge. A wooden henge or circular +monument was first made several hundred years earlier, but the site +now received its great circles of stone uprights and lintels. The +diameter of the surrounding ditch at Stonehenge is about 350 feet, the +diameter of the inner circle of large stones is about 100 feet, and +the tallest stone of the innermost horseshoe-shaped enclosure is 29 +feet 8 inches high. One circle is made of blue stones which must have +been transported from Pembrokeshire, 145 miles away as the crow flies. +Recently, many carvings representing the profile of a standard type of +bronze axe of the time, and several profiles of bronze daggers--one of +which has been called Mycenean in type--have been found carved in the +stones. We cannot, of course, describe the details of the religious +ceremonies which must have been staged in Stonehenge, but we can +certainly imagine the well-integrated and smoothly working culture +which must have been necessary before such a great monument could have +been built. + + +�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� +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 +marked. The different aspects of the assemblages of the Beaker folk and +the clearly expressed activities of the Food-vessel folk and the Wessex +warriors show that Britain was already taking on her characteristic +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. +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 +encountered traders who traveled the great north-south European road, +by which Baltic amber moved southward to Greece and the Levant, and +ideas and things moved northward again. + +There was, however, the Channel between England and Europe, and this +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, +the far north, and the distant islands not yet fully touched by +food-production. The �personality of Britain� was being formed. + + +CREMATION BURIALS BEGIN + +Along with people of certain religious faiths, archeologists are +against cremation (for other people!). Individuals to be cremated seem +in past times to have been dressed in their trappings and put upon a +large pyre: it takes a lot of wood and a very hot fire for a thorough +cremation. When the burning had been completed, the few fragile scraps +of bone and such odd beads of stone or other rare items as had resisted +the great heat seem to have been whisked into a pot and the pot buried. +The archeologist is left with the pot and the unsatisfactory scraps in +it. + +Tentatively, after about 1400 B.C. and almost completely over the whole +island by 1200 B.C., Britain became the scene of cremation burials +in urns. We know very little of the people themselves. None of their +settlements have been identified, although there is evidence that they +grew barley and made enclosures for cattle. The urns used for the +burials seem to have antecedents in the pottery of the Food-vessel +folk, and there are some other links with earlier British traditions. +In Lancashire, a wooden circle seems to have been built about a grave +with cremated burials in urns. Even occasional instances of cremation +may be noticed earlier in Britain, and it is not clear what, if any, +connection the British cremation burials in urns have with the classic +_Urnfields_ which were now beginning in the east Mediterranean and +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 +until after 900 B.C. and are considered to be a specialized highland +�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. + +Granting that our knowledge of this phase of British prehistory is +very restricted because the cremations have left so little for the +archeologist, it does not appear that the cremation-burial-urn folk can +be sharply set off from their immediate predecessors. But change on a +grander scale was on the way. + + +REVERBERATIONS FROM CENTRAL EUROPE + +In the centuries immediately following 1000 B.C., we see with fair +clarity two phases of a cultural process which must have been going +on for some time. Certainly several of the invasions we have already +described in this chapter were due to earlier phases of the same +cultural process, but we could not see the details. + +[Illustration: SLASHING SWORD] + +Around 1200 B.C. central Europe was upset by the spread of the +so-called Urnfield folk, who practiced cremation burial in urns and +whom we also know to have been possessors of long, slashing swords and +the horse. I told you above that we have no idea that the Urnfield +folk proper were in any way connected with the people who made +cremation-burial-urn cemeteries a century or so earlier in Britain. It +has been supposed that the Urnfield folk themselves may have shared +ideas with the people who sacked Troy. We know that the Urnfield +pressure from central Europe displaced other people in northern France, +and perhaps in northwestern Germany, and that this reverberated into +Britain about 1000 B.C. + +Soon after 750 B.C., the same thing happened again. This time, the +pressure from central Europe came from the Hallstatt folk who were iron +tool makers: the reverberation brought people from the western Alpine +region across the Channel into Britain. + +At first it is possible to see the separate results of these folk +movements, but the developing cultures soon fused with each other and +with earlier British elements. Presently there were also strains of +other northern and western European pottery and traces of Urnfield +practices themselves which appeared in the finished British product. I +hope you will sense that I am vastly over-simplifying the details. + +The result seems to have been--among other things--a new kind of +agricultural system. The land was marked off by ditched divisions. +Rectangular fields imply the plow rather than hoe cultivation. We seem +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 +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.� + +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 +parts of Wales, Scotland, and the Hebrides. + + +THE COMING OF IRON + +Iron--once the know-how of reducing it from its ore in a very hot, +closed fire has been achieved--produces a far cheaper and much more +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 +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 +phase. + +A few iron users reached Britain a little before 400 B.C. Not long +after that, a number of allied groups appeared in southern and +southeastern England. They came over the Channel from France and must +have been Celts with dialects related to those already in England. A +second wave of Celts arrived from the Marne district in France about +250 B.C. Finally, in the second quarter of the first century B.C., +there were several groups of newcomers, some of whom were Belgae of +a mixed Teutonic-Celtic confederacy of tribes in northern France and +Belgium. The Belgae preceded the Romans by only a few years. + + +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 +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. + +At Little Woodbury, near the town of Salisbury, a farmstead has been +rather completely excavated. The rustic buildings were within a +palisade, the round house itself was built of wood, and there were +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. + +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 +restlessness being shown by remote relatives in France. + + +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 +type and also the two-wheeled chariot. Like the Wessex warriors of +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] + +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 +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 +middle of the first century B.C., which was a troubled time in Britain. +The circular houses were built on timber platforms surrounded with +palisades. The preservation of antiquities by the water-logged peat of +the swamp has yielded us a long catalogue of the materials of these +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� +(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. + + +THE THIRD PHASE + +For the time of about the middle of the first century B.C., we again +see traces of frantic hill-fort construction. This simple military +architecture now took some new forms. Its multiple ramparts must +reflect the use of slings as missiles, rather than spears. We probably +know the reason. In 56 B.C., Julius Caesar chastised the Veneti of +Brittany for outraging the dignity of Roman ambassadors. The Veneti +were famous slingers, and doubtless the reverberations of escaping +Veneti were felt across the Channel. The military architecture suggests +that some Veneti did escape to Britain. + +Also, through Caesar, we learn the names of newcomers who arrived in +two waves, about 75 B.C. and about 50 B.C. These were the Belgae. Now, +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. +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 +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 +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.� + +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 +his own is able to describe the ways of men in the past. + + +BRITAIN AS A SAMPLE OF THE GENERAL COURSE OF PREHISTORY IN EUROPE + +In giving this very brief outline of the later prehistory of Britain, +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 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. + +The whole course of later prehistory in Europe is, in fact, so very +complicated that there is no single good book to cover it all; +certainly there is none in English. There are some good regional +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, +cultures, and sub-cultures--which begin to appear as history +proceeds--were in themselves very complicated. + +Hence, I selected the case of Britain as a single example of how +prehistory ends in Europe. It could have been more complicated than we +found it to be. Even in the subject matter on Britain in the chapter +before the last, we did not see direct traces of the effect on Britain +of the very important developments which took place in the Danubian +way from the Near East. Apparently Britain was not affected. Britain +received the impulses which brought copper, bronze, and iron tools from +an original east Mediterranean homeland into Europe, almost at the ends +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 +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 +how the personality of Britain was formed. The principles that Fox +used, in trying to balance cultural and environmental factors and +interrelationships would not be greatly different for other lands. + + + + +Summary + +[Illustration] + + +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 +left only 1 per cent of the story for the historians to tell. + + +THE DRAMA OF THE PAST + +Men first became men when evolution had carried them to a certain +point. This was the point where the eye-hand-brain co-ordination was +good enough so that tools could be made. When tools began to be made +according to sets of lasting habits, we know that men had appeared. +This happened over a half million years ago. The stage for the play +may have been as broad as all of Europe, Africa, and Asia. At least, +it seems unlikely that it was only one little region that saw the +beginning of the drama. + +Glaciers and different climates came and went, to change the settings. +But the play went on in the same first act for a very long time. The +men who were the players had simple roles. They had to feed themselves +and protect themselves as best they could. They did this by hunting, +catching, and finding food wherever they could, and by taking such +protection as caves, fire, and their simple tools would give them. +Before the first act was over, the last of the glaciers was melting +away, and the players had added the New World to their stage. If +we want a special name for the first act, we could call it _The +Food-Gatherers_. + +There were not many climaxes in the first act, so far as we can see. +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 activity that were over and above pure food-getting. + + +THE SECOND ACT + +The second act began only about ten thousand years ago. A few of the +players started it by themselves near the center of the Old World part +of the stage, in the Near East. It began as a plant and animal act, but +it soon became much more complicated. + +But the players in this one part of the stage--in the Near East--were +not the only ones to start off on the second act by themselves. Other +players, possibly in several places in the Far East, and certainly in +the New World, also started second acts that began as plant and animal +acts, and then became complicated. We can call the whole second act +_The Food-Producers_. + + +THE FIRST GREAT CLIMAX OF THE SECOND ACT + +In the Near East, the first marked climax of the second act happened +in Mesopotamia and Egypt. The play and the players reached that great +climax that we call civilization. This seems to have come less than +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 +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 +some of these players. They seem perfectly happy to keep on with the +first act. + +The second act moved from climax to climax. The civilizations of +Mesopotamia and Egypt were only the earliest of these climaxes. The +players to the west caught the spirit of the thing, and climaxes +followed there. So also did climaxes come in the Far Eastern and New +World portions of the stage. + +The greater part of the second act should really be described to you +by a historian. Although it was a very short act when compared to the +first one, the climaxes complicate it a great deal. I, a prehistorian, +have told you about only the first act, and the very beginning of the +second. + + +THE THIRD ACT + +Also, as a prehistorian I probably should not even mention the third +act--it began so recently. The third act is _The Industrialization_. +It is the one in which we ourselves are players. If the pace of the +second act was so much faster than that of the first, the pace of the +third act is terrific. The danger is that it may wear down the players +completely. + +What sort of climaxes will the third act have, and are we already in +one? You have seen by now that the acts of my play are given in terms +of modes or basic patterns of human economy--ways in which people +get food and protection and safety. The climaxes involve more than +human economy. Economics and technological factors may be part of the +climaxes, but they are not all. The climaxes may be revolutions in +their own way, intellectual and social revolutions if you like. + +If the third act follows the pattern of the second act, a climax should +come soon after the act begins. We may be due for one soon if we are +not already in it. Remember the terrific pace of this third act. + + +WHY BOTHER WITH PREHISTORY? + +Why do we bother about prehistory? The main reason is that we think it +may point to useful ideas for the present. We are in the troublesome +beginnings of the third act of the play. The beginnings of the second +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. +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 +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 +another. + +By the time the written records of Mesopotamia and Egypt begin to turn +up in number, the climaxes there are well under way. To understand the +beginnings of the climaxes, and the real beginnings of the second act +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 +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, +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. + + + + +List of Books + + +BOOKS OF GENERAL INTEREST + +(Chosen from a variety of the increasingly useful list of cheap +paperbound books.) + + Childe, V. Gordon + _What Happened in History._ 1954. Penguin. + _Man Makes Himself._ 1955. Mentor. + _The Prehistory of European Society._ 1958. Penguin. + + Dunn, L. C., and Dobzhansky, Th. + _Heredity, Race, and Society._ 1952. Mentor. + + Frankfort, Henri, Frankfort, H. A., Jacobsen, Thorkild, and Wilson, + John A. + _Before Philosophy._ 1954. Penguin. + + Simpson, George G. + _The Meaning of Evolution._ 1955. Mentor. + + Wheeler, Sir Mortimer + _Archaeology from the Earth._ 1956. Penguin. + + +GEOCHRONOLOGY AND THE ICE AGE + +(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.) + + Flint, R. F. + _Glacial Geology and the Pleistocene Epoch._ 1947. John Wiley + and Sons. + + Zeuner, F. E. + _Dating the Past._ 1952 (3rd ed.). Methuen and Co. + + +FOSSIL MEN AND RACE + +(The points of view of physical anthropologists and human +paleontologists are changing very quickly. Two of the different points +of view are listed here.) + + Clark, W. E. Le Gros + _History of the Primates._ 1956 (5th ed.). British Museum + (Natural History). (Also in Phoenix edition, 1957.) + + Howells, W. W. + _Mankind So Far._ 1944. Doubleday, Doran. + + +GENERAL ANTHROPOLOGY + +(These are standard texts not absolutely up to date in every detail, or +interpretative essays concerned with cultural change through time as +well as in space.) + + Kroeber, A. L. + _Anthropology._ 1948. Harcourt, Brace. + + Linton, Ralph + _The Tree of Culture._ 1955. Alfred A. Knopf, Inc. + + Redfield, Robert + _The Primitive World and Its Transformations._ 1953. Cornell + University Press. + + Steward, Julian H. + _Theory of Culture Change._ 1955. University of Illinois Press. + + White, Leslie + _The Science of Culture._ 1949. Farrar, Strauss. + + +GENERAL PREHISTORY + +(A sampling of the more useful and current standard works in English.) + + Childe, V. Gordon + _The Dawn of European Civilization._ 1957. Kegan Paul, Trench, + Trubner. + _Prehistoric Migrations in Europe._ 1950. Instituttet for + Sammenlignende Kulturforskning. + + Clark, Grahame + _Archaeology and Society._ 1957. Harvard University Press. + + Clark, J. G. D. + _Prehistoric Europe: The Economic Basis._ 1952. Methuen and Co. + + Garrod, D. A. E. + _Environment, Tools, and Man._ 1946. Cambridge University + Press. + + Movius, Hallam L., Jr. + �Old World Prehistory: Paleolithic� in _Anthropology Today_. + Kroeber, A. L., ed. 1953. University of Chicago Press. + + Oakley, Kenneth P. + _Man the Tool-Maker._ 1956. British Museum (Natural History). + (Also in Phoenix edition, 1957.) + + Piggott, Stuart + _British Prehistory._ 1949. Oxford University Press. + + Pittioni, Richard + _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.) + + +THE NEAR EAST + + Adams, Robert M. + �Developmental Stages in Ancient Mesopotamia,� _in_ Steward, + Julian, _et al_, _Irrigation Civilizations: A Comparative + Study_. 1955. Pan American Union. + + Braidwood, Robert J. + _The Near East and the Foundations for Civilization._ 1952. + University of Oregon. + + Childe, V. Gordon + _New Light on the Most Ancient East._ 1952. Oriental Dept., + Routledge and Kegan Paul. + + Frankfort, Henri + _The Birth of Civilization in the Near East._ 1951. University + of Indiana Press. (Also in Anchor edition, 1956.) + + Pallis, Svend A. + _The Antiquity of Iraq._ 1956. Munksgaard. + + Wilson, John A. + _The Burden of Egypt._ 1951. University of Chicago Press. (Also + in Phoenix edition, called _The Culture of Ancient Egypt_, + 1956.) + + +HOW DIGGING IS DONE + + Braidwood, Linda + _Digging beyond the Tigris._ 1953. Schuman, New York. + + Wheeler, Sir Mortimer + _Archaeology from the Earth._ 1954. Oxford, London. + + + + +Index + + + Abbevillian, 48; + core-biface tool, 44, 48 + + Acheulean, 48, 60 + + Acheuleo-Levalloisian, 63 + + Acheuleo-Mousterian, 63 + + Adams, R. M., 106 + + Adzes, 45 + + Africa, east, 67, 89; + north, 70, 89; + south, 22, 25, 34, 40, 67 + + Agriculture, incipient, in England, 140; + in Near East, 123 + + Ain Hanech, 48 + + Amber, taken from Baltic to Greece, 167 + + American Indians, 90, 142 + + Anatolia, used as route to Europe, 138 + + Animals, in caves, 54, 64; + in cave art, 85 + + Antevs, Ernst, 19 + + Anyathian, 47 + + Archeological interpretation, 8 + + Archeology, defined, 8 + + Architecture, at Jarmo, 128; + at Jericho, 133 + + Arrow, points, 94; + shaft straightener, 83 + + Art, in caves, 84; + East Spanish, 85; + figurines, 84; + Franco-Cantabrian, 84, 85; + movable (engravings, modeling, scratchings), 83; + painting, 83; + sculpture, 83 + + Asia, western, 67 + + Assemblage, defined, 13, 14; + European, 94; + Jarmo, 129; + Maglemosian, 94; + Natufian, 113 + + Aterian, industry, 67; + point, 89 + + Australopithecinae, 24 + + Australopithecine, 25, 26 + + Awls, 77 + + Axes, 62, 94 + + Ax-heads, 15 + + Azilian, 97 + + Aztecs, 145 + + + Baghouz, 152 + + Bakun, 134 + + Baltic sea, 93 + + Banana, 107 + + Barley, wild, 108 + + Barrow, 141 + + Battle-axe folk, 164; + assemblage, 164 + + Beads, 80; + bone, 114 + + Beaker folk, 164; + assemblage, 164-165 + + Bear, in cave art, 85; + cult, 68 + + Belgium, 94 + + Belt cave, 126 + + Bering Strait, used as route to New World, 98 + + Bison, in cave art, 85 + + Blade, awl, 77; + backed, 75; + blade-core, 71; + end-scraper, 77; + stone, defined, 71; + strangulated (notched), 76; + tanged point, 76; + tools, 71, 75-80, 90; + tool tradition, 70 + + Boar, wild, in cave art, 85 + + Bogs, source of archeological materials, 94 + + Bolas, 54 + + Bordes, Fran�ois, 62 + + Borer, 77 + + Boskop skull, 34 + + Boyd, William C., 35 + + Bracelets, 118 + + Brain, development of, 24 + + Breadfruit, 107 + + Breasted, James H., 107 + + Brick, at Jericho, 133 + + Britain, 94; + late prehistory, 163-175; + invaders, 173 + + Broch, 172 + + Buffalo, in China, 54; + killed by stampede, 86 + + Burials, 66, 86; + in �henges,� 164; + in urns, 168 + + Burins, 75 + + Burma, 90 + + Byblos, 134 + + + Camel, 54 + + Cannibalism, 55 + + Cattle, wild, 85, 112; + in cave art, 85; + domesticated, 15; + at Skara Brae, 142 + + Caucasoids, 34 + + Cave men, 29 + + Caves, 62; + art in, 84 + + Celts, 170 + + Chariot, 160 + + Chicken, domestication of, 107 + + Chiefs, in food-gathering groups, 68 + + Childe, V. Gordon, 8 + + China, 136 + + Choukoutien, 28, 35 + + Choukoutienian, 47 + + Civilization, beginnings, 144, 149, 157; + meaning of, 144 + + Clactonian, 45, 47 + + Clay, used in modeling, 128; + baked, used for tools, 153 + + Club-heads, 82, 94 + + Colonization, in America, 142; + in Europe, 142 + + Combe Capelle, 30 + + Combe Capelle-Br�nn group, 34 + + Commont, Victor, 51 + + Coon, Carlton S., 73 + + Copper, 134 + + Corn, in America, 145 + + Corrals for cattle, 140 + + �Cradle of mankind,� 136 + + Cremation, 167 + + Crete, 162 + + Cro-Magnon, 30, 34 + + Cultivation, incipient, 105, 109, 111 + + Culture, change, 99; + characteristics, defined, 38, 49; + prehistoric, 39 + + + Danube Valley, used as route from Asia, 138 + + Dates, 153 + + Deer, 54, 96 + + Dog, domesticated, 96 + + Domestication, of animals, 100, 105, 107; + of plants, 100 + + �Dragon teeth� fossils in China, 28 + + Drill, 77 + + Dubois, Eugene, 26 + + + Early Dynastic Period, Mesopotamia, 147 + + East Spanish art, 72, 85 + + Egypt, 70, 126 + + Ehringsdorf, 31 + + Elephant, 54 + + Emiliani, Cesare, 18 + + Emiran flake point, 73 + + England, 163-168; + prehistoric, 19, 40; + farmers in, 140 + + Eoanthropus dawsoni, 29 + + Eoliths, 41 + + Erich, 152 + + Eridu, 152 + + Euphrates River, floods in, 148 + + Europe, cave dwellings, 58; + at end of Ice Age, 93; + early farmers, 140; + glaciers in, 40; + huts in, 86; + routes into, 137-140; + spread of food-production to, 136 + + + Far East, 69, 90 + + Farmers, 103 + + Fauresmith industry, 67 + + Fayum, 135; + radiocarbon date, 146 + + �Fertile Crescent,� 107, 146 + + Figurines, �Venus,� 84; + at Jarmo, 128; + at Ubaid, 153 + + Fire, used by Peking man, 54 + + First Dynasty, Egypt, 147 + + Fish-hooks, 80, 94 + + Fishing, 80; + by food-producers, 122 + + Fish-lines, 80 + + Fish spears, 94 + + Flint industry, 127 + + Font�chevade, 32, 56, 58 + + Food-collecting, 104, 121; + end of, 104 + + Food-gatherers, 53, 176 + + Food-gathering, 99, 104; + in Old World, 104; + stages of, 104 + + Food-producers, 176 + + Food-producing economy, 122; + in America, 145; + in Asia, 105 + + Food-producing revolution, 99, 105; + causes of, 101; + preconditions for, 100 + + Food-production, beginnings of, 99; + carried to Europe, 110 + + Food-vessel folk, 164 + + �Forest folk,� 97, 98, 104, 110 + + Fox, Sir Cyril, 174 + + France, caves in, 56 + + + Galley Hill (fossil type), 29 + + Garrod, D. A., 73 + + Gazelle, 114 + + Germany, 94 + + Ghassul, 156 + + Glaciers, 18, 30; + destruction by, 40 + + Goat, wild, 108; + domesticated, 128 + + Grain, first planted, 20 + + Graves, passage, 141; + gallery, 141 + + Greece, civilization in, 163; + as route to western Europe, 138; + towns in, 162 + + Grimaldi skeletons, 34 + + + Hackberry seeds used as food, 55 + + Halaf, 151; + assemblage, 151 + + Hallstatt, tradition, 169 + + Hand, development of, 24, 25 + + Hand adzes, 46 + + Hand axes, 44 + + Harpoons, antler, 83, 94; + bone, 82, 94 + + Hassuna, 131; + assemblage, 131, 132 + + Heidelberg, fossil type, 28 + + Hill-forts, in England, 171; + in Scotland, 172 + + Hilly flanks of Near East, 107, 108, 125, 131, 146, 147 + + History, beginning of, 7, 17 + + Hoes, 112 + + Holland, 164 + + Homo sapiens, 32 + + Hooton, E. A., 34 + + Horse, 112; + wild, in cave art, 85; + in China, 54 + + Hotu cave, 126 + + Houses, 122; + at Jarmo, 128; + at Halaf, 151 + + Howe, Bruce, 116 + + Howell, F. Clark, 30 + + Hunting, 93 + + + Ice Age, in Asia, 99; + beginning of, 18; + glaciers in, 41; + last glaciation, 93 + + Incas, 145 + + India, 90, 136 + + Industrialization, 178 + + Industry, blade-tool, 88; + defined, 58; + ground stone, 94 + + Internationalism, 162 + + Iran, 107, 147 + + Iraq, 107, 124, 127, 136, 147 + + Iron, introduction of, 170 + + Irrigation, 123, 149, 155 + + Italy, 138 + + + Jacobsen, T. J., 157 + + Jarmo, 109, 126, 128, 130; + assemblage, 129 + + Java, 23, 29 + + Java man, 26, 27, 29 + + Jefferson, Thomas, 11 + + Jericho, 119, 133 + + Judaidah, 134 + + + Kafuan, 48 + + Kanam, 23, 36 + + Karim Shahir, 116-119, 124; + assemblage, 116, 117 + + Keith, Sir Arthur, 33 + + Kelley, Harper, 51 + + Kharga, 126 + + Khartoum, 136 + + Knives, 80 + + Krogman, W. M., 3, 25 + + + Lamps, 85 + + Land bridges in Mediterranean, 19 + + La T�ne phase, 170 + + Laurel leaf point, 78, 89 + + Leakey, L. S. B., 40 + + Le Moustier, 57 + + Levalloisian, 47, 61, 62 + + Levalloiso-Mousterian, 47, 63 + + Little Woodbury, 170 + + + Magic, used by hunters, 123 + + Maglemosian, assemblage, 94, 95; + folk, 98 + + Makapan, 40 + + Mammoth, 93; + in cave art, 85 + + �Man-apes,� 26 + + Mango, 107 + + Mankind, age, 17 + + Maringer, J., 45 + + Markets, 155 + + Marston, A. T., 11 + + Mathiassen, T., 97 + + McCown, T. D., 33 + + Meganthropus, 26, 27, 36 + + Men, defined, 25; + modern, 32 + + Merimde, 135 + + Mersin, 133 + + Metal-workers, 160, 163, 167, 172 + + Micoquian, 48, 60 + + Microliths, 87; + at Jarmo, 130; + �lunates,� 87; + trapezoids, 87; + triangles, 87 + + Minerals used as coloring matter, 66 + + Mine-shafts, 140 + + M�lefaat, 126, 127 + + Mongoloids, 29, 90 + + Mortars, 114, 118, 127 + + Mounds, how formed, 12 + + Mount Carmel, 11, 33, 52, 59, 64, 69, 113, 114 + + �Mousterian man,� 64 + + �Mousterian� tools, 61, 62; + of Acheulean tradition, 62 + + Movius, H. L., 47 + + + Natufian, animals in, 114; + assemblage, 113, 114, 115; + burials, 114; + date of, 113 + + Neanderthal man, 29, 30, 31, 56 + + Near East, beginnings of civilization in, 20, 144; + cave sites, 58; + climate in Ice Age, 99; + �Fertile Crescent,� 107, 146; + food-production in, 99; + Natufian assemblage in, 113-115; + stone tools, 114 + + Needles, 80 + + Negroid, 34 + + New World, 90 + + Nile River valley, 102, 134; + floods in, 148 + + Nuclear area, 106, 110; + in Near East, 107 + + + Obsidian, used for blade tools, 71; + at Jarmo, 130 + + Ochre, red, with burials, 86 + + Oldowan, 48 + + Old World, 67, 70, 90; + continental phases in, 18 + + Olorgesailie, 40, 51 + + Ostrich, in China, 54 + + Ovens, 128 + + Oxygen isotopes, 18 + + + Paintings in caves, 83 + + Paleoanthropic man, 50 + + Palestine, burials, 56; + cave sites, 52; + types of man, 69 + + Parpallo, 89 + + Patjitanian, 45, 47 + + Pebble tools, 42 + + Peking cave, 54; + animals in, 54 + + Peking man, 27, 28, 29, 54, 58 + + Pendants, 80; + bone, 114 + + Pestle, 114 + + Peterborough, 141; + assemblage, 141 + + Pictographic signs, 158 + + Pig, wild, 108 + + �Piltdown man,� 29 + + Pins, 80 + + Pithecanthropus, 26, 27, 30, 36 + + Pleistocene, 18, 25 + + Plows developed, 123 + + Points, arrow, 76; + laurel leaf, 78; + shouldered, 78, 79; + split-based bone, 80, 82; + tanged, 76; + willow leaf, 78 + + Potatoes, in America, 145 + + Pottery, 122, 130, 156; + decorated, 142; + painted, 131, 151, 152; + Susa style, 156; + in tombs, 141 + + Prehistory, defined, 7; + range of, 18 + + Pre-neanderthaloids, 30, 31, 37 + + Pre-Solutrean point, 89 + + Pre-Stellenbosch, 48 + + Proto-Literate assemblage, 157-160 + + + Race, 35; + biological, 36; + �pure,� 16 + + Radioactivity, 9, 10 + + Radioactive carbon dates, 18, 92, 120, 130, 135, 156 + + Redfield, Robert, 38, 49 + + Reed, C. A., 128 + + Reindeer, 94 + + Rhinoceros, 93; + in cave art, 85 + + Rhodesian man, 32 + + Riss glaciation, 58 + + Rock-shelters, 58; + art in, 85 + + + Saccopastore, 31 + + Sahara Desert, 34, 102 + + Samarra, 152; + pottery, 131, 152 + + Sangoan industry, 67 + + Sauer, Carl, 136 + + Sbaikian point, 89 + + Schliemann, H., 11, 12 + + Scotland, 171 + + Scraper, flake, 79; + end-scraper on blade, 77, 78; + keel-shaped, 79, 80, 81 + + Sculpture in caves, 83 + + Sebilian III, 126 + + Shaheinab, 135 + + Sheep, wild, 108; + at Skara Brae, 142; + in China, 54 + + Shellfish, 142 + + Ship, Ubaidian, 153 + + Sialk, 126, 134; + assemblage, 134 + + Siberia, 88; + pathway to New World, 98 + + Sickle, 112, 153; + blade, 113, 130 + + Silo, 122 + + Sinanthropus, 27, 30, 35 + + Skara Brae, 142 + + Snails used as food, 128 + + Soan, 47 + + Solecki, R., 116 + + Solo (fossil type), 29, 32 + + Solutrean industry, 77 + + Spear, shaft, 78; + thrower, 82, 83 + + Speech, development of organs of, 25 + + Squash, in America, 145 + + Steinheim fossil skull, 28 + + Stillbay industry, 67 + + Stonehenge, 166 + + Stratification, in caves, 12, 57; + in sites, 12 + + Swanscombe (fossil type), 11, 28 + + Syria, 107 + + + Tabun, 60, 71 + + Tardenoisian, 97 + + Taro, 107 + + Tasa, 135 + + Tayacian, 47, 59 + + Teeth, pierced, in beads and pendants, 114 + + Temples, 123, 155 + + Tepe Gawra, 156 + + Ternafine, 29 + + Teshik Tash, 69 + + Textiles, 122 + + Thong-stropper, 80 + + Tigris River, floods in, 148 + + Toggle, 80 + + Tomatoes, in America, 145 + + Tombs, megalithic, 141 + + Tool-making, 42, 49 + + Tool-preparation traditions, 65 + + Tools, 62; + antler, 80; + blade, 70, 71, 75; + bone, 66; + chopper, 47; + core-biface, 43, 48, 60, 61; + flake, 44, 47, 51, 60, 64; + flint, 80, 127; + ground stone, 68, 127; + handles, 94; + pebble, 42, 43, 48, 53; + use of, 24 + + Touf (mud wall), 128 + + Toynbee, A. J., 101 + + Trade, 130, 155, 162 + + Traders, 167 + + Traditions, 15; + blade tool, 70; + definition of, 51; + interpretation of, 49; + tool-making, 42, 48; + chopper-tool, 47; + chopper-chopping tool, 45; + core-biface, 43, 48; + flake, 44, 47; + pebble tool, 42, 48 + + Tool-making, prehistory of, 42 + + Turkey, 107, 108 + + + Ubaid, 153; + assemblage, 153-155 + + Urnfields, 168, 169 + + + Village-farming community era, 105, 119 + + + Wad B, 72 + + Wadjak, 34 + + Warka phase, 156; + assemblage, 156 + + Washburn, Sherwood L., 36 + + Water buffalo, domestication of, 107 + + Weidenreich, F., 29, 34 + + Wessex, 166, 167 + + Wheat, wild, 108; + partially domesticated, 127 + + Willow leaf point, 78 + + Windmill Hill, 138; + assemblage, 138, 140 + + Witch doctors, 68 + + Wool, 112; + in garments, 167 + + Writing, 158; + cuneiform, 158 + + W�rm I glaciation, 58 + + + Zebu cattle, domestication of, 107 + + Zeuner, F. E., 73 + + + + + * * * * * * + + + + +Transcriber�s note: + +Punctuation, hyphenation, and spelling were made consistent when a +predominant preference was found in this book; otherwise they were not +changed. + +Simple typographical errors were corrected; occasional unbalanced +quotation marks retained. + +Ambiguous hyphens at the ends of lines were retained. + +Index not checked for proper alphabetization or correct page references. + +In the original book, chapter headings were accompanied by +illustrations, sometimes above, sometimes below, and sometimes +adjacent. In this eBook those ilustrations always appear below the +headings. + + + +***END OF THE PROJECT GUTENBERG EBOOK PREHISTORIC MEN*** + + +******* This file should be named 52664-0.txt or 52664-0.zip ******* + + +This and all associated files of various formats will be found in: +http://www.gutenberg.org/dirs/5/2/6/6/52664 + + +Updated editions will replace the previous one--the old editions will +be renamed. + +Creating the works from print editions not protected by U.S. copyright +law means that no one owns a United States copyright in these works, +so the Foundation (and you!) can copy and distribute it in the United +States without permission and without paying copyright +royalties. Special rules, set forth in the General Terms of Use part +of this license, apply to copying and distributing Project +Gutenberg-tm electronic works to protect the PROJECT GUTENBERG-tm +concept and trademark. Project Gutenberg is a registered trademark, +and may not be used if you charge for the eBooks, unless you receive +specific permission. If you do not charge anything for copies of this +eBook, complying with the rules is very easy. You may use this eBook +for nearly any purpose such as creation of derivative works, reports, +performances and research. They may be modified and printed and given +away--you may do practically ANYTHING in the United States with eBooks +not protected by U.S. copyright law. Redistribution is subject to the +trademark license, especially commercial redistribution. + +START: FULL LICENSE + +THE FULL PROJECT GUTENBERG LICENSE +PLEASE READ THIS BEFORE YOU DISTRIBUTE OR USE THIS WORK + +To protect the Project Gutenberg-tm mission of promoting the free +distribution of electronic works, by using or distributing this work +(or any other work associated in any way with the phrase "Project +Gutenberg"), you agree to comply with all the terms of the Full +Project Gutenberg-tm License available with this file or online at +www.gutenberg.org/license. + +Section 1. General Terms of Use and Redistributing Project +Gutenberg-tm electronic works + +1.A. By reading or using any part of this Project Gutenberg-tm +electronic work, you indicate that you have read, understand, agree to +and accept all the terms of this license and intellectual property +(trademark/copyright) agreement. If you do not agree to abide by all +the terms of this agreement, you must cease using and return or +destroy all copies of Project Gutenberg-tm electronic works in your +possession. If you paid a fee for obtaining a copy of or access to a +Project Gutenberg-tm electronic work and you do not agree to be bound +by the terms of this agreement, you may obtain a refund from the +person or entity to whom you paid the fee as set forth in paragraph +1.E.8. + +1.B. "Project Gutenberg" is a registered trademark. It may only be +used on or associated in any way with an electronic work by people who +agree to be bound by the terms of this agreement. There are a few +things that you can do with most Project Gutenberg-tm electronic works +even without complying with the full terms of this agreement. See +paragraph 1.C below. There are a lot of things you can do with Project +Gutenberg-tm electronic works if you follow the terms of this +agreement and help preserve free future access to Project Gutenberg-tm +electronic works. See paragraph 1.E below. + +1.C. The Project Gutenberg Literary Archive Foundation ("the +Foundation" or PGLAF), owns a compilation copyright in the collection +of Project Gutenberg-tm electronic works. Nearly all the individual +works in the collection are in the public domain in the United +States. If an individual work is unprotected by copyright law in the +United States and you are located in the United States, we do not +claim a right to prevent you from copying, distributing, performing, +displaying or creating derivative works based on the work as long as +all references to Project Gutenberg are removed. Of course, we hope +that you will support the Project Gutenberg-tm mission of promoting +free access to electronic works by freely sharing Project Gutenberg-tm +works in compliance with the terms of this agreement for keeping the +Project Gutenberg-tm name associated with the work. You can easily +comply with the terms of this agreement by keeping this work in the +same format with its attached full Project Gutenberg-tm License when +you share it without charge with others. + +1.D. The copyright laws of the place where you are located also govern +what you can do with this work. Copyright laws in most countries are +in a constant state of change. If you are outside the United States, +check the laws of your country in addition to the terms of this +agreement before downloading, copying, displaying, performing, +distributing or creating derivative works based on this work or any +other Project Gutenberg-tm work. The Foundation makes no +representations concerning the copyright status of any work in any +country outside the United States. + +1.E. Unless you have removed all references to Project Gutenberg: + +1.E.1. The following sentence, with active links to, or other +immediate access to, the full Project Gutenberg-tm License must appear +prominently whenever any copy of a Project Gutenberg-tm work (any work +on which the phrase "Project Gutenberg" appears, or with which the +phrase "Project Gutenberg" is associated) is accessed, displayed, +performed, viewed, copied or distributed: + + 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 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 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. + +1.E.2. If an individual Project Gutenberg-tm electronic work is +derived from texts not protected by U.S. copyright law (does not +contain a notice indicating that it is posted with permission of the +copyright holder), the work can be copied and distributed to anyone in +the United States without paying any fees or charges. If you are +redistributing or providing access to a work with the phrase "Project +Gutenberg" associated with or appearing on the work, you must comply +either with the requirements of paragraphs 1.E.1 through 1.E.7 or +obtain permission for the use of the work and the Project Gutenberg-tm +trademark as set forth in paragraphs 1.E.8 or 1.E.9. + +1.E.3. If an individual Project Gutenberg-tm electronic work is posted +with the permission of the copyright holder, your use and distribution +must comply with both paragraphs 1.E.1 through 1.E.7 and any +additional terms imposed by the copyright holder. Additional terms +will be linked to the Project Gutenberg-tm License for all works +posted with the permission of the copyright holder found at the +beginning of this work. + +1.E.4. Do not unlink or detach or remove the full Project Gutenberg-tm +License terms from this work, or any files containing a part of this +work or any other work associated with Project Gutenberg-tm. + +1.E.5. Do not copy, display, perform, distribute or redistribute this +electronic work, or any part of this electronic work, without +prominently displaying the sentence set forth in paragraph 1.E.1 with +active links or immediate access to the full terms of the Project +Gutenberg-tm License. + +1.E.6. You may convert to and distribute this work in any binary, +compressed, marked up, nonproprietary or proprietary form, including +any word processing or hypertext form. However, if you provide access +to or distribute copies of a Project Gutenberg-tm work in a format +other than "Plain Vanilla ASCII" or other format used in the official +version posted on the official Project Gutenberg-tm web site +(www.gutenberg.org), you must, at no additional cost, fee or expense +to the user, provide a copy, a means of exporting a copy, or a means +of obtaining a copy upon request, of the work in its original "Plain +Vanilla ASCII" or other form. Any alternate format must include the +full Project Gutenberg-tm License as specified in paragraph 1.E.1. + +1.E.7. Do not charge a fee for access to, viewing, displaying, +performing, copying or distributing any Project Gutenberg-tm works +unless you comply with paragraph 1.E.8 or 1.E.9. + +1.E.8. You may charge a reasonable fee for copies of or providing +access to or distributing Project Gutenberg-tm electronic works +provided that + +* You pay a royalty fee of 20% of the gross profits you derive from + the use of Project Gutenberg-tm works calculated using the method + you already use to calculate your applicable taxes. The fee is owed + to the owner of the Project Gutenberg-tm trademark, but he has + agreed to donate royalties under this paragraph to the Project + Gutenberg Literary Archive Foundation. Royalty payments must be paid + within 60 days following each date on which you prepare (or are + legally required to prepare) your periodic tax returns. Royalty + payments should be clearly marked as such and sent to the Project + Gutenberg Literary Archive Foundation at the address specified in + Section 4, "Information about donations to the Project Gutenberg + Literary Archive Foundation." + +* You provide a full refund of any money paid by a user who notifies + you in writing (or by e-mail) within 30 days of receipt that s/he + does not agree to the terms of the full Project Gutenberg-tm + License. You must require such a user to return or destroy all + copies of the works possessed in a physical medium and discontinue + all use of and all access to other copies of Project Gutenberg-tm + works. + +* You provide, in accordance with paragraph 1.F.3, a full refund of + any money paid for a work or a replacement copy, if a defect in the + electronic work is discovered and reported to you within 90 days of + receipt of the work. + +* You comply with all other terms of this agreement for free + distribution of Project Gutenberg-tm works. + +1.E.9. If you wish to charge a fee or distribute a Project +Gutenberg-tm electronic work or group of works on different terms than +are set forth in this agreement, you must obtain permission in writing +from both the Project Gutenberg Literary Archive Foundation and The +Project Gutenberg Trademark LLC, the owner of the Project Gutenberg-tm +trademark. Contact the Foundation as set forth in Section 3 below. + +1.F. + +1.F.1. Project Gutenberg volunteers and employees expend considerable +effort to identify, do copyright research on, transcribe and proofread +works not protected by U.S. copyright law in creating the Project +Gutenberg-tm collection. Despite these efforts, Project Gutenberg-tm +electronic works, and the medium on which they may be stored, may +contain "Defects," such as, but not limited to, incomplete, inaccurate +or corrupt data, transcription errors, a copyright or other +intellectual property infringement, a defective or damaged disk or +other medium, a computer virus, or computer codes that damage or +cannot be read by your equipment. + +1.F.2. LIMITED WARRANTY, DISCLAIMER OF DAMAGES - Except for the "Right +of Replacement or Refund" described in paragraph 1.F.3, the Project +Gutenberg Literary Archive Foundation, the owner of the Project +Gutenberg-tm trademark, and any other party distributing a Project +Gutenberg-tm electronic work under this agreement, disclaim all +liability to you for damages, costs and expenses, including legal +fees. YOU AGREE THAT YOU HAVE NO REMEDIES FOR NEGLIGENCE, STRICT +LIABILITY, BREACH OF WARRANTY OR BREACH OF CONTRACT EXCEPT THOSE +PROVIDED IN PARAGRAPH 1.F.3. YOU AGREE THAT THE FOUNDATION, THE +TRADEMARK OWNER, AND ANY DISTRIBUTOR UNDER THIS AGREEMENT WILL NOT BE +LIABLE TO YOU FOR ACTUAL, DIRECT, INDIRECT, CONSEQUENTIAL, PUNITIVE OR +INCIDENTAL DAMAGES EVEN IF YOU GIVE NOTICE OF THE POSSIBILITY OF SUCH +DAMAGE. + +1.F.3. LIMITED RIGHT OF REPLACEMENT OR REFUND - If you discover a +defect in this electronic work within 90 days of receiving it, you can +receive a refund of the money (if any) you paid for it by sending a +written explanation to the person you received the work from. If you +received the work on a physical medium, you must return the medium +with your written explanation. The person or entity that provided you +with the defective work may elect to provide a replacement copy in +lieu of a refund. If you received the work electronically, the person +or entity providing it to you may choose to give you a second +opportunity to receive the work electronically in lieu of a refund. If +the second copy is also defective, you may demand a refund in writing +without further opportunities to fix the problem. + +1.F.4. Except for the limited right of replacement or refund set forth +in paragraph 1.F.3, this work is provided to you 'AS-IS', WITH NO +OTHER WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PURPOSE. + +1.F.5. Some states do not allow disclaimers of certain implied +warranties or the exclusion or limitation of certain types of +damages. If any disclaimer or limitation set forth in this agreement +violates the law of the state applicable to this agreement, the +agreement shall be interpreted to make the maximum disclaimer or +limitation permitted by the applicable state law. The invalidity or +unenforceability of any provision of this agreement shall not void the +remaining provisions. + +1.F.6. INDEMNITY - You agree to indemnify and hold the Foundation, the +trademark owner, any agent or employee of the Foundation, anyone +providing copies of Project Gutenberg-tm electronic works in +accordance with this agreement, and any volunteers associated with the +production, promotion and distribution of Project Gutenberg-tm +electronic works, harmless from all liability, costs and expenses, +including legal fees, that arise directly or indirectly from any of +the following which you do or cause to occur: (a) distribution of this +or any Project Gutenberg-tm work, (b) alteration, modification, or +additions or deletions to any Project Gutenberg-tm work, and (c) any +Defect you cause. + +Section 2. Information about the Mission of Project Gutenberg-tm + +Project Gutenberg-tm is synonymous with the free distribution of +electronic works in formats readable by the widest variety of +computers including obsolete, old, middle-aged and new computers. It +exists because of the efforts of hundreds of volunteers and donations +from people in all walks of life. + +Volunteers and financial support to provide volunteers with the +assistance they need are critical to reaching Project Gutenberg-tm's +goals and ensuring that the Project Gutenberg-tm collection will +remain freely available for generations to come. In 2001, the Project +Gutenberg Literary Archive Foundation was created to provide a secure +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 + +Section 3. Information about the Project Gutenberg Literary +Archive Foundation + +The Project Gutenberg Literary Archive Foundation is a non profit +501(c)(3) educational corporation organized under the laws of the +state of Mississippi and granted tax exempt status by the Internal +Revenue Service. The Foundation's EIN or federal tax identification +number is 64-6221541. Contributions to the Project Gutenberg Literary +Archive Foundation are tax deductible to the full extent permitted by +U.S. federal laws and your state's laws. + +The Foundation's principal office is in Fairbanks, Alaska, with the +mailing address: PO Box 750175, Fairbanks, AK 99775, but its +volunteers and employees are scattered throughout numerous +locations. Its business office is located at 809 North 1500 West, Salt +Lake City, UT 84116, (801) 596-1887. Email contact links and up to +date contact information can be found at the Foundation's web site and +official page at www.gutenberg.org/contact + +For additional contact information: + + Dr. Gregory B. Newby + Chief Executive and Director + gbnewby@pglaf.org + +Section 4. Information about Donations to the Project Gutenberg +Literary Archive Foundation + +Project Gutenberg-tm depends upon and cannot survive without wide +spread public support and donations to carry out its mission of +increasing the number of public domain and licensed works that can be +freely distributed in machine readable form accessible by the widest +array of equipment including outdated equipment. Many small donations +($1 to $5,000) are particularly important to maintaining tax exempt +status with the IRS. + +The Foundation is committed to complying with the laws regulating +charities and charitable donations in all 50 states of the United +States. Compliance requirements are not uniform and it takes a +considerable effort, much paperwork and many fees to meet and keep up +with these requirements. We do not solicit donations in locations +where we have not received written confirmation of compliance. To SEND +DONATIONS or determine the status of compliance for any particular +state visit www.gutenberg.org/donate + +While we cannot and do not solicit contributions from states where we +have not met the solicitation requirements, we know of no prohibition +against accepting unsolicited donations from donors in such states who +approach us with offers to donate. + +International donations are gratefully accepted, but we cannot make +any statements concerning tax treatment of donations received from +outside the United States. U.S. laws alone swamp our small staff. + +Please check the Project Gutenberg Web pages for current donation +methods and addresses. Donations are accepted in a number of other +ways including checks, online payments and credit card donations. To +donate, please visit: www.gutenberg.org/donate + +Section 5. General Information About Project Gutenberg-tm electronic works. + +Professor Michael S. Hart was the originator of the Project +Gutenberg-tm concept of a library of electronic works that could be +freely shared with anyone. For forty years, he produced and +distributed Project Gutenberg-tm eBooks with only a loose network of +volunteer support. + +Project Gutenberg-tm eBooks are often created from several printed +editions, all of which are confirmed as not protected by copyright in +the U.S. unless a copyright notice is included. Thus, we do not +necessarily keep eBooks in compliance with any particular paper +edition. + +Most people start at our Web site which has the main PG search +facility: www.gutenberg.org + +This Web site includes information about Project Gutenberg-tm, +including how to make donations to the Project Gutenberg Literary +Archive Foundation, how to help produce our new eBooks, and how to +subscribe to our email newsletter to hear about new eBooks. diff --git a/ciphers/rabin_miller.py b/ciphers/rabin_miller.py index f71fb03c0051..410d559d4315 100644 --- a/ciphers/rabin_miller.py +++ b/ciphers/rabin_miller.py @@ -1,9 +1,9 @@ -from __future__ import print_function # Primality Testing with the Rabin-Miller Algorithm import random -def rabinMiller(num): + +def rabin_miller(num: int) -> bool: s = num - 1 t = 0 @@ -11,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: @@ -21,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] + 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 lowPrimes: + 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 index 2abf981e9d7d..b367c3215127 100644 --- a/ciphers/rot13.py +++ b/ciphers/rot13.py @@ -1,25 +1,37 @@ -from __future__ import print_function -def dencrypt(s, n): - out = '' +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 c >= 'A' and c <= 'Z': - out += chr(ord('A') + (ord(c) - ord('A') + n) % 26) - elif c >= 'a' and c <= 'z': - out += chr(ord('a') + (ord(c) - ord('a') + n) % 26) + 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(): - s0 = 'HELLO' +def main() -> None: + s0 = input("Enter message: ") s1 = dencrypt(s0, 13) - print(s1) # URYYB + print("Encryption:", s1) s2 = dencrypt(s1, 13) - print(s2) # HELLO + print("Decryption: ", s2) + +if __name__ == "__main__": + import doctest -if __name__ == '__main__': + doctest.testmod() main() diff --git a/ciphers/rsa_cipher.py b/ciphers/rsa_cipher.py index 94f69ddc2533..d974e0c8a176 100644 --- a/ciphers/rsa_cipher.py +++ b/ciphers/rsa_cipher.py @@ -1,125 +1,152 @@ -from __future__ import print_function -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 541e90d6e884..44970e8cbc15 100644 --- a/ciphers/rsa_key_generator.py +++ b/ciphers/rsa_key_generator.py @@ -1,46 +1,63 @@ -from __future__ import print_function -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 1bdd7dc04a57..3c6898c7b1ee 100644 --- a/ciphers/simple_substitution_cipher.py +++ b/ciphers/simple_substitution_cipher.py @@ -1,71 +1,95 @@ -from __future__ import print_function -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 dbb358315d22..76178cb6a1bc 100644 --- a/ciphers/transposition_cipher.py +++ b/ciphers/transposition_cipher.py @@ -1,54 +1,68 @@ -from __future__ import print_function 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 57620d83948c..000000000000 --- a/ciphers/transposition_cipher_encrypt-decrypt_file.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import print_function -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 5d5be0792835..37b96ea14a46 100644 --- a/ciphers/vigenere_cipher.py +++ b/ciphers/vigenere_cipher.py @@ -1,47 +1,51 @@ -from __future__ import print_function -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) - - print('\n%sed message:' % mode.title()) +LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + +def main() -> None: + message = input("Enter message: ") + key = input("Enter key [alphanumeric]: ") + mode = input("Encrypt/Decrypt [e/d]: ") + + 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) @@ -50,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..425876f5e5e6 --- /dev/null +++ b/data_compression/huffman.py @@ -0,0 +1,92 @@ +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 = {**letter.bitstring for letter in traverse_tree(root, "")} + 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 d01e8f825368..000000000000 --- a/data_structures/AVL/AVL.py +++ /dev/null @@ -1,181 +0,0 @@ -""" -An AVL tree -""" -from __future__ import print_function - - -class Node: - - def __init__(self, label): - self.label = label - self._parent = None - self._left = None - self._right = None - self.height = 0 - - @property - def right(self): - return self._right - - @right.setter - def right(self, node): - if node is not None: - node._parent = self - self._right = node - - @property - def left(self): - return self._left - - @left.setter - def left(self, node): - if node is not None: - node._parent = self - self._left = node - - @property - def parent(self): - return self._parent - - @parent.setter - def parent(self, node): - if node is not None: - self._parent = node - self.height = self.parent.height + 1 - else: - self.height = 0 - - -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.root.height = 0 - 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.label < curr_node.label: - curr_node = curr_node.left - else: - curr_node = curr_node.right - else: - node.height = dad_node.height - dad_node.height += 1 - if node.label < dad_node.label: - dad_node.left = node - else: - dad_node.right = node - self.rebalance(node) - self.size += 1 - break - - def rebalance(self, node): - n = node - - while n is not None: - height_right = n.height - height_left = n.height - - if n.right is not None: - height_right = n.right.height - - if n.left is not None: - height_left = n.left.height - - if abs(height_left - height_right) > 1: - if height_left > height_right: - left_child = n.left - if left_child is not None: - h_right = (left_child.right.height - if (left_child.right is not None) else 0) - h_left = (left_child.left.height - if (left_child.left is not None) else 0) - if (h_left > h_right): - self.rotate_left(n) - break - else: - self.double_rotate_right(n) - break - else: - right_child = n.right - if right_child is not None: - h_right = (right_child.right.height - if (right_child.right is not None) else 0) - h_left = (right_child.left.height - if (right_child.left is not None) else 0) - if (h_left > h_right): - self.double_rotate_left(n) - break - else: - self.rotate_right(n) - break - n = n.parent - - def rotate_left(self, node): - aux = node.parent.label - node.parent.label = node.label - node.parent.right = Node(aux) - node.parent.right.height = node.parent.height + 1 - node.parent.left = node.right - - - def rotate_right(self, node): - aux = node.parent.label - node.parent.label = node.label - node.parent.left = Node(aux) - node.parent.left.height = node.parent.height + 1 - node.parent.right = node.right - - 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) - - def empty(self): - if self.root is None: - return True - return False - - def preShow(self, curr_node): - if curr_node is not None: - self.preShow(curr_node.left) - print(curr_node.label, end=" ") - self.preShow(curr_node.right) - - def preorder(self, curr_node): - if curr_node is not None: - self.preShow(curr_node.left) - self.preShow(curr_node.right) - print(curr_node.label, end=" ") - - def getRoot(self): - return self.root - -t = AVL() -t.insert(1) -t.insert(2) -t.insert(3) -# t.preShow(t.root) -# print("\n") -# t.insert(4) -# t.insert(5) -# t.preShow(t.root) -# t.preorden(t.root) diff --git a/data_structures/Arrays.py b/data_structures/Arrays.py deleted file mode 100644 index 3ec9f8976673..000000000000 --- a/data_structures/Arrays.py +++ /dev/null @@ -1,3 +0,0 @@ -arr = [10, 20, 30, 40] -arr[1] = 30 -print(arr) diff --git a/data_structures/Binary Tree/FenwickTree.py b/data_structures/Binary Tree/FenwickTree.py deleted file mode 100644 index f429161c8c36..000000000000 --- a/data_structures/Binary Tree/FenwickTree.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import print_function -class FenwickTree: - - def __init__(self, SIZE): # create fenwick tree with size SIZE - self.Size = SIZE - self.ft = [0 for i in range (0,SIZE)] - - def update(self, i, val): # update data (adding) in index i in O(lg N) - while (i < self.Size): - self.ft[i] += val - i += i & (-i) - - def query(self, i): # query cumulative data from index 0 to i in O(lg N) - ret = 0 - while (i > 0): - ret += self.ft[i] - i -= i & (-i) - return ret - -if __name__ == '__main__': - f = FenwickTree(100) - f.update(1,20) - f.update(4,4) - print (f.query(1)) - print (f.query(3)) - print (f.query(4)) - f.update(2,-5) - print (f.query(1)) - print (f.query(3)) diff --git a/data_structures/Binary Tree/LazySegmentTree.py b/data_structures/Binary Tree/LazySegmentTree.py deleted file mode 100644 index 9b14b24e81fa..000000000000 --- a/data_structures/Binary Tree/LazySegmentTree.py +++ /dev/null @@ -1,91 +0,0 @@ -from __future__ import print_function -import math - -class SegmentTree: - - def __init__(self, N): - self.N = N - self.st = [0 for i in range(0,4*N)] # approximate the overall size of segment tree with array N - self.lazy = [0 for i in range(0,4*N)] # create array to store lazy update - self.flag = [0 for i in range(0,4*N)] # flag for lazy update - - def left(self, idx): - return idx*2 - - def right(self, idx): - return idx*2 + 1 - - def build(self, idx, l, r, A): - if l==r: - self.st[idx] = A[l-1] - else : - mid = (l+r)//2 - self.build(self.left(idx),l,mid, A) - self.build(self.right(idx),mid+1,r, A) - self.st[idx] = max(self.st[self.left(idx)] , self.st[self.right(idx)]) - - # update with O(lg N) (Normal segment tree without lazy update will take O(Nlg N) for each update) - def update(self, idx, l, r, a, b, val): # update(1, 1, N, a, b, v) for update val v to [a,b] - if self.flag[idx] == True: - self.st[idx] = self.lazy[idx] - self.flag[idx] = False - if l!=r: - 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 r < a or l > b: - return True - if l >= a and r <= b : - self.st[idx] = val - if l!=r: - 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 = (l+r)//2 - self.update(self.left(idx),l,mid,a,b,val) - self.update(self.right(idx),mid+1,r,a,b,val) - self.st[idx] = max(self.st[self.left(idx)] , self.st[self.right(idx)]) - return True - - # query with O(lg N) - def query(self, idx, l, r, a, b): #query(1, 1, N, a, b) for query max of [a,b] - if self.flag[idx] == True: - self.st[idx] = self.lazy[idx] - self.flag[idx] = False - if l != r: - 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 r < a or l > b: - return -math.inf - if l >= a and r <= b: - return self.st[idx] - mid = (l+r)//2 - q1 = self.query(self.left(idx),l,mid,a,b) - q2 = self.query(self.right(idx),mid+1,r,a,b) - return max(q1,q2) - - def showData(self): - showList = [] - for i in range(1,N+1): - showList += [self.query(1, 1, self.N, i, i)] - print (showList) - - -if __name__ == '__main__': - A = [1,2,-4,7,3,-5,6,11,-20,9,14,15,5,2,-8] - N = 15 - segt = SegmentTree(N) - segt.build(1,1,N,A) - print (segt.query(1,1,N,4,6)) - print (segt.query(1,1,N,7,11)) - print (segt.query(1,1,N,7,12)) - segt.update(1,1,N,1,3,111) - print (segt.query(1,1,N,1,15)) - segt.update(1,1,N,7,8,235) - segt.showData() diff --git a/data_structures/Binary Tree/SegmentTree.py b/data_structures/Binary Tree/SegmentTree.py deleted file mode 100644 index 001bf999f391..000000000000 --- a/data_structures/Binary Tree/SegmentTree.py +++ /dev/null @@ -1,71 +0,0 @@ -from __future__ import print_function -import math - -class SegmentTree: - - def __init__(self, A): - self.N = len(A) - self.st = [0] * (4 * self.N) # approximate the overall size of segment tree with array N - self.build(1, 0, self.N - 1) - - def left(self, idx): - return idx * 2 - - def right(self, idx): - return idx * 2 + 1 - - def build(self, idx, l, r): - if l == r: - self.st[idx] = A[l] - else: - mid = (l + r) // 2 - self.build(self.left(idx), l, mid) - self.build(self.right(idx), mid + 1, r) - self.st[idx] = max(self.st[self.left(idx)] , self.st[self.right(idx)]) - - def update(self, a, b, val): - return self.update_recursive(1, 0, self.N - 1, a - 1, b - 1, val) - - def update_recursive(self, idx, l, r, a, b, val): # update(1, 1, N, a, b, v) for update val v to [a,b] - if r < a or l > b: - return True - if l == r : - self.st[idx] = val - return True - mid = (l+r)//2 - self.update_recursive(self.left(idx), l, mid, a, b, val) - self.update_recursive(self.right(idx), mid+1, r, a, b, val) - self.st[idx] = max(self.st[self.left(idx)] , self.st[self.right(idx)]) - return True - - def query(self, a, b): - return self.query_recursive(1, 0, self.N - 1, a - 1, b - 1) - - def query_recursive(self, idx, l, r, a, b): #query(1, 1, N, a, b) for query max of [a,b] - if r < a or l > b: - return -math.inf - if l >= a and r <= b: - return self.st[idx] - mid = (l+r)//2 - q1 = self.query_recursive(self.left(idx), l, mid, a, b) - q2 = self.query_recursive(self.right(idx), mid + 1, r, a, b) - return max(q1, q2) - - def showData(self): - showList = [] - for i in range(1,N+1): - showList += [self.query(i, i)] - print (showList) - - -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.showData() diff --git a/data_structures/Binary Tree/binary_search_tree.py b/data_structures/Binary Tree/binary_search_tree.py deleted file mode 100644 index b4021e4f861f..000000000000 --- a/data_structures/Binary Tree/binary_search_tree.py +++ /dev/null @@ -1,258 +0,0 @@ -''' -A binary search Tree -''' -from __future__ import print_function -class Node: - - def __init__(self, label, parent): - self.label = label - self.left = None - self.right = None - #Added in order to delete a node easier - self.parent = parent - - 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 - - def getParent(self): - return self.parent - - def setParent(self, parent): - self.parent = parent - -class BinarySearchTree: - - def __init__(self): - self.root = None - - def insert(self, label): - # Create a new Node - new_node = Node(label, None) - # If Tree is empty - if self.empty(): - self.root = new_node - else: - #If Tree is not empty - curr_node = self.root - #While we don't get to a leaf - while curr_node is not None: - #We keep reference of the parent node - parent_node = curr_node - #If node label is less than current node - if new_node.getLabel() < curr_node.getLabel(): - #We go left - curr_node = curr_node.getLeft() - else: - #Else we go right - curr_node = curr_node.getRight() - #We insert the new node in a leaf - if new_node.getLabel() < parent_node.getLabel(): - parent_node.setLeft(new_node) - else: - parent_node.setRight(new_node) - #Set parent to the new node - new_node.setParent(parent_node) - - def delete(self, label): - if (not self.empty()): - #Look for the node with that label - node = self.getNode(label) - #If the node exists - if(node is not None): - #If it has no children - if(node.getLeft() is None and node.getRight() is None): - self.__reassignNodes(node, None) - node = None - #Has only right children - elif(node.getLeft() is None and node.getRight() is not None): - self.__reassignNodes(node, node.getRight()) - #Has only left children - elif(node.getLeft() is not None and node.getRight() is None): - self.__reassignNodes(node, node.getLeft()) - #Has two children - else: - #Gets the max value of the left branch - tmpNode = self.getMax(node.getLeft()) - #Deletes the tmpNode - self.delete(tmpNode.getLabel()) - #Assigns the value to the node to delete and keesp tree structure - node.setLabel(tmpNode.getLabel()) - - def getNode(self, label): - curr_node = None - #If the tree is not empty - if(not self.empty()): - #Get tree root - curr_node = self.getRoot() - #While we don't find the node we look for - #I am using lazy evaluation here to avoid NoneType Attribute error - while curr_node is not None and curr_node.getLabel() is not label: - #If node label is less than current node - if label < curr_node.getLabel(): - #We go left - curr_node = curr_node.getLeft() - else: - #Else we go right - curr_node = curr_node.getRight() - return curr_node - - def getMax(self, root = None): - if(root is not None): - curr_node = root - else: - #We go deep on the right branch - curr_node = self.getRoot() - if(not self.empty()): - while(curr_node.getRight() is not None): - curr_node = curr_node.getRight() - return curr_node - - def getMin(self, root = None): - if(root is not None): - curr_node = root - else: - #We go deep on the left branch - curr_node = self.getRoot() - if(not self.empty()): - curr_node = self.getRoot() - while(curr_node.getLeft() is not None): - curr_node = curr_node.getLeft() - return curr_node - - def empty(self): - if self.root is None: - return True - return False - - def __InOrderTraversal(self, curr_node): - nodeList = [] - if curr_node is not None: - nodeList.insert(0, curr_node) - nodeList = nodeList + self.__InOrderTraversal(curr_node.getLeft()) - nodeList = nodeList + self.__InOrderTraversal(curr_node.getRight()) - return nodeList - - def getRoot(self): - return self.root - - def __isRightChildren(self, node): - if(node == node.getParent().getRight()): - return True - return False - - def __reassignNodes(self, node, newChildren): - if(newChildren is not None): - newChildren.setParent(node.getParent()) - if(node.getParent() is not None): - #If it is the Right Children - if(self.__isRightChildren(node)): - node.getParent().setRight(newChildren) - else: - #Else it is the left children - node.getParent().setLeft(newChildren) - - #This function traversal the tree. By default it returns an - #In order traversal list. You can pass a function to traversal - #The tree as needed by client code - def traversalTree(self, traversalFunction = None, root = None): - if(traversalFunction is None): - #Returns a list of nodes in preOrder by default - return self.__InOrderTraversal(self.root) - else: - #Returns a list of nodes in the order that the users wants to - return traversalFunction(self.root) - - #Returns an string of all the nodes labels in the list - #In Order Traversal - def __str__(self): - list = self.__InOrderTraversal(self.root) - str = "" - for x in list: - str = str + " " + x.getLabel().__str__() - return str - -def InPreOrder(curr_node): - nodeList = [] - if curr_node is not None: - nodeList = nodeList + InPreOrder(curr_node.getLeft()) - nodeList.insert(0, curr_node.getLabel()) - nodeList = nodeList + InPreOrder(curr_node.getRight()) - return nodeList - -def testBinarySearchTree(): - ''' - Example - 8 - / \ - 3 10 - / \ \ - 1 6 14 - / \ / - 4 7 13 - ''' - - ''' - Example After Deletion - 7 - / \ - 1 4 - - ''' - t = BinarySearchTree() - t.insert(8) - t.insert(3) - t.insert(6) - t.insert(1) - t.insert(10) - t.insert(14) - t.insert(13) - t.insert(4) - t.insert(7) - - #Prints all the elements of the list in order traversal - print(t.__str__()) - - if(t.getNode(6) is not None): - print("The label 6 exists") - else: - print("The label 6 doesn't exist") - - if(t.getNode(-1) is not None): - print("The label -1 exists") - else: - print("The label -1 doesn't exist") - - if(not t.empty()): - print(("Max Value: ", t.getMax().getLabel())) - print(("Min Value: ", t.getMin().getLabel())) - - t.delete(13) - t.delete(10) - t.delete(8) - t.delete(3) - t.delete(6) - t.delete(14) - - #Gets all the elements of the tree In pre order - #And it prints them - list = t.traversalTree(InPreOrder, t.root) - for x in list: - print(x) - -if __name__ == "__main__": - testBinarySearchTree() diff --git a/data_structures/Graph/BellmanFord.py b/data_structures/Graph/BellmanFord.py deleted file mode 100644 index 82db80546b94..000000000000 --- a/data_structures/Graph/BellmanFord.py +++ /dev/null @@ -1,54 +0,0 @@ -from __future__ import print_function - -def printDist(dist, V): - print("\nVertex Distance") - for i in range(V): - if dist[i] != float('inf') : - print(i,"\t",int(dist[i]),end = "\t") - else: - print(i,"\t","INF",end="\t") - print() - -def BellmanFord(graph, V, E, src): - mdist=[float('inf') for i in range(V)] - mdist[src] = 0.0 - - for i in range(V-1): - for j in range(V): - u = graph[j]["src"] - v = graph[j]["dst"] - w = graph[j]["weight"] - - if mdist[u] != float('inf') and mdist[u] + w < mdist[v]: - mdist[v] = mdist[u] + w - for j in range(V): - u = graph[j]["src"] - v = graph[j]["dst"] - w = graph[j]["weight"] - - if mdist[u] != float('inf') and mdist[u] + w < mdist[v]: - print("Negative cycle found. Solution not possible.") - return - - printDist(mdist, V) - - - -#MAIN -V = int(input("Enter number of vertices: ")) -E = int(input("Enter number of edges: ")) - -graph = [dict() for j in range(E)] - -for i in range(V): - graph[i][i] = 0.0 - -for i in range(E): - print("\nEdge ",i+1) - src = int(input("Enter source:")) - dst = int(input("Enter destination:")) - weight = float(input("Enter weight:")) - graph[i] = {"src": src,"dst": dst, "weight": weight} - -gsrc = int(input("\nEnter shortest path source:")) -BellmanFord(graph, V, E, gsrc) diff --git a/data_structures/Graph/BreadthFirstSearch.py b/data_structures/Graph/BreadthFirstSearch.py deleted file mode 100644 index 02f6af83ff66..000000000000 --- a/data_structures/Graph/BreadthFirstSearch.py +++ /dev/null @@ -1,63 +0,0 @@ -# Author: OMKAR PATHAK -from __future__ import print_function - - -class Graph(): - def __init__(self): - self.vertex = {} - - # for printing the Graph vertexes - def printGraph(self): - for i in self.vertex.keys(): - print(i,' -> ', ' -> '.join([str(j) for j in self.vertex[i]])) - - # for adding the edge beween two vertexes - def addEdge(self, fromVertex, toVertex): - # check if vertex is already present, - if fromVertex in self.vertex.keys(): - self.vertex[fromVertex].append(toVertex) - else: - # else make a new vertex - self.vertex[fromVertex] = [toVertex] - - def BFS(self, startVertex): - # Take a list for stoting already visited vertexes - visited = [False] * len(self.vertex) - - # create a list to store all the vertexes for BFS - queue = [] - - # mark the source node as visited and enqueue it - visited[startVertex] = True - queue.append(startVertex) - - while queue: - startVertex = queue.pop(0) - print(startVertex, end = ' ') - - # mark all adjacent nodes as visited and print them - for i in self.vertex[startVertex]: - if visited[i] == False: - queue.append(i) - visited[i] = True - -if __name__ == '__main__': - g = Graph() - g.addEdge(0, 1) - g.addEdge(0, 2) - g.addEdge(1, 2) - g.addEdge(2, 0) - g.addEdge(2, 3) - g.addEdge(3, 3) - - g.printGraph() - print('BFS:') - g.BFS(2) - - # OUTPUT: - # 0  ->  1 -> 2 - # 1  ->  2 - # 2  ->  0 -> 3 - # 3  ->  3 - # BFS: - # 2 0 3 1 diff --git a/data_structures/Graph/DepthFirstSearch.py b/data_structures/Graph/DepthFirstSearch.py deleted file mode 100644 index 0f10a8600099..000000000000 --- a/data_structures/Graph/DepthFirstSearch.py +++ /dev/null @@ -1,63 +0,0 @@ -# Author: OMKAR PATHAK -from __future__ import print_function - - -class Graph(): - def __init__(self): - self.vertex = {} - - # for printing the Graph vertexes - def printGraph(self): - print(self.vertex) - for i in self.vertex.keys(): - print(i,' -> ', ' -> '.join([str(j) for j in self.vertex[i]])) - - # for adding the edge beween two vertexes - def addEdge(self, fromVertex, toVertex): - # check if vertex is already present, - if fromVertex in self.vertex.keys(): - self.vertex[fromVertex].append(toVertex) - else: - # else make a new vertex - self.vertex[fromVertex] = [toVertex] - - def DFS(self): - # visited array for storing already visited nodes - visited = [False] * len(self.vertex) - - # call the recursive helper function - for i in range(len(self.vertex)): - if visited[i] == False: - self.DFSRec(i, visited) - - def DFSRec(self, startVertex, visited): - # mark start vertex as visited - visited[startVertex] = True - - print(startVertex, end = ' ') - - # Recur for all the vertexes that are adjacent to this node - for i in self.vertex.keys(): - if visited[i] == False: - self.DFSRec(i, visited) - -if __name__ == '__main__': - g = Graph() - g.addEdge(0, 1) - g.addEdge(0, 2) - g.addEdge(1, 2) - g.addEdge(2, 0) - g.addEdge(2, 3) - g.addEdge(3, 3) - - g.printGraph() - print('DFS:') - g.DFS() - - # OUTPUT: - # 0  ->  1 -> 2 - # 1  ->  2 - # 2  ->  0 -> 3 - # 3  ->  3 - # DFS: - # 0 1 2 3 diff --git a/data_structures/Graph/Dijkstra.py b/data_structures/Graph/Dijkstra.py deleted file mode 100644 index 8917171417c4..000000000000 --- a/data_structures/Graph/Dijkstra.py +++ /dev/null @@ -1,57 +0,0 @@ -from __future__ import print_function - -def printDist(dist, V): - print("\nVertex Distance") - for i in range(V): - if dist[i] != float('inf') : - print(i,"\t",int(dist[i]),end = "\t") - else: - print(i,"\t","INF",end="\t") - print() - -def minDist(mdist, vset, V): - minVal = float('inf') - minInd = -1 - for i in range(V): - if (not vset[i]) and mdist[i] < minVal : - minInd = i - minVal = mdist[i] - return minInd - -def Dijkstra(graph, V, src): - mdist=[float('inf') for i in range(V)] - vset = [False for i in range(V)] - mdist[src] = 0.0; - - for i in range(V-1): - u = minDist(mdist, vset, V) - vset[u] = True - - for v in range(V): - if (not vset[v]) and graph[u][v]!=float('inf') and mdist[u] + graph[u][v] < mdist[v]: - mdist[v] = mdist[u] + graph[u][v] - - - - printDist(mdist, V) - - - -#MAIN -V = int(input("Enter number of vertices: ")) -E = int(input("Enter number of edges: ")) - -graph = [[float('inf') for i in range(V)] for j in range(V)] - -for i in range(V): - graph[i][i] = 0.0 - -for i in range(E): - print("\nEdge ",i+1) - src = int(input("Enter source:")) - dst = int(input("Enter destination:")) - weight = float(input("Enter weight:")) - graph[src][dst] = weight - -gsrc = int(input("\nEnter shortest path source:")) -Dijkstra(graph, V, gsrc) diff --git a/data_structures/Graph/FloydWarshall.py b/data_structures/Graph/FloydWarshall.py deleted file mode 100644 index fae8b19b351a..000000000000 --- a/data_structures/Graph/FloydWarshall.py +++ /dev/null @@ -1,48 +0,0 @@ -from __future__ import print_function - -def printDist(dist, V): - print("\nThe shortest path matrix using Floyd Warshall algorithm\n") - for i in range(V): - for j in range(V): - if dist[i][j] != float('inf') : - print(int(dist[i][j]),end = "\t") - else: - print("INF",end="\t") - print() - - - -def FloydWarshall(graph, V): - dist=[[float('inf') for i in range(V)] for j in range(V)] - - for i in range(V): - for j in range(V): - dist[i][j] = graph[i][j] - - for k in range(V): - for i in range(V): - for j in range(V): - if dist[i][k]!=float('inf') and dist[k][j]!=float('inf') and dist[i][k]+dist[k][j] < dist[i][j]: - dist[i][j] = dist[i][k] + dist[k][j] - - printDist(dist, V) - - - -#MAIN -V = int(input("Enter number of vertices: ")) -E = int(input("Enter number of edges: ")) - -graph = [[float('inf') for i in range(V)] for j in range(V)] - -for i in range(V): - graph[i][i] = 0.0 - -for i in range(E): - print("\nEdge ",i+1) - src = int(input("Enter source:")) - dst = int(input("Enter destination:")) - weight = float(input("Enter weight:")) - graph[src][dst] = weight - -FloydWarshall(graph, V) diff --git a/data_structures/Graph/Graph.py b/data_structures/Graph/Graph.py deleted file mode 100644 index d091f713b8d9..000000000000 --- a/data_structures/Graph/Graph.py +++ /dev/null @@ -1,41 +0,0 @@ -from __future__ import print_function -# 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 d67bc96c4a81..000000000000 --- a/data_structures/Graph/Graph_list.py +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import print_function - - -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 de25301d6dd1..000000000000 --- a/data_structures/Graph/Graph_matrix.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import print_function - - -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/Graph/dijkstra_algorithm.py b/data_structures/Graph/dijkstra_algorithm.py deleted file mode 100644 index 985c7f6c1301..000000000000 --- a/data_structures/Graph/dijkstra_algorithm.py +++ /dev/null @@ -1,212 +0,0 @@ -# Title: Dijkstra's Algorithm for finding single source shortest path from scratch -# Author: Shubham Malik -# References: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm - -from __future__ import print_function -import math -import sys -# For storing the vertex set to retreive node with the lowest distance - - -class PriorityQueue: - # Based on Min Heap - def __init__(self): - self.cur_size = 0 - self.array = [] - self.pos = {} # To store the pos of node in array - - def isEmpty(self): - return self.cur_size == 0 - - def min_heapify(self, idx): - lc = self.left(idx) - rc = self.right(idx) - if lc < self.cur_size and self.array(lc)[0] < self.array(idx)[0]: - smallest = lc - else: - smallest = idx - if rc < self.cur_size and self.array(rc)[0] < self.array(smallest)[0]: - smallest = rc - if smallest != idx: - self.swap(idx, smallest) - self.min_heapify(smallest) - - def insert(self, tup): - # Inserts a node into the Priority Queue - self.pos[tup[1]] = self.cur_size - self.cur_size += 1 - self.array.append((sys.maxsize, tup[1])) - self.decrease_key((sys.maxsize, tup[1]), tup[0]) - - def extract_min(self): - # Removes and returns the min element at top of priority queue - min_node = self.array[0][1] - self.array[0] = self.array[self.cur_size - 1] - self.cur_size -= 1 - self.min_heapify(1) - del self.pos[min_node] - return min_node - - def left(self, i): - # returns the index of left child - return 2 * i + 1 - - def right(self, i): - # returns the index of right child - return 2 * i + 2 - - def par(self, i): - # returns the index of parent - return math.floor(i / 2) - - def swap(self, i, j): - # swaps array elements at indices i and j - # update the pos{} - self.pos[self.array[i][1]] = j - self.pos[self.array[j][1]] = i - temp = self.array[i] - self.array[i] = self.array[j] - self.array[j] = temp - - def decrease_key(self, tup, new_d): - idx = self.pos[tup[1]] - # assuming the new_d is atmost old_d - self.array[idx] = (new_d, tup[1]) - while idx > 0 and self.array[self.par(idx)][0] > self.array[idx][0]: - self.swap(idx, self.par(idx)) - idx = self.par(idx) - - -class Graph: - def __init__(self, num): - self.adjList = {} # To store graph: u -> (v,w) - self.num_nodes = num # Number of nodes in graph - # To store the distance from source vertex - self.dist = [0] * self.num_nodes - self.par = [-1] * self.num_nodes # To store the path - - def add_edge(self, u, v, w): - # Edge going from node u to v and v to u with weight w - # u (w)-> v, v (w) -> u - # Check if u already in graph - if u in self.adjList.keys(): - self.adjList[u].append((v, w)) - else: - self.adjList[u] = [(v, w)] - - # Assuming undirected graph - if v in self.adjList.keys(): - self.adjList[v].append((u, w)) - else: - self.adjList[v] = [(u, w)] - - def show_graph(self): - # u -> v(w) - for u in self.adjList: - print(u, '->', ' -> '.join(str("{}({})".format(v, w)) - for v, w in self.adjList[u])) - - def dijkstra(self, src): - # Flush old junk values in par[] - self.par = [-1] * self.num_nodes - # src is the source node - self.dist[src] = 0 - Q = PriorityQueue() - Q.insert((0, src)) # (dist from src, node) - for u in self.adjList.keys(): - if u != src: - self.dist[u] = sys.maxsize # Infinity - self.par[u] = -1 - - while not Q.isEmpty(): - u = Q.extract_min() # Returns node with the min dist from source - # Update the distance of all the neighbours of u and - # if their prev dist was INFINITY then push them in Q - for v, w in self.adjList[u]: - new_dist = self.dist[u] + w - if self.dist[v] > new_dist: - if self.dist[v] == sys.maxsize: - Q.insert((new_dist, v)) - else: - Q.decrease_key((self.dist[v], v), new_dist) - self.dist[v] = new_dist - self.par[v] = u - - # Show the shortest distances from src - self.show_distances(src) - - def show_distances(self, src): - print("Distance from node: {}".format(src)) - for u in range(self.num_nodes): - print('Node {} has distance: {}'.format(u, self.dist[u])) - - def show_path(self, src, dest): - # To show the shortest path from src to dest - # WARNING: Use it *after* calling dijkstra - path = [] - cost = 0 - temp = dest - # Backtracking from dest to src - while self.par[temp] != -1: - path.append(temp) - if temp != src: - for v, w in self.adjList[temp]: - if v == self.par[temp]: - cost += w - break - temp = self.par[temp] - path.append(src) - path.reverse() - - print('----Path to reach {} from {}----'.format(dest, src)) - for u in path: - print('{}'.format(u), end=' ') - if u != dest: - print('-> ', end='') - - print('\nTotal cost of path: ', cost) - - -if __name__ == '__main__': - graph = Graph(9) - graph.add_edge(0, 1, 4) - graph.add_edge(0, 7, 8) - graph.add_edge(1, 2, 8) - graph.add_edge(1, 7, 11) - graph.add_edge(2, 3, 7) - graph.add_edge(2, 8, 2) - graph.add_edge(2, 5, 4) - graph.add_edge(3, 4, 9) - graph.add_edge(3, 5, 14) - graph.add_edge(4, 5, 10) - graph.add_edge(5, 6, 2) - graph.add_edge(6, 7, 1) - graph.add_edge(6, 8, 6) - graph.add_edge(7, 8, 7) - graph.show_graph() - graph.dijkstra(0) - graph.show_path(0, 4) - -# OUTPUT -# 0 -> 1(4) -> 7(8) -# 1 -> 0(4) -> 2(8) -> 7(11) -# 7 -> 0(8) -> 1(11) -> 6(1) -> 8(7) -# 2 -> 1(8) -> 3(7) -> 8(2) -> 5(4) -# 3 -> 2(7) -> 4(9) -> 5(14) -# 8 -> 2(2) -> 6(6) -> 7(7) -# 5 -> 2(4) -> 3(14) -> 4(10) -> 6(2) -# 4 -> 3(9) -> 5(10) -# 6 -> 5(2) -> 7(1) -> 8(6) -# Distance from node: 0 -# Node 0 has distance: 0 -# Node 1 has distance: 4 -# Node 2 has distance: 12 -# Node 3 has distance: 19 -# Node 4 has distance: 21 -# Node 5 has distance: 11 -# Node 6 has distance: 9 -# Node 7 has distance: 8 -# Node 8 has distance: 14 -# ----Path to reach 4 from 0---- -# 0 -> 7 -> 6 -> 5 -> 4 -# Total cost of path: 21 diff --git a/data_structures/Heap/heap.py b/data_structures/Heap/heap.py deleted file mode 100644 index e66d02b6d99f..000000000000 --- a/data_structures/Heap/heap.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/python - -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 - -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 6f17b7c81033..000000000000 --- a/data_structures/LinkedList/DoublyLinkedList.py +++ /dev/null @@ -1,76 +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. -''' -from __future__ import print_function - - -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=" ") 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 eb7f48f17359..000000000000 --- a/data_structures/LinkedList/singly_LinkedList.py +++ /dev/null @@ -1,69 +0,0 @@ -from __future__ import print_function - - -class Node: # create a Node - def __init__(self, data): - self.data = data # given data - self.next = None # given next to None - - -class Linked_List: - def insert_tail(Head, data): - if Head.next is None: - Head.next = Node(data) - else: - Head.next.insert_tail(data) - - def insert_head(Head, data): - tamp = Head - if tamp is 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 printList(Head): # print every node data - tamp = Head - while tamp is not None: - print(tamp.data) - tamp = tamp.next - - def delete_head(Head): # delete from head - if Head is not None: - Head = Head.next - return Head # return new Head - - def delete_tail(Head): # delete from tail - if Head is not None: - tamp = Node() - tamp = Head - while tamp.next.next is not None: # find the 2nd last element - tamp = tamp.next - # delete the last element by give next None to 2nd last Element - tamp.next = None - return Head - - def isEmpty(Head): - return Head is None # Return if Head is none - - def reverse(Head): - prev = None - current = Head - - while current: - # Store the current node's next node. - next_node = current.next - # Make the current node's next point backwards - current.next = prev - # Make the previous node be the current node - prev = current - # Make the current node the next node (to progress iteration) - current = next_node - # Return prev in order to put the head at the end - Head = prev diff --git a/data_structures/Queue/DeQueue.py b/data_structures/Queue/DeQueue.py deleted file mode 100644 index fdee64eb6ae0..000000000000 --- a/data_structures/Queue/DeQueue.py +++ /dev/null @@ -1,40 +0,0 @@ -from __future__ import print_function -# Python code to demonstrate working of -# extend(), extendleft(), rotate(), reverse() - -# importing "collections" for deque operations -import collections - -# initializing deque -de = collections.deque([1, 2, 3,]) - -# using extend() to add numbers to right end -# adds 4,5,6 to right end -de.extend([4,5,6]) - -# printing modified deque -print ("The deque after extending deque at end is : ") -print (de) - -# using extendleft() to add numbers to left end -# adds 7,8,9 to right end -de.extendleft([7,8,9]) - -# printing modified deque -print ("The deque after extending deque at beginning is : ") -print (de) - -# using rotate() to rotate the deque -# rotates by 3 to left -de.rotate(-3) - -# printing modified deque -print ("The deque after rotating deque is : ") -print (de) - -# using reverse() to reverse the deque -de.reverse() - -# printing modified deque -print ("The deque after reversing deque is : ") -print (de) 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/Queue/QueueOnPseudoStack.py b/data_structures/Queue/QueueOnPseudoStack.py deleted file mode 100644 index b69fbcc988f7..000000000000 --- a/data_structures/Queue/QueueOnPseudoStack.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Queue represented by a pseudo stack (represented by a list with pop and append)""" -class Queue(): - def __init__(self): - self.stack = [] - self.length = 0 - - def __str__(self): - printed = '<' + str(self.stack)[1:-1] + '>' - return printed - - """Enqueues {@code item} - @param item - item to enqueue""" - def put(self, item): - self.stack.append(item) - self.length = self.length + 1 - - """Dequeues {@code item} - @requirement: |self.length| > 0 - @return dequeued - item that was dequeued""" - def get(self): - self.rotate(1) - dequeued = self.stack[self.length-1] - self.stack = self.stack[:-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): - temp = self.stack[0] - self.stack = self.stack[1:] - self.put(temp) - self.length = self.length - 1 - - """Reports item at the front of self - @return item at front of self.stack""" - def front(self): - front = self.get() - self.put(front) - self.rotate(self.length-1) - return front - - """Returns the length of this.stack""" - def size(self): - return self.length 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/Stacks/balanced_parentheses.py b/data_structures/Stacks/balanced_parentheses.py deleted file mode 100644 index 8d99358bea87..000000000000 --- a/data_structures/Stacks/balanced_parentheses.py +++ /dev/null @@ -1,23 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from .Stack import Stack - -__author__ = 'Omkar Pathak' - - -def balanced_parentheses(parentheses): - """ Use a stack to check if a string of parentheses are balanced.""" - stack = Stack(len(parentheses)) - for parenthesis in parentheses: - if parenthesis == '(': - stack.push(parenthesis) - elif parenthesis == ')': - stack.pop() - return not stack.is_empty() - - -if __name__ == '__main__': - examples = ['((()))', '((())'] - print('Balanced parentheses demonstration:\n') - for example in examples: - print(example + ': ' + str(balanced_parentheses(example))) 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 75211fed258d..000000000000 --- a/data_structures/Stacks/infix_to_postfix_conversion.py +++ /dev/null @@ -1,64 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -import string - -from .Stack import Stack - -__author__ = 'Omkar Pathak' - - -def is_operand(char): - return char in string.ascii_letters or char in string.digits - - -def precedence(char): - """ Return integer value representing an operator's precedence, or - order of operation. - - https://en.wikipedia.org/wiki/Order_of_operations - """ - dictionary = {'+': 1, '-': 1, - '*': 2, '/': 2, - '^': 3} - return dictionary.get(char, -1) - - -def infix_to_postfix(expression): - """ Convert infix notation to postfix notation using the Shunting-yard - algorithm. - - https://en.wikipedia.org/wiki/Shunting-yard_algorithm - https://en.wikipedia.org/wiki/Infix_notation - https://en.wikipedia.org/wiki/Reverse_Polish_notation - """ - stack = Stack(len(expression)) - postfix = [] - for char in expression: - if is_operand(char): - postfix.append(char) - elif char not in {'(', ')'}: - while (not stack.is_empty() - and precedence(char) <= precedence(stack.peek())): - postfix.append(stack.pop()) - stack.push(char) - elif char == '(': - stack.push(char) - elif char == ')': - while not stack.is_empty() and stack.peek() != '(': - postfix.append(stack.pop()) - # Pop '(' from stack. If there is no '(', there is a mismatched - # parentheses. - if stack.peek() != '(': - raise ValueError('Mismatched parentheses') - stack.pop() - while not stack.is_empty(): - postfix.append(stack.pop()) - return ' '.join(postfix) - - -if __name__ == '__main__': - 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/next.py b/data_structures/Stacks/next.py deleted file mode 100644 index bca83339592c..000000000000 --- a/data_structures/Stacks/next.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import print_function -# Function to print element and NGE pair for all elements of list -def printNGE(arr): - - for i in range(0, len(arr), 1): - - next = -1 - for j in range(i+1, len(arr), 1): - if arr[i] < arr[j]: - next = arr[j] - break - - print(str(arr[i]) + " -- " + str(next)) - -# Driver program to test above function -arr = [11,13,21,3] -printNGE(arr) diff --git a/data_structures/Stacks/stack.py b/data_structures/Stacks/stack.py deleted file mode 100644 index 66af8c025d8c..000000000000 --- a/data_structures/Stacks/stack.py +++ /dev/null @@ -1,69 +0,0 @@ -from __future__ import print_function -__author__ = 'Omkar Pathak' - - -class Stack(object): - """ 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=10): - self.stack = [] - self.limit = limit - - def __bool__(self): - return not bool(self.stack) - - def __str__(self): - return str(self.stack) - - def push(self, data): - """ Push an element to the top of the stack.""" - if len(self.stack) >= self.limit: - raise StackOverflowError - self.stack.append(data) - - def pop(self): - """ Pop an element off of the top of the stack.""" - if self.stack: - return self.stack.pop() - else: - raise IndexError('pop from an empty stack') - - def peek(self): - """ Peek at the top-most element of the stack.""" - if self.stack: - return self.stack[-1] - - def is_empty(self): - """ Check if a stack is empty.""" - return not bool(self.stack) - - def size(self): - """ Return the size of the stack.""" - return len(self.stack) - - -class StackOverflowError(BaseException): - pass - - -if __name__ == '__main__': - stack = Stack() - for i in range(10): - stack.push(i) - - print('Stack demonstration:\n') - print('Initial stack: ' + str(stack)) - print('pop(): ' + str(stack.pop())) - print('After pop(), the stack is now: ' + str(stack)) - print('peek(): ' + str(stack.peek())) - stack.push(100) - print('After push(100), the stack is now: ' + str(stack)) - print('is_empty(): ' + str(stack.is_empty())) - print('size(): ' + str(stack.size())) diff --git a/data_structures/Trie/Trie.py b/data_structures/Trie/Trie.py deleted file mode 100644 index b6234c6704c6..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]): # noqa: E999 This syntax is Python 3 only - """ - 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): # noqa: E999 This syntax is Python 3 only - """ - 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: # noqa: E999 This syntax is Python 3 only - """ - 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): # noqa: E999 This syntax is Python 3 only - """ - 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/UnionFind/tests_union_find.py b/data_structures/UnionFind/tests_union_find.py deleted file mode 100644 index b0708778ddbd..000000000000 --- a/data_structures/UnionFind/tests_union_find.py +++ /dev/null @@ -1,78 +0,0 @@ -from __future__ import absolute_import -from .union_find import UnionFind -import unittest - - -class TestUnionFind(unittest.TestCase): - def test_init_with_valid_size(self): - uf = UnionFind(5) - self.assertEqual(uf.size, 5) - - def test_init_with_invalid_size(self): - with self.assertRaises(ValueError): - uf = UnionFind(0) - - with self.assertRaises(ValueError): - uf = UnionFind(-5) - - def test_union_with_valid_values(self): - uf = UnionFind(10) - - for i in range(11): - for j in range(11): - uf.union(i, j) - - def test_union_with_invalid_values(self): - uf = UnionFind(10) - - with self.assertRaises(ValueError): - uf.union(-1, 1) - - with self.assertRaises(ValueError): - uf.union(11, 1) - - def test_same_set_with_valid_values(self): - uf = UnionFind(10) - - for i in range(11): - for j in range(11): - if i == j: - self.assertTrue(uf.same_set(i, j)) - else: - self.assertFalse(uf.same_set(i, j)) - - uf.union(1, 2) - self.assertTrue(uf.same_set(1, 2)) - - uf.union(3, 4) - self.assertTrue(uf.same_set(3, 4)) - - self.assertFalse(uf.same_set(1, 3)) - self.assertFalse(uf.same_set(1, 4)) - self.assertFalse(uf.same_set(2, 3)) - self.assertFalse(uf.same_set(2, 4)) - - uf.union(1, 3) - self.assertTrue(uf.same_set(1, 3)) - self.assertTrue(uf.same_set(1, 4)) - self.assertTrue(uf.same_set(2, 3)) - self.assertTrue(uf.same_set(2, 4)) - - uf.union(4, 10) - self.assertTrue(uf.same_set(1, 10)) - self.assertTrue(uf.same_set(2, 10)) - self.assertTrue(uf.same_set(3, 10)) - self.assertTrue(uf.same_set(4, 10)) - - def test_same_set_with_invalid_values(self): - uf = UnionFind(10) - - with self.assertRaises(ValueError): - uf.same_set(-1, 1) - - with self.assertRaises(ValueError): - uf.same_set(11, 0) - - -if __name__ == '__main__': - unittest.main() diff --git a/data_structures/UnionFind/union_find.py b/data_structures/UnionFind/union_find.py deleted file mode 100644 index 40eea67ac944..000000000000 --- a/data_structures/UnionFind/union_find.py +++ /dev/null @@ -1,87 +0,0 @@ -class UnionFind(): - """ - https://en.wikipedia.org/wiki/Disjoint-set_data_structure - - The union-find is a disjoint-set data structure - - You can merge two sets and tell if one set belongs to - another one. - - It's used on the Kruskal Algorithm - (https://en.wikipedia.org/wiki/Kruskal%27s_algorithm) - - The elements are in range [0, size] - """ - def __init__(self, size): - if size <= 0: - raise ValueError("size should be greater than 0") - - self.size = size - - # The below plus 1 is because we are using elements - # in range [0, size]. It makes more sense. - - # Every set begins with only itself - self.root = [i for i in range(size+1)] - - # This is used for heuristic union by rank - self.weight = [0 for i in range(size+1)] - - def union(self, u, v): - """ - Union of the sets u and v. - Complexity: log(n). - Amortized complexity: < 5 (it's very fast). - """ - - self._validate_element_range(u, "u") - self._validate_element_range(v, "v") - - if u == v: - return - - # Using union by rank will guarantee the - # log(n) complexity - rootu = self._root(u) - rootv = self._root(v) - weight_u = self.weight[rootu] - weight_v = self.weight[rootv] - if weight_u >= weight_v: - self.root[rootv] = rootu - if weight_u == weight_v: - self.weight[rootu] += 1 - else: - self.root[rootu] = rootv - - def same_set(self, u, v): - """ - Return true if the elements u and v belongs to - the same set - """ - - self._validate_element_range(u, "u") - self._validate_element_range(v, "v") - - return self._root(u) == self._root(v) - - def _root(self, u): - """ - Get the element set root. - This uses the heuristic path compression - See wikipedia article for more details. - """ - - if u != self.root[u]: - self.root[u] = self._root(self.root[u]) - - return self.root[u] - - def _validate_element_range(self, u, element_name): - """ - Raises ValueError if element is not in range - """ - if u < 0 or u > self.size: - msg = ("element {0} with value {1} " - "should be in range [0~{2}]")\ - .format(element_name, u, self.size) - raise ValueError(msg) 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..6cc05d18adb1 --- /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: {*u for u in units[s]} - {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 index b96ddd478458..e69de29bb2d1 100644 --- a/data_structures/hashing/__init__.py +++ b/data_structures/hashing/__init__.py @@ -1,6 +0,0 @@ -from .hash_table import HashTable - -class QuadraticProbing(HashTable): - - def __init__(self): - super(self.__class__, self).__init__() 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 index 60098cda0ce1..d4b46679ffbb 100644 --- a/data_structures/hashing/double_hash.py +++ b/data_structures/hashing/double_hash.py @@ -1,33 +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 next_prime, check_prime +from .number_theory.prime_numbers import is_prime, next_prime class DoubleHash(HashTable): """ - Hash Table example with open addressing and Double Hash + Hash Table example with open addressing and Double Hash """ - def __init__(self, *args, **kwargs): + + 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 check_prime(value % self.size_table) else value % self.size_table #gt = bigger than + 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 _colision_resolution(self, key, data=None): + 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 - else: i += 1 + 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 index f0de128d1ad1..cfc1db4feea4 100644 --- a/data_structures/hashing/hash_table.py +++ b/data_structures/hashing/hash_table.py @@ -1,37 +1,128 @@ #!/usr/bin/env python3 -from number_theory.prime_numbers import next_prime +from abc import abstractmethod + +from .number_theory.prime_numbers import next_prime class HashTable: """ - Basic Hash Table example with open addressing and linear probing + Basic Hash Table example with open addressing and linear probing """ - def __init__(self, size_table, charge_factor=None, lim_charge=None): + 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 = [] - self._keys = {} + 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} - def keys(self): + 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): - return sum([1 for slot in self.values - if slot is not None]) / (self.size_table * self.charge_factor) + 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): - return key % self.size_table + def hash_function(self, key) -> int: + """ + Generates hash for the given key value + + Examples: - def _step_by_step(self, step_ord): + 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 - print("step {0}".format(step_ord)) - print([i for i in range(len(self.values))]) + 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): + 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: @@ -39,16 +130,104 @@ def bulk_insert(self, values): self._step_by_step(i) i += 1 - def _set_value(self, key, data): + 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 - def _colision_resolution(self, key, data=None): - new_key = self.hash_function(key + 1) + @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} - while self.values[new_key] is not None \ - and self.values[new_key] != key: + 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: @@ -57,14 +236,30 @@ def _colision_resolution(self, key, data=None): return new_key - def rehashing(self): + 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 ;/ - map(self.insert_data, survivor_values) + 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): + 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: @@ -74,11 +269,15 @@ def insert_data(self, data): pass else: - colision_resolution = self._colision_resolution(key, data) - if colision_resolution is not None: - self._set_value(colision_resolution, data) + 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 index 9689e4fc9fcf..4903db73a782 100644 --- a/data_structures/hashing/hash_table_with_linked_list.py +++ b/data_structures/hashing/hash_table_with_linked_list.py @@ -1,24 +1,27 @@ -from .hash_table import HashTable from collections import deque +from .hash_table import HashTable + class HashTableWithLinkedList(HashTable): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - def _set_value(self, key, data): - self.values[key] = deque([]) if self.values[key] is None else self.values[key] + 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): - return sum([self.charge_factor - len(slot) for slot in self.values])\ - / self.size_table * self.charge_factor - - def _colision_resolution(self, key, data=None): - if not (len(self.values[key]) == self.charge_factor - and self.values.count(None) == 0): - return key - return super()._colision_resolution(key, data) - + 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/prime_numbers.py b/data_structures/hashing/number_theory/prime_numbers.py index 8a521bc45758..82071b5e9f09 100644 --- a/data_structures/hashing/number_theory/prime_numbers.py +++ b/data_structures/hashing/number_theory/prime_numbers.py @@ -1,29 +1,59 @@ #!/usr/bin/env python3 """ - module to operations with prime numbers +module to operations with prime numbers """ +import math -def check_prime(number): - """ - it's not the best solution - """ - special_non_primes = [0,1,2] - if number in special_non_primes[:2]: - return 2 - elif number == special_non_primes[-1]: - return 3 - - return all([number % i for i in range(2, number)]) + +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 check_prime(value): - value += 1 if not ("desc" in kwargs.keys() and kwargs["desc"] is True) else -1 - + + 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 index f7a9ac1ae347..8d8b4457232a 100644 --- a/data_structures/hashing/quadratic_probing.py +++ b/data_structures/hashing/quadratic_probing.py @@ -5,22 +5,80 @@ class QuadraticProbing(HashTable): """ - Basic Hash Table example with open addressing using Quadratic Probing + Basic Hash Table example with open addressing using Quadratic Probing """ - def __init__(self, *args, **kwargs): + + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - def _colision_resolution(self, key, data=None): + 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) + new_key = self.hash_function(key + i * i) - while self.values[new_key] is not None \ - and self.values[new_key] != key: + 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 + 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..98c8b4e11505 --- /dev/null +++ b/data_structures/linked_list/print_reverse.py @@ -0,0 +1,133 @@ +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 + >>> make_linked_list(x for x in [7, 25]) + 7 -> 25 + >>> make_linked_list(x for x in range(0)) + Traceback (most recent call last): + ... + Exception: The Elements List is empty + """ + elements_list = list(elements_list) + 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/queues/queue_on_pseudo_stack.py b/data_structures/queues/queue_on_pseudo_stack.py new file mode 100644 index 000000000000..95b84a309783 --- /dev/null +++ b/data_structures/queues/queue_on_pseudo_stack.py @@ -0,0 +1,59 @@ +"""Queue represented by a pseudo stack (represented by a list with pop and append)""" + +from typing import Any + + +class Queue: + def __init__(self) -> None: + self.stack = [] + self.length = 0 + + def __str__(self) -> str: + printed = "<" + str(self.stack)[1:-1] + ">" + return printed + + """Enqueues {@code item} + @param item + item to enqueue""" + + def put(self, item: Any) -> None: + self.stack.append(item) + self.length = self.length + 1 + + """Dequeues {@code item} + @requirement: |self.length| > 0 + @return dequeued + item that was dequeued""" + + def get(self) -> Any: + self.rotate(1) + dequeued = self.stack[self.length - 1] + self.stack = self.stack[:-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: int) -> None: + for _ in range(rotation): + temp = self.stack[0] + self.stack = self.stack[1:] + self.put(temp) + self.length = self.length - 1 + + """Reports item at the front of self + @return item at front of self.stack""" + + def front(self) -> Any: + front = self.get() + self.put(front) + self.rotate(self.length - 1) + return front + + """Returns the length of this.stack""" + + 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()))