├── .clippy.toml ├── .gitattributes ├── .github └── workflows │ ├── cd.yml │ └── ci.yml ├── .gitignore ├── .rustfmt.toml ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── book.toml ├── publish.sh ├── src ├── 404.md ├── CONTRIBUTING.md ├── README.md ├── SUMMARY.md ├── collections │ ├── associative-container │ │ └── README.md │ ├── bloom_filter │ │ ├── README.md │ │ ├── cuckoo-filter.png │ │ ├── mod.rs │ │ └── quotient-filter.png │ ├── circular_linked_list │ │ └── README.md │ ├── deque │ │ ├── README.md │ │ └── mod.rs │ ├── doubly_linked_list │ │ └── README.md │ ├── hash_map │ │ ├── README.md │ │ ├── layout.dot │ │ ├── layout.svg │ │ └── mod.rs │ ├── linked_list │ │ └── README.md │ ├── mod.rs │ ├── multimap │ │ └── README.md │ ├── ordered_map │ │ └── README.md │ ├── queue │ │ ├── README.md │ │ └── mod.rs │ ├── set │ │ ├── README.md │ │ └── mod.rs │ ├── singly_linked_list │ │ ├── README.md │ │ ├── mod.rs │ │ ├── node-box.svg │ │ └── node-recursive.svg │ └── stack │ │ ├── README.md │ │ └── mod.rs ├── concepts │ └── asymptotic-notation │ │ ├── README.md │ │ ├── fig1.png │ │ ├── fig2.png │ │ ├── fig3.png │ │ ├── fig4.png │ │ └── plot.py ├── hamming_distance │ ├── README.md │ └── mod.rs ├── levenshtein_distance │ ├── 1.png │ ├── 2.png │ ├── README.md │ ├── base.png │ ├── matrix.png │ ├── mod.rs │ ├── opt-1.png │ ├── opt-2.png │ └── pre-opt-1.png ├── lib.rs ├── logo.sketch ├── logo.svg ├── searching │ ├── binary_search │ │ ├── README.md │ │ └── mod.rs │ ├── exponential_search │ │ ├── README.md │ │ └── mod.rs │ ├── interpolation_search │ │ ├── README.md │ │ └── mod.rs │ ├── linear_search │ │ ├── README.md │ │ └── mod.rs │ ├── mod.rs │ └── test_cases.rs └── sorting │ ├── bubble_sort │ ├── README.md │ └── mod.rs │ ├── bucket_sort │ ├── README.md │ └── mod.rs │ ├── counting_sort │ ├── README.md │ └── mod.rs │ ├── heapsort │ ├── README.md │ ├── mod.rs │ └── tree.png │ ├── insertion_sort │ ├── README.md │ └── mod.rs │ ├── introsort │ ├── README.md │ └── mod.rs │ ├── mergesort │ ├── README.md │ └── mod.rs │ ├── mod.rs │ ├── pdqsort │ ├── README.md │ └── mod.rs │ ├── quicksort │ ├── README.md │ └── mod.rs │ ├── radix_sort │ ├── README.md │ └── mod.rs │ ├── selection_sort │ ├── README.md │ └── mod.rs │ ├── shellsort │ ├── README.md │ └── mod.rs │ ├── test_cases.rs │ └── timsort │ ├── README.md │ └── mod.rs └── theme ├── custom.js ├── favicon.png └── favicon.svg /.clippy.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/rust-algorithm-club/1954301f21d44c18ea44864a776a3f6fd72eaefe/.clippy.toml -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.rs text 7 | *.py text 8 | *.svg text 9 | *.md text 10 | 11 | # Denote all files that are truly binary and should not be modified. 12 | *.png binary 13 | *.jpg binary 14 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | deploy: 11 | name: Deploy to GitHub Pages 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Setup mdBook 19 | uses: peaceiris/actions-mdbook@v1 20 | with: 21 | mdbook-version: '0.4.43' 22 | 23 | - name: Build the book 24 | run: | 25 | mdbook build 26 | cargo doc --lib --no-deps 27 | mv target/doc .book/ 28 | 29 | - name: Deploy 30 | uses: peaceiris/actions-gh-pages@v4 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_dir: ./public 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v1 17 | 18 | - name: Cleanup pre-installed Rust toolchains 19 | # The pre-installed toolchain is under root permission, and this would 20 | # make tarball fail to extract. Here we remove the symlink and install 21 | # our own Rust toolchain if necessary. 22 | run: | 23 | which rustup 24 | which cargo 25 | if [[ -L "$HOME/.cargo" && -L "$HOME/.rustup" ]]; then 26 | rm -v "$HOME/.rustup" 27 | rm -v "$HOME/.cargo" 28 | fi 29 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 30 | 31 | - name: Cache rustup 32 | uses: actions/cache@v1 33 | with: 34 | path: ~/.rustup 35 | key: ${{ runner.os }}-rustup-${{ hashFiles('**/Cargo.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-rustup- 38 | 39 | - name: Cache cargo binaries 40 | uses: actions/cache@v1 41 | with: 42 | path: ~/.cargo/bin 43 | key: ${{ runner.os }}-cargo-bin-${{ hashFiles('**/Cargo.lock') }} 44 | restore-keys: | 45 | ${{ runner.os }}-cargo-bin- 46 | 47 | - name: Cache cargo registry cache 48 | uses: actions/cache@v1 49 | with: 50 | path: ~/.cargo/registry/cache 51 | key: ${{ runner.os }}-cargo-registry-cache-${{ hashFiles('**/Cargo.lock') }} 52 | restore-keys: | 53 | ${{ runner.os }}-cargo-registry-cache- 54 | 55 | - name: Cache cargo registry index 56 | uses: actions/cache@v1 57 | with: 58 | path: ~/.cargo/registry/index 59 | key: ${{ runner.os }}-cargo-registry-index-${{ hashFiles('**/Cargo.lock') }} 60 | restore-keys: | 61 | ${{ runner.os }}-cargo-registry-index- 62 | 63 | - name: Cache cargo git db 64 | uses: actions/cache@v1 65 | with: 66 | path: ~/.cargo/git/db 67 | key: ${{ runner.os }}-cargo-git-db-${{ hashFiles('**/Cargo.lock') }} 68 | restore-keys: | 69 | ${{ runner.os }}-cargo-git-db- 70 | 71 | - name: Cache cargo build 72 | uses: actions/cache@v1 73 | with: 74 | path: target 75 | key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} 76 | restore-keys: | 77 | ${{ runner.os }}-cargo-build-target- 78 | 79 | - name: Install stable Rust 80 | run: | 81 | if [[ ! -d "$HOME/.cargo" || ! -d "$HOME/.rustup" ]]; then 82 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup-init.sh 83 | sh rustup-init.sh -y --default-toolchain none 84 | fi 85 | rustup update stable 86 | rustup default stable 87 | 88 | - name: Test and build 89 | run: | 90 | cargo test --verbose 91 | cargo build --verbose 92 | 93 | - name: Lint 94 | run: cargo fmt -- --check # TODO: add clippy 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | .book/ 5 | .vscode/ 6 | .idea/ 7 | .DS_Store 8 | 9 | *.log 10 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Ref: https://rust-lang.github.io/rustfmt/ 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to the Rust Algorithm Club. We appreciate all kinds of contributions. Here are some kinds of tasks you can take. 4 | 5 | - Add new algorithms 6 | - Fix existing bugs 7 | - Polish documetation 8 | 9 | Next, we'll introduce some tips to make contributions easily. 10 | 11 | ## Before Contribuing 12 | 13 | If you decide to make a awesome work, please search [existing issues and pull requests][issues] first, as maybe there are some similar one already reported. 14 | 15 | If there is no duplicate issue, please file a work-in-progress issue to notify others you are working on it. Your time are precious and must prevent from duplicate works. Maintainers would also track those issues in order to keep our club well organized. 16 | 17 | There are also some meta issues tracking features under construction 🚧. Take a look if you are interested in them. 18 | 19 | ## Submit Your Contributions 20 | 21 | Before submitting your contribution, make sure your works satisify the following requirements: 22 | 23 | - Do not break existing tests. Run `cargo test` before sending pull requests. A new algorithm is also expected to contain its own unit tests. 24 | - Every public interface must be documented. The documention needn't be perfect but at least explain its intent and usage clearly. 25 | - Try to keep the writing style and structure consistent across posts. E.g. contains a brief description at first paragraph, explains performance with asymptotic notations. 26 | - Coding style should conform to Rust conventions. Such as using `into` to refer to an ownership transfer or naming additional contructors prefixed by `with`. Currently, the use of [Clippy][rust-clippy] and [rustfmt][rust-fmt] are not required. 27 | 28 | [issues]: https://github.com/weihanglo/rust-algorithm-club/search?q=&type=Issues&utf8=%E2%9C%93 29 | [rust-clippy]: https://github.com/rust-lang-nursery/rust-clippy 30 | [rust-fmt]: https://github.com/rust-lang-nursery/rustfmt 31 | 32 | Welcome to join the Rust Algorithm Club and may algorithms be with you! 33 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "cfg-if" 7 | version = "1.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 10 | 11 | [[package]] 12 | name = "getrandom" 13 | version = "0.2.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 16 | dependencies = [ 17 | "cfg-if", 18 | "libc", 19 | "wasi", 20 | ] 21 | 22 | [[package]] 23 | name = "libc" 24 | version = "0.2.76" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "755456fae044e6fa1ebbbd1b3e902ae19e73097ed4ed87bb79934a867c007bc3" 27 | 28 | [[package]] 29 | name = "ppv-lite86" 30 | version = "0.2.9" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" 33 | 34 | [[package]] 35 | name = "rand" 36 | version = "0.8.4" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" 39 | dependencies = [ 40 | "libc", 41 | "rand_chacha", 42 | "rand_core", 43 | "rand_hc", 44 | ] 45 | 46 | [[package]] 47 | name = "rand_chacha" 48 | version = "0.3.1" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 51 | dependencies = [ 52 | "ppv-lite86", 53 | "rand_core", 54 | ] 55 | 56 | [[package]] 57 | name = "rand_core" 58 | version = "0.6.3" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 61 | dependencies = [ 62 | "getrandom", 63 | ] 64 | 65 | [[package]] 66 | name = "rand_hc" 67 | version = "0.3.1" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" 70 | dependencies = [ 71 | "rand_core", 72 | ] 73 | 74 | [[package]] 75 | name = "rust-algorithm-club" 76 | version = "0.0.1" 77 | dependencies = [ 78 | "rand", 79 | ] 80 | 81 | [[package]] 82 | name = "wasi" 83 | version = "0.10.2+wasi-snapshot-preview1" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 86 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-algorithm-club" 3 | version = "0.0.1" 4 | edition = '2018' 5 | authors = ["Weihang Lo "] 6 | description = "Learn algorithms and data structures with Rust" 7 | homepage = "https://weihanglo.tw/rust-algorithm-club/" 8 | repository = "https://github.com/weihanglo/rust-algorithm-club" 9 | 10 | [dev-dependencies] 11 | rand = "0.8" 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 - 2018 Weihang Lo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

4 | 5 | # Rust Algorithm Club 6 | 7 | > ### 🚧 🚧 This repo is under construction. Most materials are written in Chinese. [Check it out here][main-site] if you are able to read Chinese. 8 | 9 | Welcome to the Rust Algorithm Club! This repository was originally inspired by [Swift Algorithm Club][swift-algorithm-club]. All algorithms here would be explained and implemented in [Rust programming language][rust]! 10 | You can find out more on the [Rust Algorithm Club][main-site] main site. Just pick up some algorithms you are interested in and start learning. If you are brave enough, we recommend you the [auto-generated API documentation][generated-doc]. Go and fight with the source code. 11 | 12 | This project along with its source code are on [GitHub][source-code] and we are looking forward to your contributions. 13 | 14 | [![Rust Edition](https://img.shields.io/badge/Rust_Edition-2018-green.svg)][edition-guide] 15 | [![Build Status](https://github.com/weihanglo/rust-algorithm-club/workflows/CI/badge.svg)][ci-status] 16 | [![Documentation](https://img.shields.io/badge/doc-available-blue.svg)][generated-doc] 17 | 18 | [swift-algorithm-club]: https://github.com/raywenderlich/swift-algorithm-club 19 | [rust]: https://www.rust-lang.org/ 20 | [source-code]: https://github.com/weihanglo/rust-algorithm-club 21 | [main-site]: https://weihanglo.tw/rust-algorithm-club/ 22 | [ci-status]: https://github.com/weihanglo/rust-algorithm-club/actions?query=workflow%3ACI 23 | [generated-doc]: https://weihanglo.tw/rust-algorithm-club/doc/rust_algorithm_club/ 24 | [edition-guide]: https://rust-lang.github.io/edition-guide/rust-2018 25 | 26 | ## General Concepts 27 | 28 | - [Asymptotic Notation](src/concepts/asymptotic-notation) 29 | 30 | ## Algorithms 31 | 32 | ### Searching 33 | 34 | - [Linear search](src/searching/linear_search) 35 | - [Binary search](src/searching/binary_search) 36 | - [Interpolation search](src/searching/interpolation_search) 37 | - [Exponential search](src/searching/exponential_search) 38 | 39 | ### Sorting 40 | 41 | Simple sorts: 42 | 43 | - [Insertion sort](src/sorting/insertion_sort) 44 | - [Selection sort](src/sorting/selection_sort) 45 | - [Bubble sort](src/sorting/bubble_sort) 46 | - [Shellsort](src/sorting/shellsort) 47 | 48 | Efficient sorts: 49 | 50 | - [Heapsort](src/sorting/heapsort) 51 | - [Quicksort](src/sorting/quicksort) 52 | - [Mergesort](src/sorting/mergesort) 53 | 54 | Hybrid sorts (more efficient): 55 | 56 | - 🚧 [Introsort](src/sorting/introsort) 57 | - 🚧 [Timsort](src/sorting/timsort) 58 | - 🚧 [Pdqsort](src/sorting/pdqsort) 59 | 60 | Special-purpose sorts: 61 | 62 | - [Counting sort](src/sorting/counting_sort) 63 | - [Bucket sort](src/sorting/bucket_sort) 64 | - [Radix sort](src/sorting/radix_sort) 65 | 66 | ## Data Structures 67 | 68 | ### Stack and Queue 69 | 70 | - [Stack](src/collections/stack) 71 | - [Queue](src/collections/queue) 72 | - [Deque](src/collections/deque) 73 | 74 | ### Linked List 75 | 76 | [Introduction to linked list](src/collections/linked_list) 77 | 78 | - [Singly linked list](src/collections/singly_linked_list) 79 | - [🚧 Doubly linked list](src/collections/doubly_linked_list) 80 | - [🚧 Circular linked list](src/collections/circular_linked_list) 81 | 82 | ### Associative Container 83 | 84 | [Introduction to associative container](src/collections/associative-container) 85 | 86 | - [Hash map](src/collections/hash_map) 87 | - [🚧 Ordered map](src/collections/ordered_map) 88 | - [🚧 Multimap](src/collections/multimap) 89 | - [Set](src/collections/set) 90 | - [Bloom filter](src/collections/bloom_filter) 91 | 92 | ### String Manipulation 93 | 94 | - [Hamming distance](src/hamming_distance) 95 | - [Levenshtein distance](src/levenshtein_distance) 96 | - [🚧 Longest common substring](src/longest_common_substring) 97 | 98 | ## Learning Resources 99 | 100 | For learning more, you may check out following online resources: 101 | 102 | - [VisuAlgo](https://visualgo.net/) - Probably the best algorithms visualization website. 103 | - [Big-O Cheat Sheet](http://bigocheatsheet.com/) - Comprehensive Big-O notation cheat sheet. 104 | - [Rosetta Code](http://rosettacode.org) - Hundred of solutions of tasks in almost every programming languages. 105 | - [Competitive Programmer's Handbook](https://cses.fi/book.html) - Make you more competitive. The book itself is also competitive. 106 | 107 | ## Contributing 108 | 109 | All contributions are welcome, including typo fix! Please read the [contrubuting](CONTRIBUTING.md) guideline first before starting your work. 110 | 111 | ## Contributors 112 | 113 | - [@weihanglo](https://github.com/weihanglo) 114 | - [@choznerol](https://github.com/choznerol) 115 | - [@henry40408](https://github.com/henry40408) 116 | - [@wiasliaw77210](https://github.com/wiasliaw77210) 117 | - [@LebranceBW](https://github.com/LebranceBW) 118 | 119 | ## License 120 | 121 | This project is released under different licenses based on type of the content. 122 | 123 | - Source code is licensed under [The MIT License (MIT)](LICENSE). 124 | - Articles and creative works are licensed under [Creative Commons 4.0 (CC BY-NC-SA 4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/). 125 | 126 | Copyright © 2017 - 2021 Weihang Lo 127 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | title = "Rust Algorithm Club" 3 | authors = ["Weihang Lo"] 4 | description = "Learn algorithms and data structures with Rust" 5 | 6 | [build] 7 | build-dir = ".book" 8 | create-missing = false 9 | 10 | [output.html] 11 | curly-quotes = true 12 | no-section-label = true 13 | mathjax-support = true 14 | additional-js = ["theme/custom.js"] 15 | git-repository-url = "https://github.com/weihanglo/rust-algorithm-club" 16 | 17 | [output.html.playpen] 18 | editable = true 19 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TARGET_BRANCH="gh-pages" 4 | PUBLIC_DIR=".book" 5 | 6 | if [[ $(git status -s) ]] 7 | then 8 | echo "The working directory is dirty. Please commit any pending changes." 9 | exit 1; 10 | fi 11 | 12 | echo "Deleting old publication" 13 | rm -rf "${PUBLIC_DIR}" 14 | mkdir "${PUBLIC_DIR}" 15 | git worktree prune 16 | rm -rf ".git/worktrees/${PUBLIC_DIR}/" 17 | 18 | echo "Checking out branch ${TARGET_BRANCH} into ${PUBLIC_DIR}" 19 | git worktree add -B "${TARGET_BRANCH}" "${PUBLIC_DIR}" "origin/${TARGET_BRANCH}" 20 | 21 | echo "Removing existing files" 22 | rm -rf "${PUBLIC_DIR}/*" 23 | 24 | echo "Generating site" 25 | mdbook build -d __tmp_book 26 | cargo doc --lib --no-deps 27 | mv "target/doc" __tmp_book/ 28 | cp -rp __tmp_book/* "${PUBLIC_DIR}/" 29 | rm -rf __tmp_book 30 | 31 | echo "Updating branch ${TARGET_BRANCH}" 32 | cd "${PUBLIC_DIR}" && git add --all && \ 33 | git commit -m "Published via publish.sh at $(date)" && \ 34 | git push $1 35 | -------------------------------------------------------------------------------- /src/404.md: -------------------------------------------------------------------------------- 1 | # Oops! Something went wrong... 2 | 3 | ![](https://weihanglo.tw/assets/ferris-404.png) 4 | -------------------------------------------------------------------------------- /src/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 貢獻指南 2 | 3 | 感謝您有興趣貢獻 Rust 演算法俱樂部。我們歡迎各種形式的協助。這裡列出幾種任務供你挑選。 4 | 5 | - 增加新的演算法 6 | - 修正已知的漏洞 7 | - 改善文件的品質 8 | 9 | 接下來,將介紹幾個貢獻的注意事項。 10 | 11 | ## 開始貢獻之前 12 | 13 | 若您決定著手做些厲害的事,請先在[已知 issues 與 pull requests][issues] 搜尋,那裡可能已有回報相似的問題。 14 | 15 | 若沒有重複的問題,請發起一個「進行中(work-in-progress)」的 issue,告知其他人你正在做這項功能。你的時間很寶貴,必須防止重工發生。維護團隊也會追蹤這些 issue 以利管理俱樂部。 16 | 17 | 有些 meta issue 專門追蹤尚未完成的工作 🚧,可以去看看是否有感興趣的主題。 18 | 19 | ## 提交你的成果 20 | 21 | 在提交你的貢獻之前,確認成果滿足下列需求: 22 | 23 | - 不要搞壞既有測試。發起 pull request 前執行 `cargo test`。新的演算法也需包含自身的單元測試。 24 | - 每個對外介面都需要有文件。這個文件不需要完美無缺,但至少清楚說明它的目的與用法。 25 | - 儘量維持文章間寫作風格與結構一致。例如:首段需包含簡扼的敘述、解釋效能時請愛用漸進符號。 26 | - 程式碼撰寫風格應貼近 Rust 的慣例,例如:涉及所有權轉移請使用 `into`、替額外建構式命名請添加 `with` 前綴。目前為止,並不強制使用 [Clippy][rust-clippy] 與 [rustfmt][rust-fmt]。 27 | 28 | [issues]: https://github.com/weihanglo/rust-algorithm-club/search?q=&type=Issues&utf8=%E2%9C%93 29 | [rust-clippy]: https://github.com/rust-lang-nursery/rust-clippy 30 | [rust-fmt]: https://github.com/rust-lang-nursery/rustfmt 31 | 32 | ### 歡迎加入 Rust 演算法俱樂部,願演算法與你同在! 33 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

4 | 5 | # Rust Algorithm Club 6 | 7 | 歡迎來到 Rust 演算法俱樂部!本專案受 [Swift Algorithm Club][swift-algorithm-club] 啟發,專案中的演算法皆使用 [Rust 程式語言][rust]撰寫說明與實作!您可以在 [Rust Algorithm Club][main-site] 一站,依您的意願,挑選有興趣的演算法知識學習;若您夠大膽,推薦您閱讀[自動生成的 API 文件][generated-doc],直接單挑程式原始碼。 8 | 9 | 本專案原始碼放在 [GitHub][source-code] 上,非常期待您的貢獻。 10 | 11 | [![Rust Edition](https://img.shields.io/badge/Rust_Edition-2018-green.svg)][edition-guide] 12 | [![Build Status](https://github.com/weihanglo/rust-algorithm-club/workflows/CI/badge.svg)][ci-status] 13 | [![Documentation](https://img.shields.io/badge/doc-available-blue.svg)][generated-doc] 14 | 15 | [swift-algorithm-club]: https://github.com/raywenderlich/swift-algorithm-club 16 | [rust]: https://www.rust-lang.org/ 17 | [source-code]: https://github.com/weihanglo/rust-algorithm-club 18 | [main-site]: https://weihanglo.tw/rust-algorithm-club/ 19 | [ci-status]: https://github.com/weihanglo/rust-algorithm-club/actions?query=workflow%3ACI 20 | [generated-doc]: https://weihanglo.tw/rust-algorithm-club/doc/rust_algorithm_club/ 21 | [edition-guide]: https://rust-lang.github.io/edition-guide/rust-2018 22 | 23 | ## 基礎概念 24 | 25 | - [漸進符號 Asymptotic Notation](concepts/asymptotic-notation) 26 | 27 | ## 演算法 28 | 29 | ### 搜尋 30 | 31 | - [線性搜尋 Linear search](searching/linear_search) 32 | - [二元搜尋 Binary search](searching/binary_search) 33 | - [內插搜尋 Interpolation search](searching/interpolation_search) 34 | - [指數搜尋 Exponential search](searching/exponential_search) 35 | 36 | ### 排序 37 | 38 | 簡單排序: 39 | 40 | - [插入排序 Insertion sort](sorting/insertion_sort) 41 | - [選擇排序 Selection sort](sorting/selection_sort) 42 | - [氣泡排序 Bubble sort](sorting/bubble_sort) 43 | - [希爾排序 Shellsort](sorting/shellsort) 44 | 45 | 高效排序: 46 | 47 | - [堆積排序 Heapsort](sorting/heapsort) 48 | - [快速排序 Quicksort](sorting/quicksort) 49 | - [合併排序 Mergesort](sorting/mergesort) 50 | 51 | 混合排序(更高效): 52 | 53 | - 🚧 [內省排序 Introsort](sorting/introsort) 54 | - 🚧 [自適應的合併排序 Timsort](sorting/timsort) 55 | - 🚧 [模式消除快速排序 Pdqsort](sorting/pdqsort) 56 | 57 | 特殊排序: 58 | 59 | - [計數排序 Counting sort](sorting/counting_sort) 60 | - [桶排序 Bucket sort](sorting/bucket_sort) 61 | - [基數排序 Radix sort](sorting/radix_sort) 62 | 63 | ## 資料結構 64 | 65 | ### 堆疊與佇列 66 | 67 | - [堆疊 Stack](collections/stack) 68 | - [佇列 Queue](collections/queue) 69 | - [雙端佇列 Deque](collections/deque) 70 | 71 | ### 鏈結串列 72 | 73 | [鏈結串列概述](collections/linked_list) 74 | 75 | - [單向鏈結串列 Singly linked list](collections/singly_linked_list) 76 | - [🚧 雙向鏈結串列 Doubly linked list](collections/doubly_linked_list) 77 | - [🚧 循環鏈結串列 Circular linked list](collections/circular_linked_list) 78 | 79 | ### 關聯容器 80 | 81 | [關聯容器概述](collections/associative-container) 82 | 83 | - [雜湊表 Hash map](collections/hash_map) 84 | - [🚧 有序映射表 Ordered map](collections/ordered_map) 85 | - [🚧 多重映射表 Multimap](collections/multimap) 86 | - [集合 Set](collections/set) 87 | - [布隆過濾器 Bloom filter](collections/bloom_filter/) 88 | 89 | ### 字串處理 90 | 91 | - [漢明距離 Hamming distance](hamming_distance) 92 | - [萊文斯坦距離 Levenshtein distance](levenshtein_distance) 93 | - [🚧 最長共同子字串 Longest common substring](longest_common_substring) 94 | 95 | ## 學習資源 96 | 97 | 有許多優秀的網站與學習資源,分享給大家學習演算法。 98 | 99 | - [VisuAlgo](https://visualgo.net/) - 也許是最好的演算法視覺化專案。 100 | - [Big-O Cheat Sheet](http://bigocheatsheet.com/) - 最全面的 Big O cheat sheet。 101 | - [Rosetta Code](http://rosettacode.org) - 使用各種程式語言,解答上百種不同程式問題。 102 | - [Competitive Programmer's Handbook](https://cses.fi/book.html) - 讓你更有競爭力。這書本身也很有競爭力。 103 | 104 | ## 如何貢獻 105 | 106 | 歡迎各式各樣的貢獻,修正錯字也行!開始動手之前,請先閱讀[貢獻指南](CONTRIBUTING.md)。 107 | 108 | ## 貢獻者 109 | 110 | - [@weihanglo](https://github.com/weihanglo) 111 | - [@choznerol](https://github.com/choznerol) 112 | - [@henry40408](https://github.com/henry40408) 113 | - [@wiasliaw77210](https://github.com/wiasliaw77210) 114 | - [@LebranceBW](https://github.com/LebranceBW) 115 | 116 | ## 授權條款 117 | 118 | 本專案分為兩部分授權: 119 | 120 | - 程式碼與函式庫依據 [The MIT License (MIT)](https://github.com/weihanglo/rust-algorithm-club/blob/master/LICENSE) 授權條款發佈。 121 | - 文章與相關著作依據 [Creative Commons 4.0 (CC BY-NC-SA 4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/) 授權條款發佈。 122 | 123 | Copyright © 2017 - 2021 Weihang Lo 124 | -------------------------------------------------------------------------------- /src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # 目錄 2 | 3 | [Rust Algorithm Club](README.md) 4 | 5 | ------ 6 | 7 | # 💡 基礎概念 8 | 9 | - [漸進符號 Asymptotic Notation](concepts/asymptotic-notation/README.md) 10 | 11 | ------ 12 | 13 | # 🔍 搜尋 14 | 15 | - [線性搜尋 Linear search](searching/linear_search/README.md) 16 | - [二元搜尋 Binary search](searching/binary_search/README.md) 17 | - [內插搜尋 Interpolation search](searching/interpolation_search/README.md) 18 | - [指數搜尋 Exponential search](searching/exponential_search/README.md) 19 | 20 | ------ 21 | 22 | # 📚 排序 23 | 24 | # 簡單排序 25 | 26 | - [插入排序 Insertion sort](sorting/insertion_sort/README.md) 27 | - [選擇排序 Selection sort](sorting/selection_sort/README.md) 28 | - [氣泡排序 Bubble sort](sorting/bubble_sort/README.md) 29 | - [希爾排序 Shellsort](sorting/shellsort/README.md) 30 | 31 | # 高效排序 32 | 33 | - [堆積排序 Heapsort](sorting/heapsort/README.md) 34 | - [快速排序 Quicksort](sorting/quicksort/README.md) 35 | - [合併排序 Mergesort](sorting/mergesort/README.md) 36 | 37 | # 混合排序 38 | 39 | - [🚧 內省排序 Introsort]() 40 | - [🚧 自適應合併排序 Timsort]() 41 | - [🚧 模式消除快速排序 Pdqsort]() 42 | 43 | # 特殊排序 44 | 45 | - [計數排序 Counting sort](sorting/counting_sort/README.md) 46 | - [桶排序 Bucket sort](sorting/bucket_sort/README.md) 47 | - [基數排序 Radix sort](sorting/radix_sort/README.md) 48 | 49 | ------ 50 | 51 | # 🏠 資料結構 52 | 53 | # 堆疊與佇列 54 | 55 | - [堆疊 Stack](collections/stack/README.md) 56 | - [佇列 Queue](collections/queue/README.md) 57 | - [雙端佇列 Deque](collections/deque/README.md) 58 | 59 | # 鏈結串列 60 | 61 | - [鏈結串列概述](collections/linked_list/README.md) 62 | - [單向鏈結串列 Singly linked list](collections/singly_linked_list/README.md) 63 | - [🚧 雙向鏈結串列 Doubly linked list]() 64 | - [🚧 循環鏈結串列 Circular linked list]() 65 | 66 | # 關聯容器 67 | 68 | - [關聯容器概述](collections/associative-container/README.md) 69 | - [雜湊表 Hash map](collections/hash_map/README.md) 70 | - [🚧 有序映射表 Ordered map]() 71 | - [🚧 多重映射表 Multimap]() 72 | - [集合 Set](collections/set/README.md) 73 | - [布隆過濾器 Bloom filter](collections/bloom_filter/README.md) 74 | 75 | ------ 76 | 77 | # 🧵 字串處理 78 | 79 | - [漢明距離 Hamming distance](hamming_distance/README.md) 80 | - [萊文斯坦距離 Levenshtein distance](levenshtein_distance/README.md) 81 | - [🚧 最長共同子字串 Longest common substring]() 82 | 83 | ------ 84 | 85 | [貢獻指南](CONTRIBUTING.md) 86 | 87 | [404](404.md) 88 | -------------------------------------------------------------------------------- /src/collections/associative-container/README.md: -------------------------------------------------------------------------------- 1 | # 關聯容器 Associative Container 2 | 3 | 關聯容器是一種抽象資料型別,儲存鍵與值配對關係(key-value pair)的集合,並透過鍵存取元素,所謂「鍵值對」好比身份證字號與公民,戶政單位知道一個人證號,就可在關聯容器內,透過證號查找是否有這個公民,以及此證號對應的公民基本資訊。 4 | 5 | 關聯容器有許多別名,例如字典(dictionary)、關聯陣列(associative array)、映射(map)、表(table)等。在大多數程式語言函式庫中,關聯容器通常是最基本的容器型別之一,如 Python 的 `dict`,JavaScript 的 `Map`,以及 Rust 的 `HashMap`。 6 | 7 | 方便起見,本文以「**映射表**」統稱這類集合型別。 8 | 9 | ![](https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/Hash_table_5_0_1_1_1_1_0_LL.svg/1280px-Hash_table_5_0_1_1_1_1_0_LL.svg.png) 10 | 11 | _(雜湊表示意圖)_ 12 | 13 | ## 特性 14 | 15 | 一般來說,映射表有以下特性: 16 | 17 | - **鍵值對為單向關係**:可透過鍵取得其唯一值;但無法確保一值僅對應唯一的鍵。 18 | - **鍵值唯一性**:同個映射表內,同個鍵不重複,只會出現一次。 19 | - **元素組合性**:映射表內每個元素都是「鍵值對」,鍵或值無法單獨存在。 20 | - **操作開銷小**:合理實作下,基本操作開銷相對較小,不高於線性時間。 21 | 22 | > 註:多重映射表為一對多的例外。 23 | 24 | 映射表會有以下幾種基本操作: 25 | 26 | - **新增**:配對鍵值關聯,又稱為綁定 binding。 27 | - **修改**:修改任意鍵之下的值。 28 | - **移除**:透過任意鍵移除該鍵值對,又稱 unbinding。 29 | - **查找**:透過任意鍵搜尋該鍵值對。 30 | 31 | 不難看出,基本操作都是透過鍵取得值。事實上,合理實作的映射表,只要透過鍵來操作,就能有良好效能,甚至上述操作能達到 $O(1)$ 複雜度。 32 | 33 | ## 適用場景 34 | 35 | 雖然映射表依實作不同,效能有所權衡。但其最大優勢仍是可「高效地透過鍵尋找值」,只要有映射關係的資料,都非常適合使用映射表。例如,快取暫存機制需透過特定鍵快速查找暫存值。此外,現代常用的 JSON、TOML 等資料交換格式,都是「鍵—值對」的形式,非常適合使用映射表處理。而應用映射表最有名的實際案例莫過於資料庫的索引,透過索引,我們可以大大降低搜尋的成本,從線性時間直落到對數甚至常數時間,不過相對就需要付出額外時空間建立索引。 36 | 37 | 我們再次把應用場景條列出來,方便懶人帶著走。 38 | 39 | - 有映射關係,處理「鍵—值」配對的資料結構。 40 | - 處理 JSON、TOML 等資料交換,資料序列化。 41 | - 實作快取(cache)機制。 42 | - 資料庫索引的實作方法之一。 43 | - 查找操作頻率遠高於其他操作時。 44 | 45 | 總的來說,只要資料有對應綁定關係,就可以考慮使用映射表處理。 46 | 47 | ## 種類 48 | 49 | 以下簡單介紹常見的映射表,詳情請點擊各連結。 50 | 51 | ### 雜湊表 Hash Map 52 | 53 | [雜湊表](../hash_map)是以雜湊函數實作的映射表。透過[雜湊函數](../../hash)將任意資料轉換為固定長度的雜湊值,並將此鍵與一筆資料綁定,再映射到內部資料結構的某位置。理論上,只要雜湊函數品質過得去,雜湊表的基本操作都能在常數時間完成。 54 | 55 | ### 有序映射表 Ordered Map 56 | 57 | [有序映射表](../ordered_map)係一種有特定排序方式的映射表。常見兩種排序方式,其一是依照插入映射表的先後順序;其二則是依照鍵的大小。不同排序的底層資料結構各異,操作複雜度也不盡相同,如依鍵大小排序的映射表通常使用搜索樹實作,因此「新增」操作的複雜度為較差的 $O(\log n)$。 58 | 59 | ### 多重映射表 Multimap 60 | 61 | [多重映射表](../multimap)允許鍵值對重複,一個鍵可對應多個值(一對多)。類似於映射表內放入陣列,但能以較方便輕鬆的介面來操作或疊代整張映射表。 62 | 63 | ### 集合 Set 64 | 65 | [集合](../set)實際上並無鍵值「關聯」,可將其想像成普通的映射表。只關心鍵而值不重要。集合借用了數學[集合論(set theory)][set-theory]中有限集合的概念,常應用於需要操作交集、聯集、差集等集合運算場景。 66 | 67 | [set-theory]: https://en.wikipedia.org/wiki/Set_theory 68 | 69 | ### 布隆過濾器 Bloom Filter 70 | 71 | [布隆過濾器](../bloom_filter)是一種類似於集合,但只會回報「絕對不存在」或「可能存在」的機率資料結構,實作上節省空間,常用於在海量資料中確認成員是否存在,並能有一定容錯率的場景。 72 | 73 | ## 參考資料 74 | 75 | - [Wiki: Associative array](https://en.wikipedia.org/wiki/Associative_array) 76 | - [Wiki: Associative containers](https://en.wikipedia.org/wiki/Associative_containers) 77 | - [cpprefernce.com: std::map](https://en.cppreference.com/w/cpp/container/map) 78 | - [Rust documentation: std::colledtion](https://doc.rust-lang.org/stable/std/collections/) 79 | - Map graph by Jorge Stolfi [CC BY-SA-3.0](http://creativecommons.org/licenses/by-sa/3.0/) via Wikimedia Commons. 80 | -------------------------------------------------------------------------------- /src/collections/bloom_filter/cuckoo-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/rust-algorithm-club/1954301f21d44c18ea44864a776a3f6fd72eaefe/src/collections/bloom_filter/cuckoo-filter.png -------------------------------------------------------------------------------- /src/collections/bloom_filter/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map::{DefaultHasher, RandomState}; 2 | use std::hash::{BuildHasher, Hash, Hasher}; 3 | use std::marker::PhantomData; 4 | 5 | /// A space efficient probablistic data structures offering an approximate 6 | /// containment test with only false positive error. 7 | /// 8 | /// The false positive error probability _ε_ of a Bloom filter is defined as 9 | /// the probability that a Bloom filter claims an element is contained in it 10 | /// but actually not. 11 | /// 12 | /// The count of hash functions denoted by _k_ indicates thant _k_ different 13 | /// hash functions that map _k_ position on the bit array. Typically _k_ is a 14 | /// small constant depends on error probability _ε_. 15 | /// 16 | /// The underlying container is a bit array of _m_ bits, where the optimal _m_ 17 | /// is proportional to count of hash functions _k_. 18 | /// 19 | /// Cheat sheet: 20 | /// 21 | /// - _m_: total bits (memory usage) 22 | /// - _n_: expected number of input elements (cardinality) 23 | /// - _k_: number of hash functions counted for each input 24 | /// - _ε_: expected false positive error probability 25 | /// 26 | /// References: 27 | /// 28 | /// - [Google Guava: BloomFilter][1] 29 | /// - [Onat: Let's implement a Bloom Filter][2] 30 | /// 31 | /// [1]: https://github.com/google/guava/blob/v29.0/guava/src/com/google/common/hash/BloomFilter.java 32 | /// [2]: https://onatm.dev/2020/08/10/let-s-implement-a-bloom-filter/ 33 | pub struct BloomFilter { 34 | /// The bit array of _m_ bits that stores existence information of elements. 35 | bits: Vec, 36 | /// Count of hash functions. Denoted by _k_. 37 | hash_fn_count: usize, 38 | /// The hashers that do real works. See [Less Hashing, Same Performance:Building a Better Bloom Filter][1] 39 | /// to figure out why two-hashers strategy would not significantly deteriorate 40 | /// the performance of a Bloom filter. 41 | /// 42 | /// [1]: https://www.eecs.harvard.edu/~michaelm/postscripts/rsa2008.pdf 43 | hashers: [DefaultHasher; 2], 44 | _phantom: PhantomData, 45 | } 46 | 47 | impl BloomFilter { 48 | /// Creates an empty Bloom filter with desired capacity and error rate. 49 | /// 50 | /// This constructor would give an optimal size for bit array based on 51 | /// provided `capacity` and `err_rate`. 52 | /// 53 | /// # Parameters 54 | /// 55 | /// * `capacity` - Expected size of elements will put in. 56 | /// * `err_rate` - False positive error probability. 57 | pub fn new(capacity: usize, err_rate: f64) -> Self { 58 | let bits_count = Self::optimal_bits_count(capacity, err_rate); 59 | let hash_fn_count = Self::optimal_hashers_count(err_rate); 60 | let hashers = [ 61 | RandomState::new().build_hasher(), 62 | RandomState::new().build_hasher(), 63 | ]; 64 | 65 | Self { 66 | bits: vec![false; bits_count], 67 | hash_fn_count, 68 | hashers, 69 | _phantom: PhantomData, 70 | } 71 | } 72 | 73 | /// Inserts an element into the container. 74 | /// 75 | /// This function simulates multiple hashers with only two hashers using 76 | /// the following formula: 77 | /// 78 | /// > g_i(x) = h1(x) + i * h2(x) 79 | /// 80 | /// # Parameters 81 | /// 82 | /// * `elem` - Element to be inserted. 83 | /// 84 | /// # Complexity 85 | /// 86 | /// Linear in the size of `hash_fn_count` _k_. 87 | pub fn insert(&mut self, elem: &T) 88 | where 89 | T: Hash, 90 | { 91 | // g_i(x) = h1(x) + i * h2(x) 92 | let hashes = self.make_hash(elem); 93 | for fn_i in 0..self.hash_fn_count { 94 | let index = self.get_index(hashes, fn_i as u64); 95 | self.bits[index] = true; 96 | } 97 | } 98 | 99 | /// Returns whether an element is present in the container. 100 | /// 101 | /// # Parameters 102 | /// 103 | /// * `elem` - Element to be checked whether is in the container. 104 | /// 105 | /// # Complexity 106 | /// 107 | /// Linear in the size of `hash_fn_count` _k_. 108 | pub fn contains(&self, elem: &T) -> bool 109 | where 110 | T: Hash, 111 | { 112 | let hashes = self.make_hash(elem); 113 | (0..self.hash_fn_count).all(|fn_i| { 114 | let index = self.get_index(hashes, fn_i as u64); 115 | self.bits[index] 116 | }) 117 | } 118 | 119 | /// Gets index of the bit array for a single hash iteration. 120 | /// 121 | /// As a part of multiple hashers simulation for this formula: 122 | /// 123 | /// > g_i(x) = h1(x) + i * h2(x) 124 | /// 125 | /// This function calculate the right hand side of the formula. 126 | /// 127 | /// Note that the usage fo `wrapping_` is acceptable here for a hash 128 | /// algorithm to get a valid slot. 129 | fn get_index(&self, (h1, h2): (u64, u64), fn_i: u64) -> usize { 130 | (h1.wrapping_add(fn_i.wrapping_mul(h2)) % self.bits.len() as u64) as usize 131 | } 132 | 133 | /// Hashes the element. 134 | /// 135 | /// As a part of multiple hashers simulation for this formula: 136 | /// 137 | /// > g_i(x) = h1(x) + i * h2(x) 138 | /// 139 | /// This function do the actual `hash` work with two independant hashers, 140 | /// returing both h1(x) and h2(x) within a tuple. 141 | fn make_hash(&self, elem: &T) -> (u64, u64) 142 | where 143 | T: Hash, 144 | { 145 | let hasher1 = &mut self.hashers[0].clone(); 146 | let hasher2 = &mut self.hashers[1].clone(); 147 | 148 | elem.hash(hasher1); 149 | elem.hash(hasher2); 150 | 151 | (hasher1.finish(), hasher2.finish()) 152 | } 153 | 154 | /// m = -1 * (n * ln ε) / (ln 2)^2 155 | /// 156 | /// See [Wikipedia: Bloom filter][1]. 157 | /// 158 | /// [1]: https://en.wikipedia.org/wiki/Bloom_filter#Optimal_number_of_hash_functions 159 | fn optimal_bits_count(capacity: usize, err_rate: f64) -> usize { 160 | let ln_2_2 = std::f64::consts::LN_2.powf(2f64); 161 | (-1f64 * capacity as f64 * err_rate.ln() / ln_2_2).ceil() as usize 162 | } 163 | 164 | /// k = -log_2 ε 165 | /// 166 | /// See [Wikipedia: Bloom filter][1]. 167 | /// 168 | /// [1]: https://en.wikipedia.org/wiki/Bloom_filter#Optimal_number_of_hash_functions 169 | fn optimal_hashers_count(err_rate: f64) -> usize { 170 | (-1f64 * err_rate.log2()).ceil() as usize 171 | } 172 | } 173 | 174 | #[cfg(test)] 175 | mod classic { 176 | use super::BloomFilter; 177 | 178 | #[test] 179 | fn insert() { 180 | let mut bf = BloomFilter::new(100, 0.01); 181 | (0..20).for_each(|i| bf.insert(&i)); 182 | (0..20).for_each(|i| assert!(bf.contains(&i))); 183 | } 184 | 185 | #[test] 186 | fn contains() { 187 | let mut bf = BloomFilter::new(100, 0.1); 188 | assert!(!bf.contains("1")); 189 | bf.insert("1"); 190 | assert!(bf.contains("1")); 191 | } 192 | 193 | #[test] 194 | fn err_rate_100() { 195 | let bf = BloomFilter::new(100, 1.0); 196 | // Test correctness of optimal formula. 197 | assert_eq!(bf.bits.len(), 0); 198 | // Always positive 199 | assert!(bf.contains("1")); 200 | assert!(bf.contains("2")); 201 | assert!(bf.contains("3")); 202 | } 203 | 204 | #[test] 205 | fn get_one_slots_bf() { 206 | let mut bf = BloomFilter::new(1, 0.8); 207 | // Test correctness of optimal formula. 208 | assert_eq!(bf.bits.len(), 1); 209 | assert!(!bf.contains("1")); 210 | bf.insert("1"); 211 | // Now all slots are occupied. 212 | assert!(bf.contains("2")); 213 | assert!(bf.contains("3")); 214 | } 215 | 216 | #[test] 217 | fn is_a_generics_container() { 218 | let mut bf = BloomFilter::new(100, 0.1); 219 | bf.insert("1"); 220 | assert!(bf.contains("1")); 221 | let mut bf = BloomFilter::new(100, 0.1); 222 | bf.insert(&1); 223 | assert!(bf.contains(&1)); 224 | let mut bf = BloomFilter::new(100, 0.1); 225 | bf.insert(&'1'); 226 | assert!(bf.contains(&'1')); 227 | 228 | #[derive(Hash)] 229 | struct S; 230 | let mut bf = BloomFilter::new(100, 0.1); 231 | let s = S; 232 | assert!(!bf.contains(&s)); 233 | bf.insert(&s); 234 | assert!(bf.contains(&s)); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/collections/bloom_filter/quotient-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/rust-algorithm-club/1954301f21d44c18ea44864a776a3f6fd72eaefe/src/collections/bloom_filter/quotient-filter.png -------------------------------------------------------------------------------- /src/collections/circular_linked_list/README.md: -------------------------------------------------------------------------------- 1 | # 循環鏈結串列 Circular linked list 2 | 3 | ## 操作 4 | 5 | ## 效能 6 | -------------------------------------------------------------------------------- /src/collections/doubly_linked_list/README.md: -------------------------------------------------------------------------------- 1 | # 雙向鏈結串列 Doubly linked list 2 | 3 | ## 操作 4 | 5 | ## 效能 6 | -------------------------------------------------------------------------------- /src/collections/hash_map/layout.dot: -------------------------------------------------------------------------------- 1 | digraph { 2 | rankdir=LR; 3 | overlap=false; 4 | node [shape="plaintext"]; 5 | 6 | hashmap[label=< 7 | 8 | 9 | 10 |
HashMap
buckets
len4
>]; 11 | buckets[label=< 12 | 13 | 14 | 15 | 16 | 17 | 18 |
Vec<Bucket>
indexptr
0
1
2
3
>]; 19 | 20 | vec0[label=< 21 | 22 | 23 | 24 | 25 |
Bucket<K, V>
indexpair
0John, blue
1Bob, green
>]; 26 | vec1[label=< 27 | 28 | 29 | 30 |
Bucket<K, V>
indexpair
0Alice, red
>]; 31 | 32 | vec2[label=< 33 | 34 | 35 | 36 |
Bucket<K, V>
indexpair
0Eve, black
>]; 37 | 38 | edge[tailclip="false"]; 39 | hashmap:to_buckets:c -> buckets:from_hashmap; 40 | buckets:to_vec0:c -> vec0:from_buckets; 41 | buckets:to_vec1:c -> vec1:from_buckets; 42 | buckets:to_vec2:c -> vec2:from_buckets; 43 | } 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/collections/hash_map/layout.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | %0 11 | 12 | 13 | 14 | hashmap 15 | 16 | HashMap 17 | 18 | buckets 19 | 20 | 21 | len 22 | 23 | 4 24 | 25 | 26 | 27 | buckets 28 | 29 | Vec<Bucket> 30 | 31 | index 32 | 33 | ptr 34 | 35 | 0 36 | 37 | 38 | 1 39 | 40 | 41 | 2 42 | 43 | 44 | 3 45 | 46 | 47 | 48 | 49 | hashmap:c->buckets:from_hashmap 50 | 51 | 52 | 53 | 54 | 55 | vec0 56 | 57 | Bucket<K, V> 58 | 59 | index 60 | 61 | pair 62 | 63 | 0 64 | 65 | John, blue 66 | 67 | 1 68 | 69 | Bob, green 70 | 71 | 72 | 73 | buckets:c->vec0:from_buckets 74 | 75 | 76 | 77 | 78 | 79 | vec1 80 | 81 | Bucket<K, V> 82 | 83 | index 84 | 85 | pair 86 | 87 | 0 88 | 89 | Alice, red 90 | 91 | 92 | 93 | buckets:c->vec1:from_buckets 94 | 95 | 96 | 97 | 98 | 99 | vec2 100 | 101 | Bucket<K, V> 102 | 103 | index 104 | 105 | pair 106 | 107 | 0 108 | 109 | Eve, black 110 | 111 | 112 | 113 | buckets:c->vec2:from_buckets 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /src/collections/linked_list/README.md: -------------------------------------------------------------------------------- 1 | # 鏈結串列 Linked list 2 | 3 | 鏈結串列是一種基本線性資料集合,每一個資料元素都是獨立的物件。儲存資料的方式和一般陣列配置連續物理記憶體空間不同,而是在各節點儲存額外的指標指向下一個節點。 4 | 5 | ![](https://upload.wikimedia.org/wikipedia/commons/thumb/6/6d/Singly-linked-list.svg/612px-Singly-linked-list.svg.png) 6 | 7 | _(單向鏈結串列示意圖)_ 8 | 9 | ## 特性 10 | 11 | 鏈結串列有以下特性與優點: 12 | 13 | - 不需事先知道資料型別大小,充分利用動態記憶體管理。 14 | - 以常數時間插入/刪除,不需重新配置記憶體(reallocation)。 15 | 16 | 但也因動態配置記憶體等因素,連帶產生一些缺陷: 17 | 18 | - **空間開銷大**:每個元素需儲存額外的指標空間。 19 | - **較差的 CPU 快取**:不連續存取的特性,不利於 [CPU 快取][wiki-cpu-cache]。 20 | - **不允許隨機存取(random access)**:搜尋特定節點仍需線性時間循序存取。 21 | 22 | [wiki-cpu-cache]: https://en.wikipedia.org/wiki/CPU_cache 23 | 24 | ## 適用場景 25 | 26 | 大多數的場景其實不太常使用鏈結串列,Rust 內建的 [`LinkedList`][rust-linked-list] 文件也建議,除非肯定要用鏈結串列,不然建議優先考慮其他類似的資料結構如 [`VecDeque`][rust-vec-deque]。話雖如此,鏈結串列仍有不少應用場景: 27 | 28 | - 需要頻繁地插入與刪除資料。 29 | - 需要頻繁分離與合併(split and merge)資料。 30 | - 不需要隨機存取的資料。 31 | - 遞迴友好,因此成為大多函數式語言中基本資料型別之一。 32 | - 教學上,常用於實作抽象資料型別,如[堆疊](../stack)與[佇列](../queue)等等。 33 | 34 | [rust-linked-list]: https://doc.rust-lang.org/std/collections/struct.LinkedList.html 35 | [rust-vec-deque]: https://doc.rust-lang.org/std/collections/vec_deque/struct.VecDeque.html 36 | 37 | ## 術語 38 | 39 | ### Node 40 | 41 | 又稱「節點」,為組成鏈結串列的基本元素,節點包含資料儲存區與指標儲存區,指標儲存區用以儲存指向其他節點位址的變數。此外,最後一個節點的不指向其他節點位址的指標成為 null pointer,慣例以 NULL 表示。 42 | 43 | ![node-box](../singly_linked_list/node-box.svg) 44 | 45 | _(節點示意圖)_ 46 | 47 | ### Head and tail 48 | 49 | Head 為指向整個串列第一個節點的指標。而 tail 則為指向最後一個節點的指標。用 ASCII 圖表示如下: 50 | 51 | ``` 52 | head tail 53 | | | 54 | v v 55 | +--------+ +--------+ +--------+ 56 | | | | | | | 57 | | node 0 |-->| node 1 |-->| node 2 |--> NULL 58 | | | | | | | 59 | +--------+ +--------+ +--------+ 60 | ``` 61 | 62 | ### Sentinel node 63 | 64 | Sentinal node 一個特殊的節點,資料值為 NULL 的節點,用意代表鏈結串列的端點。也就是說,sentinel node 指向串列第一個節點,而串列最後一個節點也會指向 sentinel node,就像哨兵一樣守著串列前後,因而得名。 65 | 66 | 實作鏈結串列時,常常因為判斷節點是否為 NULL 而讓程式變得複雜,而 sentinel node 可減少程式操作步驟,也能增加程式可讀性。詳細資訊可以參考這篇 [NULL 與 sentinel node 的比較討論](https://stackoverflow.com/questions/5384358/)。 67 | 68 | ``` 69 | +-----------------------------------------------+ 70 | | | 71 | v | 72 | +---------+ +--------+ +--------+ +--------+ | 73 | |sentinel | | | | | | | | 74 | | |-->| node 0 |-->| node 1 |-->| node 3 |--+ 75 | | node | | | | | | | 76 | +---------+ +--------+ +--------+ +--------+ 77 | ``` 78 | 79 | ## 種類 80 | 81 | 依據每個節點的鏈結多寡,可分為 82 | 83 | [單向鏈結串列](../singly_linked_list),每個節點只有一個指標,指向下一個節點。 84 | 85 | ``` 86 | +--------+ +--------+ +--------+ 87 | | | | | | | 88 | | node 0 |-->| node 1 |-->| node 2 |--> NULL 89 | | | | | | | 90 | +--------+ +--------+ +--------+ 91 | ``` 92 | 93 | [雙向鏈結串列](../doubly_linked_list),每個節點有兩個指標,分別指向前後一個節點。 94 | 95 | ``` 96 | +--------+ +--------+ +--------+ 97 | | |-->| |-->| |--> NULL 98 | | node 0 | | node 1 | | node 2 | 99 | NULL <--| |<--| |<--| | 100 | +--------+ +--------+ +--------+ 101 | ``` 102 | 103 | 倘若該鏈結串列末端節點的指標指向第一個的節點,形成一個循環,則稱之為「[循環鏈結串列](../circular_linked_list)」。 104 | 105 | ``` 106 | Singly linked list as circular 107 | 108 | +-----------------------------------------+ 109 | | | 110 | | +--------+ +--------+ +--------+ | 111 | | | | | | | | | 112 | +-->| node 0 |-->| node 1 |-->| node 3 |--+ 113 | | | | | | | 114 | +--------+ +--------+ +--------+ 115 | ``` 116 | 117 | 詳細說明與實作請點選各個連結。 118 | 119 | ## 參考資料 120 | 121 | - [Wiki: Linked list](https://en.wikipedia.org/wiki/Linked_list) 122 | - Singly linked list SVG By Lasindi [Public domain], via Wikimedia Commons. 123 | -------------------------------------------------------------------------------- /src/collections/mod.rs: -------------------------------------------------------------------------------- 1 | //! Colletions are general-purpose or specialized data structures in which elements store. 2 | 3 | mod singly_linked_list; 4 | pub use self::singly_linked_list::SinglyLinkedList; 5 | 6 | mod hash_map; 7 | pub use self::hash_map::HashMap; 8 | 9 | mod set; 10 | pub use self::set::HashSet; 11 | 12 | mod stack; 13 | pub use self::stack::Stack; 14 | 15 | mod bloom_filter; 16 | pub use self::bloom_filter::BloomFilter; 17 | 18 | mod deque; 19 | pub use self::deque::Deque; 20 | 21 | mod queue; 22 | pub use self::queue::Queue; 23 | -------------------------------------------------------------------------------- /src/collections/multimap/README.md: -------------------------------------------------------------------------------- 1 | # 多重映射表 Multimap 2 | 3 | 允許鍵值對重複,一個鍵可對應多個值(一對多)映射表。 4 | -------------------------------------------------------------------------------- /src/collections/ordered_map/README.md: -------------------------------------------------------------------------------- 1 | # 有序映射表 Ordered Map 2 | 3 | 有特定排序方式的映射表。 4 | -------------------------------------------------------------------------------- /src/collections/queue/README.md: -------------------------------------------------------------------------------- 1 | # 佇列 Queue 2 | 3 | ![Queue - Wiki](https://upload.wikimedia.org/wikipedia/commons/thumb/5/52/Data_Queue.svg/450px-Data_Queue.svg.png) 4 | 5 | 佇列是一個具有*先進先出* FIFO 特性的資料結構。從 Wikipedia 上的圖為例,一個資料從左邊進入佇列並從右邊離開,最先進入佇列的資料會最先被取出。 6 | 7 | 佇列常見實作方式有:陣列 array、鏈結串列 linked list。為了使概念容易理解,我們選擇以類似陣列的 Vector 實作。 8 | 9 | > 本次實作的程式碼置於 [`rust_algorithm_club::collections::Queue`][doc] API 文件中。 10 | 11 | [doc]: /doc/rust_algorithm_club/collections/struct.Queue.html 12 | 13 | ## 架構設計 14 | 15 | ```rust 16 | {{#include mod.rs:struct}} 17 | ``` 18 | 19 | 以 `items` 保存加入佇列的資料。大部分用陣列實作的佇列可能會有 `front` 和 `rear` 兩個欄位負責保存指向佇列開頭和尾端的索引,作為佇列新增刪除資料的依據,但是透過 Rust 的 [`std::vec::Vec`][](線形動態成長的陣列容器),我們可以直接取得佇列第一以及最後一筆資料,所以這邊實作忽略這兩個欄位。 20 | 21 | [`std::vec::Vec`]: https://doc.rust-lang.org/std/vec/struct.Vec.html 22 | 23 | ## 基本操作 24 | 25 | - `enqueue`:將新資料加入佇列 26 | - `dequeue`:將最先放入的資料移出佇列 27 | - `peek`:在不將資料移出佇列的情況下取得最先放入的資料 28 | - `size`:取得佇列大小 29 | 30 | ### 定義佇列 31 | 32 | ```rust 33 | {{#include mod.rs:new}} 34 | ``` 35 | 36 | 初始化具有 `Vec` 的佇列。 37 | 38 | ### 將新資料加入佇列 39 | 40 | ```rust 41 | {{#include mod.rs:enqueue}} 42 | ``` 43 | 44 | 由於 `enqueue` 會改變 `items`,因此需要佇列的 mutable reference。再來,我們沒有限制佇列大小,全由 Rust 的 `Vec` 自行分配空間,將新資料放到 `items` 的最後端。 45 | 46 | ### 將最先放入的資料移出佇列 47 | 48 | ```rust 49 | {{#include mod.rs:dequeue}} 50 | ``` 51 | 52 | `items` 有可能是空的,在移出資料之前需要檢查,然後移出 index 為零的資料,也就是最先放入的資料。 53 | 54 | ### 取得佇列大小 55 | 56 | ```rust 57 | {{#include mod.rs:size}} 58 | ``` 59 | 60 | 取得 `items` 的大小。 61 | 62 | ### 不改變佇列的情況下,取得最先放入的資料 63 | 64 | ```rust 65 | {{#include mod.rs:peek}} 66 | ``` 67 | 68 | 這裡的作法很單純,就是呼叫 `Vec` 底層 [`slice::first`][],回傳一個 `Option`,不會影響到底層的 69 | `Vec` 的內容。 70 | 71 | [`slice::first`]: http://doc.rust-lang.org/std/primitive.slice.html#method.first 72 | 73 | ## 效能 74 | 75 | | Operation | Best Complexity | Worst Complexity | 76 | | --- | --- | --- | 77 | | enqueue (insert) | O(1) | O(1) | 78 | | dequeue (delete) | O(n)\* | O(n)* | 79 | 80 | \*:注意,一般來說 `dequeue` 會選用 `O(1)` 的實作方式,這裡直接呼叫 [`Vec::remove`][] 會導致整個 `Vec` 的元素向前位移一個,是較耗費計算資源的 `O(n)` 操作。 81 | 82 | 我們可以選用其他方式實作,例如用額外指標紀錄當前 head 所在位置的[雙端佇列 Deque][],或是使用[單向鏈結串列 Singly linked list][] 實作,都能達到 `O(1)` 的時間複雜度。 83 | 84 | [`Vec::remove`]: http://doc.rust-lang.org/std/vec/struct.Vec.html#method.remove 85 | [雙端佇列 Deque]: ../deque/ 86 | [單向鏈結串列 Singly linked list]: ../singly_linked_list/ 87 | 88 | ## 參考資料 89 | 90 | - [Queue (abstract data type)]() 91 | -------------------------------------------------------------------------------- /src/collections/queue/mod.rs: -------------------------------------------------------------------------------- 1 | /// A queue-like data structure implement through [`std::vec::Vec`][]. 2 | /// 3 | /// This is a naive implementation whose insertion time complexity is `O(n)`, 4 | /// which can be improved trivially by using a `Deque` or 5 | /// [`SinglyLinkedList`](crate::collections::SinglyLinkedList). 6 | /// 7 | /// References: 8 | /// 9 | /// - [Queue (abstract data type)]() 10 | // ANCHOR: struct 11 | pub struct Queue { 12 | items: Vec, 13 | } 14 | // ANCHOR_END: struct 15 | 16 | impl Queue { 17 | /// Initialize a queue with empty vector 18 | // ANCHOR: new 19 | pub fn new() -> Self { 20 | Self { items: Vec::new() } 21 | } 22 | // ANCHOR_END: new 23 | 24 | /// Adds an element into queue. 25 | /// 26 | /// # Complexity 27 | /// 28 | /// Constant. 29 | // ANCHOR: enqueue 30 | pub fn enqueue(&mut self, item: T) { 31 | self.items.push(item); 32 | } 33 | // ANCHOR_END: enqueue 34 | 35 | /// Removes the oldest added element in queue. 36 | /// 37 | /// # Complexity 38 | /// 39 | /// Linear in the size of the container. 40 | // ANCHOR: dequeue 41 | pub fn dequeue(&mut self) -> Option { 42 | match self.items.is_empty() { 43 | false => Some(self.items.remove(0)), 44 | true => None, 45 | } 46 | } 47 | // ANCHOR_END: dequeue 48 | 49 | /// Retrieves the least recently added element without dequeuing. 50 | /// 51 | /// # Complexity 52 | /// 53 | /// Constant. 54 | // ANCHOR: peek 55 | pub fn peek(&self) -> Option<&T> { 56 | self.items.first() 57 | } 58 | // ANCHOR_END: peek 59 | 60 | /// Retrieves the size of the queue. 61 | /// 62 | /// # Complexity 63 | /// 64 | /// Constant. 65 | // ANCHOR: size 66 | pub fn size(&self) -> usize { 67 | self.items.len() 68 | } 69 | // ANCHOR_END: size 70 | } 71 | 72 | #[cfg(test)] 73 | mod impl_by_vec { 74 | use super::*; 75 | 76 | #[test] 77 | fn new() { 78 | let queue = Queue::<()>::new(); 79 | assert!(queue.items.is_empty()); 80 | } 81 | 82 | #[test] 83 | fn enqueue() { 84 | let mut queue = Queue::new(); 85 | queue.enqueue(32); 86 | assert_eq!(Some(&32), queue.peek()); 87 | assert_eq!(1, queue.size()); 88 | } 89 | #[test] 90 | fn dequeue() { 91 | let mut queue = Queue::new(); 92 | queue.enqueue(32); 93 | assert_eq!(Some(32), queue.dequeue()); 94 | assert_eq!(None, queue.dequeue()); 95 | } 96 | #[test] 97 | fn size() { 98 | let mut queue = Queue::new(); 99 | queue.enqueue(-20); 100 | assert_eq!(1, queue.size()); 101 | assert_eq!(Some(&-20), queue.peek()); 102 | } 103 | #[test] 104 | fn integration() { 105 | let mut queue = Queue::new(); 106 | queue.enqueue(1); 107 | queue.enqueue(2); 108 | queue.enqueue(3); 109 | assert_eq!(3, queue.size()); 110 | assert_eq!(Some(1), queue.dequeue()); 111 | assert_eq!(Some(&2), queue.peek()); 112 | assert_eq!(Some(2), queue.dequeue()); 113 | assert_eq!(Some(3), queue.dequeue()); 114 | assert_eq!(0, queue.size()); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/collections/singly_linked_list/node-box.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | %0 4 | 5 | 6 | 7 | table0 8 | 9 | Node 10 | 11 | T 12 | 13 | 14 | Box 15 | 16 | usize 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/collections/singly_linked_list/node-recursive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | %0 4 | 5 | 6 | 7 | table0 8 | 9 | Node 10 | 11 | T 12 | 13 | 14 | Node 15 | 16 | T 17 | 18 | 19 | Node 20 | 21 | T 22 | 23 | 24 | Node 25 | 26 | T 27 | 28 | 29 | Node 30 | 31 | T 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/collections/stack/README.md: -------------------------------------------------------------------------------- 1 | # 堆疊 Stack 2 | 3 | ![Stack - Wikipedia](https://upload.wikimedia.org/wikipedia/commons/b/b4/Lifo_stack.png) 4 | 5 | 堆疊是一個具有*後進先出* LIFO 特性的資料結構。以從 Wikipedia 借來的上圖為例,在第五張圖的狀況下,如果要取得 2,就必須先把 3、4、5 都退出堆疊。 6 | 7 | 堆疊的底部與頂部都是抽象的概念,頂部是資料被加入、移除、較為繁忙的那一端,底部即另一端。 8 | 9 | 堆疊的空間可能是有限的,亦即也有可能實現空間無限的堆疊。有鑑於有限空間的堆疊較為常見,我們選擇實作空間有限的堆疊。 10 | 11 | 堆疊 stack 有兩種實作方式:陣列 array 與鏈結串列 linked list,在此選擇以類似陣列的 Vector 實現。 12 | 13 | > 本次實作的程式碼置於 [`rust_algorithm_club::collections::Stack`][doc] API 文件中。 14 | 15 | [doc]: /doc/rust_algorithm_club/collections/struct.Stack.html 16 | 17 | ## 架構設計 18 | 19 | ```rust 20 | {{#include mod.rs:struct}} 21 | ``` 22 | 23 | `maxsize` 用於模擬堆疊空間有限的特性;`items` 負責保存加入堆疊的資料。 24 | 25 | 在此刻意將 `maxsize`、`items` 定義為 private member,避免外部直接存取。 26 | 27 | ## 基本操作 28 | 29 | * `with_capacity`:定義一個空間有限的堆疊。 30 | * `push`:將新資料加入資料結構。 31 | * `pop`:將最新加入的資料移出資料結構。 32 | * `size`:(選用)取得堆疊的大小。 33 | * `peek`:(選用)在不將資料退出堆疊的情況下偷看最後加入堆疊的資料。 34 | 35 | ### 定義一個空間有限的堆疊 36 | 37 | ```rust 38 | {{#include mod.rs:with_capacity}} 39 | ``` 40 | 41 | 初始化一個帶有預先分配空間 Vector 的堆疊。 42 | 43 | ⚠ 注意,即使預先分配了有限的空間,Rust 的 vector 在空間已滿的情況下會重新分配。假設一開始為 vector 分配了 10 單位的空間,在將第 11 筆資料插入 vector 前,vector 在記憶體的空間將被重新分配,以容納這第 11 筆資料。為了模擬堆疊空間有限的特性,我們會在 `push` 的操作動點手腳。 44 | 45 | ### 將新資料加入資料結構 46 | 47 | ```rust 48 | {{#include mod.rs:push}} 49 | ``` 50 | 51 | 由於 `push` 操作會改變 `items`,因此需要堆疊的 mutable reference。由於 Rust 的 vector 有重新分配的特性,在將資料正式加入堆疊之前,必須先檢查堆疊初始化時設定的空間是否已經被塞滿了。如果結果為是,則拒絕將資料加入堆疊。 52 | 53 | ### 將最新加入的資料移出資料結構 54 | 55 | ```rust 56 | {{#include mod.rs:pop}} 57 | ``` 58 | 59 | 堆疊有可能是空的,在此以 `Option` 表現這個情況。如果針對一個空堆疊進行 `pop` 操作,將會得到 `None`。 60 | 61 | ### 取得堆疊的大小 62 | 63 | ```rust 64 | {{#include mod.rs:size}} 65 | ``` 66 | 67 | 一個空堆疊的大小是 0,加入一筆資料後是 1⋯⋯以此類推。注意容量 capcity 與大小 size 是兩個不同的概念。容量是這個堆疊最多可以塞下多少資料,大小則是這個堆疊已經被塞入了多少資料。由於 `push` 的檢查機制,堆疊的大小永遠不會超過 `maxsize`。 68 | 69 | ### 在不將資料退出堆疊的情況下偷看最後加入堆疊的資料 70 | 71 | ```rust 72 | {{#include mod.rs:peek}} 73 | ``` 74 | 75 | 與 `pop` 操作類似,但不會對堆疊造成任何影響。如果偷看的是一個空堆疊,會得到 `None`。 76 | 77 | ## 效能 78 | 79 | | Operation | Best Complexity | Worst Complexity | 80 | | --- | --- | --- | 81 | | push (insert) | O(1) | O(1) | 82 | | pop (delete) | O(1) | O(1) | 83 | 84 | 無論堆疊大小如何變化,`push` 與 `pop` 的效能都不會被影響。 85 | 86 | ## 參考資料 87 | 88 | * [Stack (abstract data type) 89 | ](https://en.wikipedia.org/wiki/Stack_\(abstract_data_type\)) 90 | * [Big-O Algorithm Complexity Cheat Sheet](http://bigocheatsheet.com/) 91 | -------------------------------------------------------------------------------- /src/collections/stack/mod.rs: -------------------------------------------------------------------------------- 1 | /// A stack-like data structure implemented through a `Vec`. 2 | /// 3 | /// The name "stack" for this type of structure comes from the analogy to a set 4 | /// of physical items stacked on top of each other, which makes it easy to take 5 | /// an item off the top of the stack, while getting to an item deeper in the 6 | /// stack may require taking off multiple other items first. 7 | /// 8 | /// Considered as a linear data structure, or more abstractly a sequential 9 | /// collection, the push and pop operations occur only at one end of the 10 | /// structure, referred to as the top of the stack. 11 | /// 12 | /// References: 13 | /// 14 | /// * [Stack (abstract data type)](https://en.wikipedia.org/wiki/Stack_\(abstract_data_type\)) 15 | /// * [Big-O Algorithm Complexity Cheat Sheet](http://bigocheatsheet.com/) 16 | // ANCHOR: struct 17 | pub struct Stack { 18 | maxsize: usize, 19 | items: Vec, 20 | } 21 | // ANCHOR_END: struct 22 | 23 | impl Stack { 24 | /// Initialize a stack of certain capacity. 25 | /// 26 | /// # Parameters 27 | /// 28 | /// * `maxsize`: Capacity of the collection. It limits how many items can 29 | /// be stored. 30 | // ANCHOR: with_capacity 31 | pub fn with_capacity(maxsize: usize) -> Self { 32 | Self { 33 | maxsize, 34 | items: Vec::with_capacity(maxsize), 35 | } 36 | } 37 | // ANCHOR_END: with_capacity 38 | 39 | /// Removes the most recently added element that was not yet removed. 40 | /// 41 | /// # Returns 42 | /// 43 | /// Returns the most recently added item. If nothing was added, `None` will be returned. 44 | /// 45 | /// # Complexity 46 | /// 47 | /// Constant. 48 | // ANCHOR: pop 49 | pub fn pop(&mut self) -> Option { 50 | self.items.pop() 51 | } 52 | // ANCHOR_END: pop 53 | 54 | /// Adds an element to the collection. 55 | /// 56 | /// # Returns 57 | /// 58 | /// Returns `true` if the collection has space left and item is 59 | /// successfully added, otherwise returns `false`. 60 | /// 61 | /// # Complexity 62 | /// 63 | /// Constant. 64 | // ANCHOR: push 65 | pub fn push(&mut self, item: T) -> bool { 66 | if self.items.len() == self.maxsize { 67 | return false; 68 | } 69 | self.items.push(item); 70 | return true; 71 | } 72 | // ANCHOR_END: push 73 | 74 | /// # Returns 75 | /// 76 | /// Returns the size of collection, indicates how many items are added in 77 | /// the collection. 78 | /// 79 | /// # Note 80 | /// 81 | /// Size and capacity are different concepts. 82 | /// Capacity limits how many items can be stored, while size indicates how 83 | /// many items is currently stored. 84 | // ANCHOR: size 85 | pub fn size(&self) -> usize { 86 | self.items.len() 87 | } 88 | // ANCHOR_END: size 89 | 90 | /// Peeks the last element added without tampering the collection. 91 | /// 92 | /// # Returns 93 | /// 94 | /// Returns the most recently added item. If nothing was added, `None` will 95 | /// be returned. 96 | // ANCHOR: peek 97 | pub fn peek(&self) -> Option<&T> { 98 | self.items.last() 99 | } 100 | // ANCHOR_END: peek 101 | } 102 | 103 | #[cfg(test)] 104 | mod impl_by_vec { 105 | use super::*; 106 | 107 | #[test] 108 | fn new_with_capacity() { 109 | let stack: Stack = Stack::with_capacity(10); 110 | assert_eq!(10, stack.items.capacity()); 111 | } 112 | 113 | #[test] 114 | fn pop() { 115 | let mut stack = Stack::with_capacity(1); 116 | stack.push(1); 117 | assert_eq!(Some(1), stack.pop()); 118 | assert_eq!(None, stack.pop()); 119 | } 120 | 121 | #[test] 122 | fn push() { 123 | let mut stack = Stack::with_capacity(1); 124 | stack.push(32); 125 | assert_eq!(Some(&32), stack.peek()); 126 | assert_eq!(1, stack.size()); 127 | } 128 | 129 | #[test] 130 | fn push_maxsize() { 131 | let mut stack = Stack::with_capacity(1); 132 | assert_eq!(true, stack.push(1)); 133 | assert_eq!(Some(&1), stack.peek()); 134 | assert_eq!(false, stack.push(2)); 135 | } 136 | 137 | #[test] 138 | fn size() { 139 | let mut stack = Stack::with_capacity(1); 140 | assert_eq!(0, stack.size()); 141 | stack.push(1); 142 | assert_eq!(1, stack.size()); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/concepts/asymptotic-notation/README.md: -------------------------------------------------------------------------------- 1 | # 漸進符號 Asymptotic Notation 2 | 3 | 日常生活中,你會如何描述處理事情的效率? 4 | 5 | 「原來她五分鐘內可以吃掉一頭牛!」 6 | 7 | 「房間這麼小你還能擺一堆雜物?還不快收拾!」 8 | 9 | 這些描述方法,著重在處理事情的花費時間,或單位空間內的儲存量。描述演算法的效率也如此,就是「測量演算法的執行成本」,例如這個排序法花了 10 秒鐘跑完兩萬筆資料,或是這個模擬演算法很吃資源需要 32 GB 的記憶體。 10 | 11 | 然而,在不同的機器規格、環境溫濕度、程式語言、實作方式,以及有沒有放乖乖的變異影響下,相同演算法的執行成本常常不一致。為了消弭這些外部因素,讓分析演算法能夠更科學化。科學家抽絲剝繭,發明一個方法: 12 | 13 | **「統計演算法內所需操作步驟的數目。」** 14 | 15 | 這是最簡單,最粗淺比較不同演算法效率的作法。 16 | 17 | ## 用數學表示演算法效率 18 | 19 | 「計算步驟數目」很像中小學的數學題目:某公司有三個能力相異的工程師,有的工程師一天解決一個 bug,有的工程師連續工作後效率大幅滑落。每個工程師的除蟲效率可以畫成「bug 數 - 解決 bug 所需時數」函數,橫軸為待處理的臭蟲數,縱軸為解決臭蟲所需時數,如圖一與表所示。 20 | 21 | | 時數 | $\log N$ | $N$ | $N \log N$ | 22 | | ---------- | ------------ | ------- | -------------- | 23 | | $N=5$ | 2.236 | 5 | 8.046 | 24 | | $N=30$ | 5.477 | 30 | 102.036 | 25 | 26 | ![Fig. 1](fig1.png) 27 | 28 | 不論從圖或表,我們都可以明確看出,當 bug 數目小時,每個工程師耗時差不多;當 bug 數目成長到一定程度時,效率好與效率差的工程師差距就很明顯了。 29 | 30 | 我們把場景拉回演算法的範疇,再闡明一次。上述的除蟲效率函數關係,可以簡單視為為「輸入資料量 - 運算成本」關係之函數。例如 $f(x)=x^2+3x+6$。當輸入資料量增大時,成本也隨之上升,這個用來描述演算法執行成本與輸入資料量之關係的函數,我們稱之為該演算法的「複雜度」。 31 | 32 | ## 何謂漸進符號 33 | 34 | 了解每個演算法的時間複雜度之後,就能比較何者效率佳。但往往天不從人願,給了我們兩個演算法進行比較。 35 | 36 | $$f(x)=\sqrt{\frac{182777}{286}}\pi x^4+5\log_{3}^{26}88x^3-e^{777^{log_2^9}}$$ 37 | 38 | $$g(x)=3x^6-2x^2$$ 39 | 40 | 「天啊!這樣要怎麼分析執行效率呀!」 41 | 42 | 為了有統一的加薪標準,我們不能假定產品只會產生特定數量的臭蟲,也不能以單一天的工作表現判定員工能力,我們知道老舊系統有無限多個 bug,因此,優秀的老闆關心的是工程師長期處理「海量臭蟲」,在極限下的**成長趨勢**,這些成長趨勢才是衡量 KPI 的關鍵。再次強調,優秀老闆關心如何榨出是工程師的「極限成長趨勢」,而非一時半刻賣弄學識。 43 | 44 | 同樣地,有太多因素干擾影響一個演算法的複雜度,假使我們只觀察當輸入資料量 $n$ 接近無窮大時,演算法的成長趨勢為何,就很接近所謂漸進符號(asymptotic notation)的定義。漸進符號 只關心演算法在極限下的漸進行為,不同的演算法可能使用相同的漸進符號表示。 45 | 46 | 我們比較兩個簡單函數,$f(x) = 10x + 29$ 以及 $g(x) = x^2 + 1$。從圖二可以看出一開始 $g(x)$ 的執行時間比 $f(x)$ 多了不少,但隨著輸入資料量 $n$ 增多,$g(x)$ 的執行時間成長愈來愈快速,最後遠遠大於 $f(x)$。 47 | 48 | ![Fig. 2](fig2.png) 49 | 50 | 若以 $an^2 + bn + c$ 表示複雜度,就是當存在一個 $a > 0$ 時,一定會有 $n$ 符合 $an^2 > bn + c$,這個差距隨著 $n$ 越大越明顯,這是因為首項(leading term),也就是帶有最高指數的那一項,隨著 輸入大小改變,執行時間變化幅度較大。因此,可捨去複雜度函數中其他較不重要的次項與常數,留下最大次項,「**透過簡單的函數來表述函數接近極限的行為**」,讓複雜度函數更易理解,這就是「漸進符號」的概念。 51 | 52 | 這裡介紹常見的幾種漸進符號: 53 | 54 | ### $O$:Big O 55 | 56 | 當我們談論演算法複雜度時,通常關心的是演算法「最糟糕的情況下」,「最多」需要執行多久。Big O 就是描述演算法複雜度上界的漸進符號,當一個演算法「實際」的複雜度(或執行成本對輸入資料量函數)為 $f(n)$ 時,欲以 Big O 描述其複雜度上界時,必須滿足以下定義: 57 | 58 | $$f(n) = O(g(n)) \colon \{\exists k>0\ \exists n_0\ \forall n>n_0\ |f(n)| \leq k \cdot g(n)\}$$ 59 | 60 | 假設有一演算法實際複雜度為 $f(n) = 3n + 4$,有一組 $k = 4\;\ g(n) = n\;\ n_0 = 4$ 滿足 61 | 62 | $$\forall n > 4,\ 0 \leq f(n) = 3n + 4 \leq 4n$$ 63 | 64 | 意思是「$f(n)$ 的複雜度上界成長趨勢最終不會超過 $g(n) = 4n$ 」,再代入 $O(g(n))$,可得演算法最差複雜度為 $f(n) = O(n)$,也就是「該演算法的成長趨勢不會比 $g(n)$ 來得快」(見圖三)。 65 | 66 | ![Fig. 3](fig3.png) 67 | 68 | 再多看一個例子,若 $f(n) = 4n^2 + n$ 有一組 $k = 5\;\ g(n) = n^2\;\ n_0 = 5$ 滿足 69 | 70 | $$\forall n > 5,\ 0 \leq f(n) = 4n^2 + n \leq 5n^2$$ 71 | 72 | 則此函數的複雜度為 $f(n) = O(n^2)$。 73 | 74 | > 注意:也寫作 $f(n) \in O(g(n))$,因為實際上 $O(g(n))$ 是所有可描述演算法成長趨勢,並滿足上述條件的函數之「集合」。 75 | 76 | ### $\Omega$:Big Omega 77 | 78 | 相較於 Big O 描述演算法成長趨勢的上界,Big Omega 則是對應成長趨勢的「下界」,定義如下: 79 | 80 | $$f(n) = \Omega(g(n)) \colon \{\exists k>0\ \exists n_0\ \forall n>n_0\ |f(n)| \geq k \cdot g(n)\}$$ 81 | 82 | 以 $f(n) = 3n + 4$ 為例,有一組 $k = 2\;\ g(n) = n\;\ n_0 = 0$ 滿足上式,因此這個演算法在輸入資料夠大時,「至少」會達到 $\Omega(n)$ 的複雜度,也就是「該演算法的成長趨勢不會比 $g(n)$ 來得慢」。 83 | 84 | ### $\Theta$:Big Theta 85 | 86 | Big Theta 則是 Big O 與 Big Omega 兩個漸進上下界所夾出的範圍,表示該演算法在輸入資料夠大時,最終的複雜度會成長到這個範圍中。其定義如下: 87 | 88 | $$f(n) = \Theta(g(n)) \colon \{\exists k_1>0\ \exists k_2>0\ \exists n_0\ \forall n>n_0\ k_1 \cdot g(n) \leq |f(n)| \leq k_2 \cdot g(n)\}$$ 89 | 90 | 繼續以 $f(n) = 3n + 4$ 為例,同樣有一組 $k_1 = 1\;\ k_2 = 5\;\ g(n) = n\;\ n_0 = 2$,滿足 91 | 92 | $$\forall n \geq 2,\ n \leq f(n) = 3n + 4 \leq 5n$$ 93 | 94 | 可得知,$f(n) = 3n + 4 \in \Theta(n)$,表示「該演算法的成長趨勢與 $g(n) = n$ 相同」(見圖四)。 95 | 96 | ![Fig. 4](fig4.png) 97 | 98 | ## 常見的複雜度 99 | 100 | 看完了讓人昏昏欲睡的數學定義,現在來認識一些常見的複雜度,從最快最有效率,到最慢最拖台錢的通通一起認識。 101 | 102 | - $O(1)$:常數時間,演算法執行時間與資料量毫無瓜葛。例如讀取 array 首個元素。 103 | - $O(\log n)$:執行時間隨資料量呈對數比例成長。常見的例子是[二元搜索(Binary search)](../../searching/binary_search)。 104 | - $O(n)$:執行時間隨資料量呈線性成長,例如在無序的 array 中尋找特定值。 105 | - $O(n \log n)$:執行時間隨資料量呈線性對數成長,常見的[合併排序(Mergesort)](../../sorting/mergesort)的複雜度即如斯。 106 | - $O(n^2)$:執行時間隨資料量呈平方成長,例如一些效率不彰的排序法如[氣泡排序(Bubble sort)](../../sorting/bubble_sort)。 107 | - $O(n^3)$:執行時間隨資料量呈立方成長,常見例子為 naïve 實作的矩陣乘法。 108 | - $O(c^n)$:執行時間隨資料量呈指數成長。 109 | - $O(n!)$:執行時間隨資料量呈階乘成長,大部分情況下,這是非常差勁的複雜度。 110 | 111 | 若想一窺各種常見演算法的複雜度,可以參考這個最全面的 [Big-O Cheat Sheet](http://bigocheatsheet.com/),圖表非常精美直觀! 112 | 113 | > 再次強調,漸進符號也可以代表其他執行成本如記憶體空間,並不一定代表執行時間。 114 | 115 | 116 | 117 | > 其他的漸進符號還有 little-o、little-omega 等等,有興趣的朋友可以參考文末的資料。 118 | 119 | ## 你可能不適合漸進符號 120 | 121 | 善用漸進符號,可以讓原本複雜艱澀的實際複雜度,簡化至人類容易理解的簡單數學符號,也讓分析演算法效率更為客觀。但實際上,漸進符號省略了常數項與低次項,僅保留最高次項,這種「漸進行為下」的效能表現,在真實世界中,若輸入資料量不夠大,實際複雜度的低次項係數又比高次項大上許多,很可能這個演算法實際上根本沒辦法使用。 122 | 123 | 另外,漸進符號僅考慮最差與最佳複雜度,沒有考慮到平均複雜度。舉例來說,[Quicksort](../../sorting/quicksort) 最差複雜度為 $O(n^2)$,乍看之下不是很理想,但這種情況非常稀少;其平均複雜度落在 $O(n \log n)$,且其係數相對較低,額外開銷少,自然成為最熱門的排序法之一。 124 | 125 | 還有,漸進符號也沒有考慮到不同語言、平台的基礎操作開銷,例如實作排序法時,有些語言「比較」兩個元素的開銷比「置換」來得大,實作上就需要盡量減少置換元素。同樣的,CPU 快取也非常容易忽略,一些快速的搜尋法很可能因為不是[線性搜尋](../../searching/linear_search),沒辦法充分利用 CPU cache,效能不一定理想。 126 | 127 | 總之,漸進符號只能告訴你「當輸入資料量夠大時,演算法的複雜度表現如何」,並不總是適用每個情境,端看你怎麼使用他。 128 | 129 | ## 參考資料 130 | 131 | - [Wiki: Time complexity](https://en.wikipedia.org/wiki/Time_complexity) 132 | - [Wiki: Big O notation](https://en.wikipedia.org/wiki/Big_O_notation) 133 | - [Brilliant: Big O Notation](https://brilliant.org/wiki/big-o-notation/) 134 | - [Infinite Loop: Complexity Analysis](http://program-lover.blogspot.com/2008/10/complexity-analysis.html) 135 | -------------------------------------------------------------------------------- /src/concepts/asymptotic-notation/fig1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/rust-algorithm-club/1954301f21d44c18ea44864a776a3f6fd72eaefe/src/concepts/asymptotic-notation/fig1.png -------------------------------------------------------------------------------- /src/concepts/asymptotic-notation/fig2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/rust-algorithm-club/1954301f21d44c18ea44864a776a3f6fd72eaefe/src/concepts/asymptotic-notation/fig2.png -------------------------------------------------------------------------------- /src/concepts/asymptotic-notation/fig3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/rust-algorithm-club/1954301f21d44c18ea44864a776a3f6fd72eaefe/src/concepts/asymptotic-notation/fig3.png -------------------------------------------------------------------------------- /src/concepts/asymptotic-notation/fig4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/rust-algorithm-club/1954301f21d44c18ea44864a776a3f6fd72eaefe/src/concepts/asymptotic-notation/fig4.png -------------------------------------------------------------------------------- /src/concepts/asymptotic-notation/plot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | 8 | OUT_DIR = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | def plot_asymptotic_notation(): 11 | """Generate figures for 'Asymptotic Notation' chapter.""" 12 | plt.rc('lines', linewidth=3) 13 | plt.rc('font', size=14) 14 | 15 | # Fig. 1 16 | x = np.linspace(1, 10) 17 | plt.plot(x, np.log(x), label='Logarithmic O(log(n))', linewidth=3) 18 | plt.plot(x, x, label='Linear O(n)', linewidth=3) 19 | plt.plot(x, x * np.log(x), label='Linearithmic O(n log(n))', linewidth=3) 20 | plt.xlabel('Size of Input') 21 | plt.ylabel('Cost') 22 | plt.legend() 23 | plt.savefig(os.path.join(OUT_DIR, 'fig1.png')) 24 | plt.clf() 25 | 26 | # Fig. 2 27 | x = np.linspace(1, 30) 28 | plt.plot(x, 10 * x + 29, label='f(x) = 10x + 29') 29 | plt.plot(x, x**2 + 1, label='g(x) = x^2 + 1') 30 | plt.xlabel('Size of Input') 31 | plt.ylabel('Cost') 32 | plt.legend() 33 | plt.savefig(os.path.join(OUT_DIR, 'fig2.png')) 34 | plt.clf() 35 | 36 | # Fig. 3 37 | x = np.linspace(1, 10) 38 | plt.axvline(x=4, linestyle='dashed', color='lightgray', linewidth=2) 39 | plt.plot(x, 3 * x + 4, label='f(n) = 3n + 4') 40 | plt.plot(x, 4 * x, label='g(n) = 4n') 41 | plt.xlabel('Size of Input') 42 | plt.ylabel('Cost') 43 | plt.legend() 44 | plt.savefig(os.path.join(OUT_DIR, 'fig3.png')) 45 | plt.clf() 46 | 47 | # Fig. 4 48 | x = np.linspace(1, 10) 49 | plt.axvline(x=2, linestyle='dashed', color='lightgray', linewidth=2) 50 | plt.plot(x, 3 * x + 4, label='f(n) = 3n + 4') 51 | plt.plot(x, x, label='k1 * g(n) = n') 52 | plt.plot(x, 5 * x, label='k2 * g(n) = 5n') 53 | plt.xlabel('Size of Input') 54 | plt.ylabel('Cost') 55 | plt.legend() 56 | plt.savefig(os.path.join(OUT_DIR, 'fig4.png')) 57 | plt.clf() 58 | 59 | # Close current window 60 | plt.close() 61 | 62 | plot_asymptotic_notation() 63 | -------------------------------------------------------------------------------- /src/hamming_distance/README.md: -------------------------------------------------------------------------------- 1 | # 漢明距離 Hamming distance 2 | 3 | 漢明距離(Hamming distance)是指兩個相同長度的序列(sequence)在相同位置上,有多少個數值不同,對二進位序列來說就是「相異位元的數目」。漢明距離同時也是一種編輯距離,即是將一個字串轉換成另一個字串,需要經過多少次置換操作(substitute)。 4 | 5 | 漢明距離多應用於編碼理論中的錯誤更正(error-correcting),漢明碼(Hammming code)中計算距離的演算法即為漢明距離。 6 | 7 | > 本次實作的程式碼置於 8 | > 9 | > - [`rust_algorithm_club::hamming_distance`][] 10 | > - [`rust_algorithm_club::hamming_distance_str`][] 11 | > 12 | > API 文件中。 13 | 14 | [`rust_algorithm_club::hamming_distance`]: /doc/rust_algorithm_club/fn.hamming_distance.html 15 | [`rust_algorithm_club::hamming_distance_str`]: /doc/rust_algorithm_club/fn.hamming_distance_str.html 16 | 17 | ## 位元版實作 18 | 19 | 計算相異位元的數目其實就是一堆位元運算,如下: 20 | 21 | ```rust 22 | {{#include mod.rs:bit}} 23 | ``` 24 | 25 | 1. 透過 XOR 操作,讓兩序列相異位元為 1,相同位元為 0。 26 | 2. 如果 XOR 操作不為零,表示還有相異位元,繼續計算。 27 | 3. 將 XOR 結果和 1 做 AND 運算,看最低有效位(least significant digit)是否為 1。 28 | 4. 將 XOR 做位元右移,捨棄最低有效位,並回到步驟二。 29 | 30 | > 根據 [《The Rust Reference》][] 指出,Rust 的位元右移在 31 | > 32 | > - 無符號整數(unsigned)是邏輯右移(logical right shift),也就是直接在最高有效位補 0; 33 | > - 有符號整數(signed)則是算術右移(arithmetic right shift),右移之後符號會被保留。 34 | 35 | [《The Rust Reference》]: https://doc.rust-lang.org/reference/expressions/operator-expr.html#arithmetic-and-logical-binary-operators 36 | 37 | 實際上,Rust 提供了一個原生的計算整數有多少個零的方法 [`{integer_type}::count_ones`][],可以省去自己做位元運算的麻煩,實作如下,帥: 38 | 39 | ```rust 40 | pub fn hamming_distance(source: u64, target: u64) -> u32 { 41 | (source ^ target).count_ones() 42 | } 43 | ``` 44 | 45 | [`{integer_type}::count_ones`]: https://doc.rust-lang.org/stable/std/?search=count_ones 46 | 47 | ## 字串版實作 48 | 49 | 字串版的漢明距離就相對好懂了。 50 | 51 | ```rust 52 | {{#include mod.rs:str}} 53 | ``` 54 | 55 | 字串版同樣吃 `source` 和 `target` 兩個輸入。 56 | 57 | 1. 用 [`str::chars`][] 讓漢明距離可以比對 Unicode 字串,而非只有 ASCII,而 `str::chars` 是 `Iterator`,所以透過 `Iterator::next` 逐一比較每個字元。 58 | 2. 這裡 `if c1 != c2` 叫做 [match guard][],是在模式匹配之外,額外條件式檢查,因此,只有 `source` 和 `target` 都有下一個字元而且兩字元不相等才會進入這個匹配分支。 59 | 3. 若有任何一個是 `None`,另外一個是 `Some`,標示輸入字串的長度不同,直接噴錯。 60 | 4. 如果都沒有下一個字元,直接結束迴圈。 61 | 5. 其他情況,例如兩個字元相同,就繼續疊代。 62 | 63 | [`str::chars`]: http://doc.rust-lang.org/std/primitive.str.html#method.chars 64 | [match guard]: https://doc.rust-lang.org/reference/expressions/match-expr.html#match-guards 65 | 66 | ## 效能 67 | 68 | 長度為 n 的序列,計算漢明距離的時間複雜度為 $O(n)$,空間複雜度為 $O(1)$。 69 | 70 | ## 參考資料 71 | 72 | - [Wiki: Hamming distance](https://en.wikipedia.org/wiki/Hamming_distance) 73 | - [錯誤更正碼簡介](https://web.math.sinica.edu.tw/math_media/d184/18404.pdf) 74 | 75 | -------------------------------------------------------------------------------- /src/hamming_distance/mod.rs: -------------------------------------------------------------------------------- 1 | /// Calculate Hamming distance to two unsigned intergers. 2 | // ANCHOR: bit 3 | pub fn hamming_distance(source: u64, target: u64) -> u32 { 4 | let mut count = 0; 5 | let mut xor = source ^ target; // 1 6 | 7 | // 2 8 | while xor != 0 { 9 | count += xor & 1; // 3 10 | xor >>= 1; // 4 11 | } 12 | 13 | count as u32 14 | } 15 | // ANCHOR_END: bit 16 | 17 | /// Calculate Hamming distance of two UTF-8 encoded strings. 18 | // ANCHOR: str 19 | pub fn hamming_distance_str(source: &str, target: &str) -> usize { 20 | let mut count = 0; 21 | // 1 22 | let mut source = source.chars(); 23 | let mut target = target.chars(); 24 | 25 | loop { 26 | // 2 27 | match (source.next(), target.next()) { 28 | // 3 29 | (Some(c1), Some(c2)) if c1 != c2 => count += 1, 30 | // 4 31 | (None, Some(_)) | (Some(_), None) => panic!("Must have the same length"), 32 | // 5 33 | (None, None) => break, 34 | // 6 35 | _ => continue, 36 | } 37 | } 38 | 39 | count 40 | } 41 | // ANCHOR_END: str 42 | 43 | #[cfg(test)] 44 | mod base { 45 | use super::*; 46 | 47 | #[test] 48 | fn bit() { 49 | let cases = [ 50 | (0, 0b0000_0000, 0b0000_0000), 51 | (0, 0b1111_1111, 0b1111_1111), 52 | (1, 0b0000_0001, 0b0000_0000), 53 | (2, 0b0000_0000, 0b0001_1000), 54 | (4, 0b1100_0011, 0b0110_0110), 55 | (8, 0b0101_0101, 0b1010_1010), 56 | ]; 57 | for &(dist, c1, c2) in &cases { 58 | assert_eq!(hamming_distance(c1, c2), dist); 59 | } 60 | } 61 | 62 | #[test] 63 | fn str() { 64 | let cases = [ 65 | (0, "", ""), 66 | (0, "rust", "rust"), 67 | (1, "cat", "bat"), 68 | (3, "abc", "xyz"), 69 | ]; 70 | for &(dist, c1, c2) in &cases { 71 | assert_eq!(hamming_distance_str(c1, c2), dist); 72 | } 73 | } 74 | 75 | #[test] 76 | #[should_panic(expected = "Must have the same length")] 77 | fn str_panic() { 78 | hamming_distance_str("abc", "z"); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/levenshtein_distance/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/rust-algorithm-club/1954301f21d44c18ea44864a776a3f6fd72eaefe/src/levenshtein_distance/1.png -------------------------------------------------------------------------------- /src/levenshtein_distance/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/rust-algorithm-club/1954301f21d44c18ea44864a776a3f6fd72eaefe/src/levenshtein_distance/2.png -------------------------------------------------------------------------------- /src/levenshtein_distance/README.md: -------------------------------------------------------------------------------- 1 | # 萊文斯坦距離 Levenshtein distance 2 | 3 | 萊文斯坦距離(Levenshtein distance)是一種量化兩字串間差異的演算法,代表從一個字串轉換為另一個字串最少需要多少次編輯操作,這種量化指標的演算法稱為[「編輯距離(Edit distance)」][wiki-edit-distance],不同的演算法允許的編輯操作不盡相同,萊文斯坦距離允許使用: 4 | 5 | - 插入(insertion) 6 | - 刪除(deletion) 7 | - 置換(substitution) 8 | 9 | 三種編輯操作,通常一般實作上三種操作的權重會相同。 10 | 11 | 萊文斯坦距離概念易理解,實作簡單,常用在簡易拼字修正與建議,為最具代表性的編輯距離演算法。 12 | 13 | 14 | > 本次實作的程式碼置於 15 | > 16 | > - [`rust_algorithm_club::levenshtein_distance`][doc-lev-dist] 17 | > - [`rust_algorithm_club::levenshtein_distance_naive`][doc-lev-dist-naive] 18 | > 19 | > API 文件中。 20 | 21 | [doc-lev-dist]: /doc/rust_algorithm_club/fn.levenshtein_distance.html 22 | [doc-lev-dist-naive]: /doc/rust_algorithm_club/fn.levenshtein_distance_naive.html 23 | 24 | ## 概念 25 | 26 | ### 萊文斯坦的遞迴函數 27 | 28 | 編輯距離愛拿 kitten 和 sitting 當例子,這兩個單字的編輯距離為 3,可以透過以下步驟,算出 kitten 與 sitting 轉換會經歷三個編輯操作: 29 | 30 | 1. **s**itting -> **k**itting:置換 /k 31 | 2. kitt**i**ng -> kitt**e**ng:置換 i/e 32 | 3. kitten**g** -> kitten ~~**g**~~:刪除最末端 g 33 | 34 | 等等,怎麼知道 3 就是最少編輯操作呢?我們可以經由下列函數計算萊文斯坦距離: 35 | 36 | $$ 37 | \operatorname{lev} _{a,b}(i,j) = 38 | \begin{cases} 39 | \max(i,j) & \text{if} \min(i,j) = 0, \\\\ 40 | \min{ 41 | \begin{cases} 42 | \operatorname{lev} _{a,b}(i-1,j) + 1, \\\\ 43 | \operatorname{lev} _{a,b}(i,j-1) + 1, \\\\ 44 | \operatorname{lev} _{a,b}(i-1,j-1) + 1 _{(a _{i} \neq b _{j})} 45 | \end{cases} 46 | } & \text{otherwise.} 47 | \end{cases} 48 | $$ 49 | 50 | > $\operatorname{lev}_{a,b}(i,j)$ 代表字串 $a$ 前 $i$ 個字元,與 $b$ 前 $j$ 個字元的萊文斯坦距離。($i$、$j$ 索引從 1 開始計算) 51 | 52 | 別怕,這個函數是一個分段函數(piecewise function),由兩個子函數組成,接下來將一一拆解。 53 | 54 | 首先來看第一個子函數: 55 | 56 | $$ 57 | \max(i,j) \hspace{10 pt} \text{if} \min(i,j) = 0 58 | $$ 59 | 60 | 根據函數定義,$i$ $j$ 可以視為 $a$ $b$ 前幾個字元的子字串(substring),所以這個子函數白話翻譯是「若有子字串是空字串,則以較長的子字串長度作為編輯距離」。這其實非常直觀,如果有一空字串和另一個非空字串 `abc`,那編輯距離一定是插入三次或刪除三次,也就是該字串長度。這帶出萊文斯坦距離一個很重要的上界:「兩字串的編輯距離至多為較長字串的長度」。 61 | 62 | 第二個子函數稍微複雜,要再從三個函數中取最小值,但剛剛好,這三個函數分別代表了萊文斯坦距離接受的插入、刪除、置換三種操作: 63 | 64 | - $\operatorname{lev} _{a,b}(i-1,j) + 1$:從 a 轉變到 b 要刪除 1 字元。 65 | - $\operatorname{lev} _{a,b}(i,j-1) + 1$:從 a 轉換到 b 要插入 1 字元。 66 | - $\operatorname{lev} _{a,b}(i-1,j-1) + 1 _{(a _{i} \neq b _{j})}$:若 a 等於 b,就不需任何操作,直接一起跳過;反之則置換一次。 67 | 68 | 這是子函數是遞迴函數,每次都會分出三個子程序計算萊文斯坦距離,空間複雜度直逼 $O(3^{m + n - 1})$,驚人! 69 | 70 | > 複雜度的 3 次方會減一是因為只要 m n 其中一個歸零,該路徑就不再遞迴。 71 | 72 | ### 動態規劃的距離矩陣 73 | 74 | 由於上述遞迴複雜度過搞,因此處理萊文斯坦距離,通常會選擇由下至上(bottom-up) 的動態規劃(Dynamic programming),利用一個距離矩陣,將兩個字串所有子字串間的萊文斯坦距離累積紀錄起來,省去重複計算的成本。 75 | 76 | 首先,先將最基礎的 a、b 其一為空字串時的編輯距離寫上去,因為其中一個為空字串,編輯距離必定是隨著另一字串增加,逐一插入字元(最左方的直行)或刪除字元(上方橫列)。 77 | 78 | > 註:若插入刪除權重相等,對萊文斯坦來說這兩種操作其實一樣,只是由 a 到 b 或 b 到 a 的差異。 79 | 80 | ![](base.png) 81 | 82 | _圖一:空字串與編輯距離_ 83 | 84 | 接下來我們要來算 k 與 s 子字串的編輯距離,按照公式來計算: 85 | 86 | 1. 紅字上方是執行刪除的累積編輯距離(1),加上刪除操作的成本(1),為 1 + 1 = 2 87 | 2. 紅字左方是執行插入的累積編輯距離(1),加上插入操作的成本(1),為 1 + 1 = 2 88 | 3. 紅字對角是執行置換的累積編輯距離(1),加上當前 k s 字元不相等的置換成本(1),為 0 + 1 = 1 89 | 4. 從刪除、插入、置換的成本中選一個最小值 MIN(2,2,1) 填入矩陣中,完成。 90 | 91 | ![](1.png) 92 | 93 | _圖二:子字串 k 與 s 的編輯距離_ 94 | 95 | 我們再來看一組範例,k 與 si 的距離為 2: 96 | 97 | 1. 紅字上方是執行刪除的累積編輯距離(2),加上刪除操作的成本(1),為 2 + 1 = 3 98 | 2. 紅字左方是執行插入的累積編輯距離(1),加上插入操作的成本(1),為 1 + 1 = 2 99 | 3. 紅字對角是執行置換的累積編輯距離(1),加上當前 k i 字元不相等的置換成本(1),為 1 + 1 = 2 100 | 4. 從刪除、插入、置換的成本中選一個最小值 MIN(3,2,2) 填入矩陣中,完成。 101 | 102 | ![](2.png) 103 | 104 | _圖三:子字串 k 與 si 的編輯距離_ 105 | 106 | 最後計算出來整個編輯距離矩陣會長得如下,取矩陣最後一行一列的元素就是累積計算出來的 kitten 與 sitting 的編輯距離。 107 | 108 | ![](matrix.png) 109 | 110 | _圖三:字串 kitten 與 sitting 的完整編輯距離矩陣_ 111 | 112 | 這就是透過動態規劃,將會重複計算的子字串編輯距離紀錄起來,降低原始算式的時空間複雜度,非常簡單暴力的演算法。 113 | 114 | ## 實作 115 | 116 | 萊文斯坦距離的函式簽名很簡單,就是吃兩個 string 回傳一個距離: 117 | 118 | ```rust 119 | pub fn levenshtein_distance(source: &str, target: &str) -> usize 120 | ``` 121 | 122 | 首先,先實作第一個子函數 123 | 124 | $$ 125 | \max(i,j) \hspace{10 pt} \text{if} \min(i,j) = 0 126 | $$ 127 | 128 | 當任一字串長度為 0 時(min(i,j)),編輯距離為另一字串之長度。 129 | 130 | ```rust 131 | pub fn levenshtein_distance(source: &str, target: &str) -> usize { 132 | if source.is_empty() { 133 | return target.len() 134 | } 135 | if target.is_empty() { 136 | return source.len() 137 | } 138 | /// ...snip 139 | } 140 | ``` 141 | 142 | ### 距離矩陣 143 | 144 | 接下來實作矩陣的部分,這是純天然沒有特別最佳化的版本,要做三件事: 145 | 146 | - 建立一個 a+1 x b+1 的空矩陣,加一是因為要算入 a b 為空字串的狀況。 147 | - 填入 a b 為空字串時第一列與第一行的編輯距離,也就是全插入和全刪除的狀況。 148 | - 按照前一節的概念計算整個距離矩陣。 149 | 150 | ```rust 151 | // ...snip 152 | {{#include mod.rs:naive_init}} 153 | ``` 154 | 155 | 1. 使用 [`str::chars`][] 計算字串長度,Rust 的 [`str::len`][] 回傳的是位元組(byte)的長度,沒有考慮人類閱讀 UTF-8 編碼字串的視覺字元長度。選用 Chars 可以處理絕大部分常見的 UTF-8 字串問題(支援 CJK)。 156 | 2. 使用 `vec!` 巨集建立一個 vector of vector,也就是我們的距離矩陣。 157 | 3. 填入第一行和第一列空字串時的狀況,也就是初始化成圖一的情形。 158 | 159 | [`str::chars`]: http://doc.rust-lang.org/std/primitive.str.html#method.chars 160 | [`str::len`]: http://doc.rust-lang.org/std/primitive.str.html#method.len 161 | 162 | 第二步則是撰寫計算距離矩陣編輯操作的邏輯: 163 | 164 | ```rust 165 | // ...snip 166 | {{#include mod.rs:naive_calc}} 167 | ``` 168 | 169 | 1. 計算插入操作成本,對應萊文斯坦函數中 $\operatorname{lev} _{a,b}(i,j-1) + 1$ 子函數。 170 | 2. 計算刪除操作成本,對應萊文斯坦函數中 $\operatorname{lev} _{a,b}(i-1,j) + 1$ 子函數。 171 | 3. 計算刪除操作成本,對應萊文斯坦函數中 $\operatorname{lev} _{a,b}(i-1,j-1) + 1 _{(a _{i} \neq b _{j})}$,這裡用了 [`as`][] 運算子,可以在原生型別(primitive type)間互相轉型。 172 | 4. 取最小值並紀錄在當前的位置,也就是圖二圖三的綠底紅字。 173 | 5. 算完整個距離矩陣後,最後一列最後一行元素就是 a b 兩字串的萊文斯坦距離,你可能好奇,直接呼叫 `unwrap` 沒有處理錯誤合理嗎?其實在「一定不可能出錯」的地方,呼叫 `unwrap` 完全可接受,省去不必要的空值處理。 174 | 175 | [`as`]: http://doc.rust-lang.org/std/keyword.as.html 176 | 177 | 完整程式碼如下: 178 | 179 | ```rust 180 | {{#include mod.rs:naive}} 181 | ``` 182 | 183 | ### 使用一維陣列減少記憶體配置 184 | 185 | Rust 的 `Vec` 並非簡單的 array,而是在堆積(heap)上配置記憶體,而每一個 `Vec` 都會帶一個指標和兩個值:指標指向在堆積上的資料,一個值儲存資料當前的長度,一個值儲存容量(圖解請參考 [cheats.rs][cheats-heap-storage])。所以 `Vec` 會佔三個 usize 的大小,在 64 位元作業系統中就是 24 個位元組。 186 | 187 | [cheats-heap-storage]: https://cheats.rs/#general-purpose-heap-storage 188 | 189 | 但實際上,我們的距離矩陣根本不需要真的矩陣,只需要兩個東西 190 | 191 | - 一個一維的 `Vec` 192 | - 一個將二維索引映射到一維空間的函數 193 | 194 | 再稍微做些小調整就行,如下: 195 | 196 | ```patch 197 | // ...snip 198 | // 1 199 | + let index = |i, j| i * (target_count + 1) + j; 200 | 201 | // 2 202 | - let mut distances = vec![vec![0; target_count + 1]; source_count + 1]; 203 | + let mut distances = vec![0; (target_count + 1) * (source_count + 1)]; 204 | 205 | for i in 1..=source_count { 206 | // 3 207 | - distances[i][0] = i; 208 | + distances[index(i, 0)] = i; 209 | } 210 | // ...change matrix indexing as above 211 | ``` 212 | 213 | 1. 二維索引轉換成一維索引的閉包(closure) 214 | 2. 原本的 `Vec>` 變為 `Vec` 的一維矩陣,總元素量不變 215 | 3. 改寫所有矩陣索引 216 | 217 | > 若要更有趣的索引方式,可以實作 [`core::ops::Index`][] trait,這裡就不贅述了。 218 | 219 | [`core::ops::Index`]: http://doc.rust-lang.org/core/ops/trait.Index.html 220 | 221 | ### 拋棄矩陣:降低空間複雜度 222 | 223 | 回想一下,當我們在計算一個編輯距離時,其實只需要取得三個累積的編輯距離:插入 `matrix[i, j-1]`、刪除 `matrix[i-1, j]`,以及置換 `matrix[i-1, j-1]`。如圖四 224 | 225 | ![](pre-opt-1.png) 226 | 227 | _圖四:計算一個編輯距離所需之資料。_ 228 | 229 | 這就表示,根本「**無需儲存完整距離矩陣**」,只需儲存前一列的資訊 + 額外一個變數儲存置換操作的成本就行。我們需要儲存的資訊從 (a + 1) x (b + 1) 大小的矩陣,縮小至 a + 1 或 b + 1 長度的一維陣列。 230 | 231 | ![](opt-1.png) 232 | 233 | _圖五:僅使用一維陣列儲存前一列的資訊。黃色為儲存的陣列,藍色為額外儲存的置換的累積編輯距離_ 234 | 235 | 這個最佳化的程式碼,比起矩陣解法又更簡短了,以下一一說明: 236 | 237 | ```rust 238 | {{#include mod.rs:lev_dist}} 239 | ``` 240 | 241 | 1. 和前一段相同,計算空字串的距離。 242 | 2. 不需要初始化 a x b 個元素,只需要初始化最多一列的陣列。這裡利用 `0..=count` 這種 [`core::ops::RangeInclusive`][] 的寫法,直接產出含有 0 到字串長度的 `Vec`,這段等同 naive 作法中填入第一列全插入的編輯距離,但省下自己手寫一個迴圈。 243 | 3. 將索引 i 作為初始化的刪除編輯距離,等同於 naive 作法中填入第一行全刪除的編輯距離,但尚未用到的距離就跟著索引 i 遞增,不需事先計算了。這個 `sub` 也等同於置換的累積編輯距離。 244 | 4. 置換的累積編輯距離 + 1 就會等於插入的累積編輯距離。 245 | 5. 前面步驟配置好陣列後,就可以來計算函數: 246 | - 插入:索引 j 的元素就是插入的累積編輯距離(圖五紅字左方)。 247 | - 刪除:索引 j + 1 的元素就是刪除的累積編輯距離(圖五紅字上方)。 248 | - 置換:`sub` 變數儲存的就是置換的累積編輯距離(圖五藍色)。 249 | 6. 更新 `sub` 置換的累積編輯距離,也就是這一輪的刪除的累積編輯距離(圖六藍色),以供內層迴圈下個 target char 疊代使用。 250 | 7. 將計算所得的編輯距離填入陣列中(圖五紅字/圖六紅字左方),以供外層迴圈計算下一列疊代時使用。 251 | 8. 計算完成,取最後一個元素就是兩字串的編輯距離了。 252 | 253 | [`core::ops::RangeInclusive`]: http://doc.rust-lang.org/core/ops/struct.RangeInclusive.html 254 | 255 | ![](opt-2.png) 256 | 257 | _圖六:使用一維陣列疊代計算,藍色為額外儲存的置換的累積編輯距離_ 258 | 259 | ## 效能 260 | 261 | | | Complexity | 262 | | ------------------- | -------------- | 263 | | Worst | $O(mn)$ | 264 | | Worst space | $O(\min(m,n))$ | 265 | | Worst space (naive) | $O(mn)$ | 266 | 267 | > $m$:字串 a 的長度 268 | > $n$:字串 b 的長度 269 | 270 | 顯而易見,萊文斯坦距離最差時間複雜度就是內外兩個迴圈疊代兩個字串的所有字元。而空間複雜度原本是 $m \cdot n$ 的矩陣,在最佳化動態規劃後,只需兩字串長度 m 或 n 中最小值長度陣列作為儲存空間。 271 | 272 | ## 參考資料 273 | 274 | - [Wiki: Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance) 275 | - [Levenshtein distance in Cargo][cargo-lev-distance] 276 | - [Levenshtein distance in Rust Std (1.0.0-alpha)][rust-std-lev-distance] 277 | - [Ilia Schelokov: Optimizing loop heavy Rust code](https://thaumant.me/optimizing-loop-heavy-rust/) 278 | - [Turnerj: Levenshtein Distance (Part 2: Gotta Go Fast)](https://turnerj.com/blog/levenshtein-distance-part-2-gotta-go-fast) 279 | 280 | [wiki-edit-distance]: https://en.wikipedia.org/wiki/Edit_distance 281 | [rust-std-lev-distance]: https://github.com/rust-lang/rust/commit/4908017d59da8694b9ceaf743baf1163c1e19086#diff-4bb86c087880e113f4d68d0b846eff3f5078612f8c08915a2b58162c332fb7dc 282 | [cargo-lev-distance]: https://github.com/rust-lang/cargo/blob/7d7fe6797ad07f313706380d251796702272b150/src/cargo/util/lev_distance.rs 283 | -------------------------------------------------------------------------------- /src/levenshtein_distance/base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/rust-algorithm-club/1954301f21d44c18ea44864a776a3f6fd72eaefe/src/levenshtein_distance/base.png -------------------------------------------------------------------------------- /src/levenshtein_distance/matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/rust-algorithm-club/1954301f21d44c18ea44864a776a3f6fd72eaefe/src/levenshtein_distance/matrix.png -------------------------------------------------------------------------------- /src/levenshtein_distance/mod.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | 3 | /// Calculate Levenshtein distance for two UTF-8 encoded strings. 4 | /// 5 | /// Returns a minimum number of edits to transform from source to target string. 6 | /// 7 | /// Levenshtein distance accepts three edit operations: insertion, deletion, 8 | /// and substitution. 9 | /// 10 | /// References: 11 | /// 12 | /// - [Levenshtein distance in Cargo][1] 13 | /// - [Ilia Schelokov: Optimizing loop heavy Rust code][2] 14 | /// 15 | /// [1]: https://github.com/rust-lang/cargo/blob/7d7fe6797ad07f313706380d251796702272b150/src/cargo/util/lev_distance.rs 16 | /// [2]: https://thaumant.me/optimizing-loop-heavy-rust/ 17 | // ANCHOR: lev_dist 18 | pub fn levenshtein_distance(source: &str, target: &str) -> usize { 19 | // 1 20 | if source.is_empty() { 21 | return target.len(); 22 | } 23 | if target.is_empty() { 24 | return source.len(); 25 | } 26 | 27 | // 2 28 | let mut distances = (0..=target.chars().count()).collect::>(); 29 | 30 | for (i, ch1) in source.chars().enumerate() { 31 | let mut sub = i; // 3 32 | distances[0] = sub + 1; // 4 33 | for (j, ch2) in target.chars().enumerate() { 34 | let dist = cmp::min( 35 | // 5 36 | cmp::min( 37 | distances[j], // insert 38 | distances[j + 1], // delete 39 | ) + 1, 40 | sub + (ch1 != ch2) as usize, // substitute 41 | ); 42 | 43 | sub = distances[j + 1]; // 6 44 | distances[j + 1] = dist; // 7 45 | } 46 | } 47 | 48 | *distances.last().unwrap() // 8 49 | } 50 | // ANCHOR_END: lev_dist 51 | 52 | /// Naïvely calculate Levenshtein distance using the whole distance matrix to 53 | /// store information for all substrings. 54 | // ANCHOR: naive 55 | pub fn levenshtein_distance_naive(source: &str, target: &str) -> usize { 56 | if source.is_empty() { 57 | return target.len(); 58 | } 59 | if target.is_empty() { 60 | return source.len(); 61 | } 62 | 63 | // ANCHOR: naive_init 64 | let source_count = source.chars().count(); // 1 65 | let target_count = target.chars().count(); 66 | 67 | let mut distances = vec![vec![0; target_count + 1]; source_count + 1]; // 2 68 | 69 | // 3 70 | for i in 1..=source_count { 71 | distances[i][0] = i; 72 | } 73 | 74 | for j in 1..=target_count { 75 | distances[0][j] = j; 76 | } 77 | // ANCHOR_END: naive_init 78 | 79 | // ANCHOR: naive_calc 80 | for (i, ch1) in source.chars().enumerate() { 81 | for (j, ch2) in target.chars().enumerate() { 82 | let ins = distances[i + 1][j] + 1; // 1 83 | let del = distances[i][j + 1] + 1; // 2 84 | let sub = distances[i][j] + (ch1 != ch2) as usize; // 3 85 | distances[i + 1][j + 1] = cmp::min(cmp::min(ins, del), sub); // 4 86 | } 87 | } 88 | 89 | // 5 90 | *distances.last().and_then(|d| d.last()).unwrap() 91 | // ANCHOR_END: naive_calc 92 | } 93 | // ANCHOR_END: naive 94 | 95 | #[cfg(test)] 96 | mod base { 97 | use super::*; 98 | 99 | fn test_equality(f: impl Fn(&str, &str) -> usize) { 100 | let cases = ["", "r", "ru", "rus", "rust"]; 101 | for &s in &cases { 102 | assert_eq!(f(s, s), 0); 103 | } 104 | } 105 | 106 | fn test_insertion(f: impl Fn(&str, &str) -> usize) { 107 | let cases = [ 108 | (1, "rustalgo", "*rustalgo"), 109 | (2, "rustalgo", "**rustalgo"), 110 | (3, "rustalgo", "***rustalgo"), 111 | (1, "rustalgo", "rust*algo"), 112 | (2, "rustalgo", "rust**algo"), 113 | (3, "rustalgo", "rust***algo"), 114 | (1, "rustalgo", "rustalgo*"), 115 | (2, "rustalgo", "rustalgo**"), 116 | (3, "rustalgo", "rustalgo***"), 117 | (2, "rustalgo", "*r*ustalgo"), 118 | (3, "rustalgo", "*r*u*stalgo"), 119 | (4, "rustalgo", "*ru**stalgo*"), 120 | ]; 121 | for &(dist, s1, s2) in &cases { 122 | assert_eq!(f(s1, s2), dist); 123 | assert_eq!(f(s2, s1), dist); 124 | } 125 | } 126 | 127 | fn test_deletion(f: impl Fn(&str, &str) -> usize) { 128 | let cases = [ 129 | (1, "rustalgo", "ustalgo"), 130 | (2, "rustalgo", "stalgo"), 131 | (3, "rustalgo", "talgo"), 132 | (1, "rustalgo", "rustalg"), 133 | (2, "rustalgo", "rustal"), 134 | (3, "rustalgo", "rusta"), 135 | (2, "rustalgo", "utalgo"), 136 | (3, "rustalgo", "rstag"), 137 | (8, "rustalgo", ""), 138 | ]; 139 | for &(dist, s1, s2) in &cases { 140 | assert_eq!(f(s1, s2), dist); 141 | assert_eq!(f(s2, s1), dist); 142 | } 143 | } 144 | 145 | fn test_substitution(f: impl Fn(&str, &str) -> usize) { 146 | let cases = [ 147 | (1, "rustalgo", "*ustalgo"), 148 | (2, "rustalgo", "**stalgo"), 149 | (3, "rustalgo", "***talgo"), 150 | (1, "rustalgo", "rusta*go"), 151 | (2, "rustalgo", "rusta**o"), 152 | (3, "rustalgo", "rusta***"), 153 | (3, "rustalgo", "*u*t*lgo"), 154 | (4, "rustalgo", "r**t*lg*"), 155 | (8, "rustalgo", "********"), 156 | ]; 157 | for &(dist, s1, s2) in &cases { 158 | assert_eq!(f(s1, s2), dist); 159 | assert_eq!(f(s2, s1), dist); 160 | } 161 | } 162 | 163 | fn test_mixed(f: impl Fn(&str, &str) -> usize) { 164 | let sample = [ 165 | (8, "", "rustalgo"), 166 | (4, "rustalgo", "**ruslgo"), 167 | (3, "kitten", "sitting"), 168 | (3, "saturday", "sunday"), 169 | (3, "台灣國語", "閩南語"), 170 | (7, "⭕️❌肺炎染", "嚴重⭕️傳染性💩肺炎"), 171 | ]; 172 | for &(dist, s1, s2) in &sample { 173 | assert_eq!(f(s1, s2), dist); 174 | assert_eq!(f(s2, s1), dist); 175 | } 176 | } 177 | 178 | // optimized 179 | 180 | #[test] 181 | fn equality() { 182 | test_equality(levenshtein_distance); 183 | } 184 | 185 | #[test] 186 | fn insertion() { 187 | test_insertion(levenshtein_distance); 188 | } 189 | 190 | #[test] 191 | fn deletion() { 192 | test_deletion(levenshtein_distance); 193 | } 194 | 195 | #[test] 196 | fn substitution() { 197 | test_substitution(levenshtein_distance); 198 | } 199 | 200 | #[test] 201 | fn mixed() { 202 | test_mixed(levenshtein_distance); 203 | } 204 | 205 | // naive implementation 206 | 207 | #[test] 208 | fn equality_naive() { 209 | test_equality(levenshtein_distance_naive); 210 | } 211 | 212 | #[test] 213 | fn insertion_naive() { 214 | test_insertion(levenshtein_distance_naive); 215 | } 216 | 217 | #[test] 218 | fn deletion_naive() { 219 | test_deletion(levenshtein_distance_naive); 220 | } 221 | 222 | #[test] 223 | fn substitution_naive() { 224 | test_substitution(levenshtein_distance_naive); 225 | } 226 | 227 | #[test] 228 | fn mixed_naive() { 229 | test_mixed(levenshtein_distance_naive); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/levenshtein_distance/opt-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/rust-algorithm-club/1954301f21d44c18ea44864a776a3f6fd72eaefe/src/levenshtein_distance/opt-1.png -------------------------------------------------------------------------------- /src/levenshtein_distance/opt-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/rust-algorithm-club/1954301f21d44c18ea44864a776a3f6fd72eaefe/src/levenshtein_distance/opt-2.png -------------------------------------------------------------------------------- /src/levenshtein_distance/pre-opt-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/rust-algorithm-club/1954301f21d44c18ea44864a776a3f6fd72eaefe/src/levenshtein_distance/pre-opt-1.png -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! ![](https://weihanglo.tw/rust-algorithm-club/logo.svg) 2 | //! 3 | //! # Welcome to Rust algorithm club! 4 | 5 | #![warn(missing_docs)] 6 | #![warn(dead_code)] 7 | #![deny(deprecated)] 8 | #![deny(nonstandard_style)] 9 | #![doc(html_logo_url = "https://weihanglo.tw/rust-algorithm-club/favicon.png")] 10 | 11 | pub mod collections; 12 | pub mod searching; 13 | pub mod sorting; 14 | 15 | mod levenshtein_distance; 16 | pub use levenshtein_distance::{levenshtein_distance, levenshtein_distance_naive}; 17 | 18 | mod hamming_distance; 19 | pub use hamming_distance::{hamming_distance, hamming_distance_str}; 20 | -------------------------------------------------------------------------------- /src/logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/rust-algorithm-club/1954301f21d44c18ea44864a776a3f6fd72eaefe/src/logo.sketch -------------------------------------------------------------------------------- /src/searching/binary_search/README.md: -------------------------------------------------------------------------------- 1 | # 二元搜尋 Binary Search 2 | 3 | Binary search,又稱對數搜尋(logarithmic search),是一個在已排序的序列中,快速找出特定元素的搜尋演算法。二元搜尋的步驟就像玩猜數字,先猜一個數字,告訴你你的猜測比正確答案大或小,再繼續往對的方向猜,捨棄猜錯的另一半。這樣持續進行好幾次猜測,每猜一次,搜尋範圍就縮小一半,因此稱為「二元」搜尋。 4 | 5 | 二元搜尋有以下幾個特點: 6 | 7 | - 概念簡單,搜尋高效,達到對數執行時間 $O(\log n)$。 8 | - 不需額外實作資料結構或配置記憶體空間。 9 | - 只能搜尋**已排序**的序列。 10 | 11 | ## 步驟 12 | 13 | 1. 從序列中間的元素開始,比較其與目標值 14 | 2. 若該元素為搜尋目標,則結束搜尋。 15 | 3. 若該元素較大或小,則將序列切一半,往較小或較大的一半搜尋。 16 | 4. 繼續從一半的序列中間的元素開始,重複步驟一到三,直到欲搜尋的序列為空。 17 | 18 | ## 說明 19 | 20 | 這裡有一個排好序的序列,共有 15 個元素,現在要找尋 9 是否在序列中。 21 | 22 | ``` 23 | * 24 | [2, 3, 3, 6, 6, 7, 9, 13, 15, 19, 20, 22, 23, 24, 25] 25 | ``` 26 | 27 | 首先,先找到中間的元素 15 / 2 ~= 8,第八個元素為 13,比 9 大,因此捨棄第八個元素之後的所有元素。 28 | 29 | ``` 30 | * 31 | [2, 3, 3, 6, 6, 7, 9, _, _, _, _, _, _, _, _] 32 | ``` 33 | 34 | 接下來繼續對半搜尋,8 / 2 = 4,找尋第四個元素來比對,6 比 9 小,,因此捨棄第四個元素前的所有元素。 35 | 36 | ``` 37 | * 38 | [_, _, _, 6, 6, 7, 9, _, _, _, _, _, _, _, _] 39 | ``` 40 | 41 | 對剩下的元素二元搜尋,4 / 2 = 2,並從第四個元素開始計算中點 4 + 2 = 6,取得第六個元素為 7,比 9 小,捨棄 7 之前的元素。 42 | 43 | ``` 44 | * 45 | [_, _, _, _, _, 7, 9, _, _, _, _, _, _, _, _] 46 | ``` 47 | 48 | 繼續切一半來搜尋,繼續找中間的元素 2 / 2 = 1,並從第六個元素計算索引位置 6 + 1 = 7,查看第七個元素是 9,終於找到了! 49 | 50 | ## 效能 51 | 52 | | | Complexity | 53 | | ------------ | ----------- | 54 | | Worst | $O(\log n)$ | 55 | | Best | $O(1)$ | 56 | | Average | $O(\log n)$ | 57 | | Worst space | $O(1)$ | 58 | 59 | 二元搜尋可以透過分治法(Divide and conquer)遞迴求解,而遞迴的終止條件是序列不能在切兩半。由此可知,二元搜尋的複雜度奠基在要切幾次,子序列長度才會等於 1。設 $n$ 為資料數目,$k$ 為要切幾次才會達成終止條件,可得: 60 | 61 | $$ \frac{n}{2^k} = 1 $$ 62 | 63 | 接下來同乘 $2^k$ 並取對數。 64 | $$ 65 | \frac{n}{2^k} = 1 \\\\ 66 | \Rightarrow 2^k = n \\\\ 67 | $$ 68 | 69 | 再將左式整理一下,得到 $k$。 70 | 71 | $$ 72 | \log_2 2^k = log_2 n \\\\ 73 | \Rightarrow k \cdot \log_2 2 = log_2 n \\\\ 74 | \Rightarrow k = log_2 n 75 | $$ 76 | 77 | 於是,我們得到二元搜尋時間複雜度為 $O(k) = O(\log_2 n) = O(\log n)$。 78 | 79 | 寫這種式子也許不好理解,我們可以把搜尋過程和每個分支寫成樹狀圖,方便觀察。假設一個數列有七個元素 `[1, 2, 3, 4, 5, 6, 7]`,其二元搜尋所有可能路徑的樹狀圖如下: 80 | 81 | ``` 82 | +---+ 83 | | 4 | 84 | +---+ 85 | / \ 86 | +---+ +---+ 87 | | 2 | | 6 | 88 | +---+ +---+ 89 | / \ / \ 90 | +---+ +---+ +---+ +---+ 91 | | 1 | | 3 | | 5 | | 7 | 92 | +---+ +---+ +---+ +---+ 93 | ``` 94 | 95 | 樹中每一條路徑都代表任意搜尋會經過的步驟,總共有 7 種不同的搜尋路徑,最短路徑僅需要 $\lfloor{\log_2 n} = 3 \rfloor$ 個操作,也就是需要執行「樹高」次的操作。 96 | 97 | ## 實作 98 | 99 | ### 函式宣告 100 | 101 | 二元搜尋概念看似簡單,實際上誤區一堆,不易寫出完全正確的演算法。我們參考 [Rust slice binary_search][rust-slice-binary-search] 的實作。先來看看函式宣告的簽名(function signature)。 102 | 103 | ```rust 104 | pub fn binary_search(arr: &[T], target: &T) -> Result 105 | where T: PartialOrd 106 | ``` 107 | 108 | 二元搜尋函式宣告中,回傳值大概是最特別的部分。如果有找到目標元素,`Result` 會是 `Ok(目標索引位置)`,如果沒有找到則回傳 `Err(目標值若插入後,不會影響序列排序的位置)`。`Err` 回傳值提供了插入點,非常方便。 109 | 110 | 再來,`T` 泛型參數需是 [`PartialOrd`][rust-trait-partialord],這是由於二元搜尋使用排序過後的元素,比起線性搜尋,仍需元素之間相互比較。 111 | 112 | ### 函式主體 113 | 114 | 市面上常見的實作通常以兩個變數 `l` 與 `r` 記錄搜尋範圍的上下界,而我們另闢蹊徑,記錄了 115 | 116 | - `base`:搜尋範圍的下界, 117 | - `size`:搜尋範圍的長度。 118 | 119 | 以下是完整實作: 120 | 121 | ```rust 122 | pub fn binary_search(arr: &[T], target: &T) -> Result 123 | where T: PartialOrd 124 | { 125 | let mut size = arr.len(); // 1 126 | if size == 0 { 127 | return Err(0); 128 | } 129 | let mut base = 0_usize; 130 | 131 | while size > 1 { // 2 132 | // mid: [base..size) 133 | let half = size / 2; // 2.1 134 | let mid = base + half; 135 | if arr[mid] <= *target { // 2.2 136 | base = mid 137 | } 138 | size -= half; // 2.3 139 | } 140 | 141 | if arr[base] == *target { // 3 142 | Ok(base) 143 | } else { 144 | Err(base + (arr[base] < *target) as usize) 145 | } 146 | } 147 | ``` 148 | 149 | 1. 第一部分先取得搜尋範圍 `size` 以及確定下界為 `0_usize`。這裡同時檢查若序列長度為零,直接回傳 `Err(0)`,告知呼叫端可直接在 index 0 新增元素。 150 | 2. 第二部分就是精髓了,將終止條件設在 `size <= 1`,以確保迴圈能夠正常結束。 151 | 1. 先將搜尋範圍對半切,再與下界 `base` 相加,算出中點。 152 | 2. 另中間元素與目標值比較,如果比較小,則移動下界至中點。 153 | 3. 將 `size` 減半,縮小搜尋範圍。 154 | 3. 到了第三部分,`base` 已經是切到長度為一的序列了,若匹配目標值就直接回傳;若否,需要傳可供目標值插入的位置,將 bool 判斷是轉型成 `usize`,若 `arr[base]` 比目標值小,則目標值要加到其後 +1 位置,反之則加在其前 -1 位置。 155 | 156 | [rust-slice-binary-search]: https://doc.rust-lang.org/std/primitive.slice.html#method.binary_search 157 | [rust-trait-partialord]: https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html 158 | 159 | ## 常見誤區與解法 160 | 161 | 1. 只適用已排序序列: 這是使用二元搜尋的前提,千萬不能忽略這重要特性,否則後果絕對大錯特錯。 162 | 163 | 2. 處理重複元素:一般的實作通常是回傳任意符合目標值的索引位置,就算有重複的元素,仍然不可預期。若要回傳特定位置(leftmost 或 rightmost),則需特別處理。 164 | 165 | 3. 整數溢位:部分二元搜尋實作會 以兩個變數儲存搜尋範圍上下界的索引位置,而取中點時千萬不可直接將上下界相加再除二,否則很可能整數溢位(integer overflow)。 166 | ```rust 167 | let mid = (end + start) / 2 // Wrong: integer overflow 168 | let mid = start + (end - start) / 2 // Correct 169 | ``` 170 | 171 | 4. 終止條件錯誤:無論如何實作,請將終止條件設為「搜尋範圍為空」,也就是下界大於上界,而不要只比較上下界是否相等。其實搜尋範圍低於一定長度,即可使用線性搜尋替代,避免處理邊界值的麻煩,實務上也幾乎沒有太多效能損失。 172 | 173 | ## 變形與衍生 174 | 175 | ### Interpolation Search 176 | 177 | [Interpolation search][interp-search] 改良自二元搜尋,差別在於,二元搜尋選擇中間的元素作為二分點,而 interpolation search 人如其名,以內插法找尋二分點。在資料平均分佈時,比二元搜尋更高效。欲知後續,待下回[內插搜尋 Interpolation search][interp-search] 分曉。 178 | 179 | ### Exponential Search 180 | 181 | [Exponential search][exp-search] 是一種特殊的二元搜尋,主要用在搜尋無限、無邊界的已排序序列,由於邊界未知長度就未知,無法以傳統二元搜尋找尋中點。Exponential 顧名思義就是不斷比較在 $2^0$,$2^1$ 直到 $2^n$ 的位置上資料是否比目標值大,若較大,再從該位置執行二元搜尋回頭找。詳情請看[指數搜尋 Exponential search][exp-search]。 182 | 183 | ### Binary Insertion Sort 184 | 185 | Insertion sort 有一個步驟是在前面已經排完序的資料中,找到適合的地方插入待排序的元素,這部分可透過二元搜尋加快在已排序資料搜尋的速度。詳情請參考 [Binary insertion sort](../../sorting/insertion_sort/#binary-insertion-sort)。 186 | 187 | [interp-search]: ../interpolation_search 188 | [exp-search]: ../exponential_search 189 | 190 | 191 | ## 參考資料 192 | 193 | - [Wiki: Binary search algorithm](https://en.wikipedia.org/wiki/Binary_search_algorithm) 194 | - [知乎:二分查找有几种写法?它们的区别是什么?](https://www.zhihu.com/question/36132386) 195 | -------------------------------------------------------------------------------- /src/searching/binary_search/mod.rs: -------------------------------------------------------------------------------- 1 | /// Handmade binary search for a sorted sequence. 2 | /// 3 | /// This version of binary search does not guarantee retrieval of the leftmost 4 | /// matching position if multiple elements found. 5 | /// 6 | /// Reference: 7 | /// 8 | /// - [`std::slice::binary_search`][1] 9 | /// 10 | /// [1]: https://doc.rust-lang.org/std/primitive.slice.html#method.binary_search 11 | pub fn binary_search(arr: &[T], target: &T) -> Result 12 | where 13 | T: PartialOrd, 14 | { 15 | let mut size = arr.len(); 16 | if size == 0 { 17 | return Err(0); 18 | } 19 | let mut base = 0_usize; 20 | 21 | while size > 1 { 22 | // mid: [base..size) 23 | let half = size / 2; 24 | let mid = base + half; 25 | if arr[mid] <= *target { 26 | base = mid 27 | } 28 | size -= half; 29 | } 30 | 31 | if arr[base] == *target { 32 | Ok(base) 33 | } else { 34 | // Return the expected position in the array. 35 | Err(base + (arr[base] < *target) as usize) 36 | } 37 | } 38 | 39 | #[cfg(test)] 40 | mod base { 41 | use super::*; 42 | 43 | sorted_no_duplicate_cases!(binary_search); 44 | } 45 | -------------------------------------------------------------------------------- /src/searching/exponential_search/README.md: -------------------------------------------------------------------------------- 1 | # 指數搜尋 Exponential Search 2 | 3 | 指數搜尋,又稱為 galloping search,是一種特殊的[二元搜尋][binary-search],主要用在搜尋無限、無邊界的已排序序列。由於邊界未知長度就未知,無法以傳統二元搜尋來找中點。而 Exponential 顧名思義就是從底數為 2,指數為 0 的索引($2^0$ )開始,不斷比較在 $2^1$、$2^2$ 直到 $2^k$ 位置上的值,若比目標值大,則停止指數成長,直接從該位置執行二元搜尋,回頭尋找目標值。 4 | 5 | 指數搜尋的特點如下: 6 | 7 | - 可以搜尋邊界未知的已排序序列。 8 | - 縮小搜尋範圍,可比 naïve 的二元搜尋效率高些。 9 | - 若目標值實際位置很靠近序列前端,效率會非常棒。 10 | 11 | ## 步驟 12 | 13 | 指數搜尋的步驟只有非常簡單的兩步驟: 14 | 15 | 1. 依照目標值大小,劃出搜尋範圍。 16 | 2. 在上述範圍內執行二元搜尋。 17 | 18 | 而劃出搜尋範圍這部分也很直觀: 19 | 20 | 1. 選定一個底數 $k$,通常為 2。 21 | 2. 比較 $k^i$ 索引下的值是否比目標值大,$i$ 從零開始。 22 | 3. 若較小,指數加一 $k^{i + 1}$ 後繼續重複步驟二比較。 23 | 4. 若較大,停止比較,得搜尋範圍為 $k^{i - 1}$ 到 $k^i$。 24 | 25 | ## 說明 26 | 27 | 這裡有個排好序的序列,我們要尋找其中是否有 22 這個數字。 28 | 29 | ``` 30 | * 31 | [2, 3, 3, 6, 6, 7, 9, 13, 15, 19, 20, 22, 23, 24, 25] 32 | ``` 33 | 34 | 首先,先尋找 $2^0 = 1$ 位置上的數字是否超過 22。`3 < 22`,很明顯沒有。 35 | 36 | 37 | ``` 38 | * * * 39 | [2, 3, 3, 6, 6, 7, 9, 13, 15, 19, 20, 22, 23, 24, 25] 40 | ``` 41 | 42 | 再來,連續看看 43 | 44 | - $2^1$:`3 < 22` 45 | - $2^2$:`6 < 22` 46 | - $2^3$:`15 < 22` 47 | 48 | 也都沒有超越 22。 49 | 50 | 51 | ``` 52 | * 53 | [2, 3, 3, 6, 6, 7, 9, 13, 15, 19, 20, 22, 23, 24, 25] _, _ 54 | ``` 55 | 56 | 最後,一口氣將指數加到 4,看看$2^4$ 上的數字是否大於 22。哎呀,$2^4 = 16$,的位置已經超出序列長度,因此取至序列最後一個數字作為比較對象。`25 > 22`,找到了! 57 | 58 | 得到搜尋的範圍是 $$2^{4-1} < x < \text{array.length} < 2^{4}$$ 59 | 60 | ## 效能 61 | 62 | | | Complexity | 63 | | ------------ | ----------- | 64 | | Worst | $O(\log i)$ | 65 | | Best | $O(1)$ | 66 | | Average | $O(\log i)$ | 67 | | Worst space | $O(1)$ | 68 | 69 | > $i$:目標值在序列中實際的索引位置。 70 | 71 | 指數搜尋的複雜度分為兩部分分析: 72 | 73 | ### 劃定搜尋範圍 74 | 75 | 設 $i$ 為目標值在序列中實際的索引位置,則搜尋上界,指數增加的操作需執行 $\lceil \log(i) \rceil$ 次,例如匹配目標值的搜尋結果位於序列第 9 個,則指數需增加 $\lceil \log(9) \rceil = 4$ 次,上界才會超過目標值。我們設這部分的複雜度為 $O(log i)$。 76 | 77 | ### 執行二元搜尋 78 | 79 | 第二部分就是二元搜尋,複雜度為 $O(log n)$,$n$ 為搜尋範圍的長度。根據第一部分,可以得知範圍長度為 $2^{\log i} - 2^{\log{i - 1}} = 2^{log{i - 1}}$ 個元素,帶入二元搜尋的複雜度,計算出第二部分的複雜度為 $log (2^{\log{i - 1}}) = \log{(i)} - 1 = O(\log i)$。 80 | 81 | 82 | 最後,將兩部分的複雜度合起來,就是指數搜尋的時間複雜度了。 83 | 84 | $$O(\log i) + O(\log i) = 2 O(\log i) = O(\log i)$$ 85 | 86 | ## 實作 87 | 88 | 本次實作有邊界的指數搜尋,主要分為三個部分: 89 | 90 | 1. 處理空序列的狀況。 91 | 2. 利用指數,決定搜尋範圍。 92 | 3. 執行二元搜尋,並將輸出結果映射回原始序列。 93 | 94 | 話不多說,直接看程式碼。 95 | 96 | ```rust 97 | use crate::searching::binary_search; 98 | 99 | pub fn exponential_search(arr: &[T], target: &T) -> Result 100 | where T: PartialOrd 101 | { 102 | // 1. Handle empty scenario. 103 | let size = arr.len(); 104 | if size == 0 { 105 | return Err(0); 106 | } 107 | 108 | // 2. Determine searching boundaries. 109 | let mut hi = 1_usize; // Upper bound. 110 | while hi < size && arr[hi] < *target { 111 | hi <<= 1; 112 | } 113 | let lo = hi >> 1; // Lower bound. 114 | 115 | // 3. Do binary search. 116 | binary_search(&arr[lo..size.min(hi + 1)], target) 117 | .map(|index| lo + index) 118 | .map_err(|index| lo + index) 119 | } 120 | ``` 121 | 122 | 1. 和二元搜尋同,遇到空序列就返回 `Err(0)` 告知呼叫端可新增資料在位置 0。 123 | 2. 決定搜尋上下界,只要 上界不超過序列長度,且 `arr[hi]` 小於目標值,就讓上界指數成長。這裡用位元左移運算子(bitwise left shift)實作乘以 2。 124 | 找到上界後,再將上界除以 2(位元右移),就是下界了。 125 | 3. 確定範圍後,利用上下界切序列的 sub slice 作為引數,傳遞給二元搜尋。要注意的是,為了避免 sub slice 超出邊界,上界需在 `size` 與 `hi + 1` 之間找最小值。 126 | 由於回傳結果的位置是以 sub slice 起始,需加上位移量(下界 `lo`)才會對應原始 slice 的位置。 127 | 128 | > 由於內部使用[二元搜尋][binary-search],若該二元搜尋沒有處理重複元素的狀況,指數搜尋連帶無法預期這個行為。 129 | 130 | ## 參考資料 131 | 132 | [Wiki: Exponential search](https://en.wikipedia.org/wiki/Exponential_search) 133 | 134 | 135 | [binary-search]: ../binary_search 136 | -------------------------------------------------------------------------------- /src/searching/exponential_search/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::searching::binary_search; 2 | 3 | /// Exponential search. An binary search variant that can perform on 4 | /// unbounded sequences and infinite lists. 5 | /// 6 | /// Use [`crate::searching::binary_search`][1] as the underlying algorithm. 7 | /// 8 | /// [1]: ./fn.binary_search.html 9 | pub fn exponential_search(arr: &[T], target: &T) -> Result 10 | where 11 | T: PartialOrd, 12 | { 13 | let size = arr.len(); 14 | if size == 0 { 15 | return Err(0); 16 | } 17 | 18 | let mut hi = 1_usize; // Upper bound. 19 | while hi < size && arr[hi] < *target { 20 | hi <<= 1; 21 | } 22 | let lo = hi >> 1; // Lower bound. 23 | 24 | // Search within [lo..size) or [lo..hi] 25 | binary_search(&arr[lo..size.min(hi + 1)], target) 26 | .map(|index| lo + index) // Adjust index offset. 27 | .map_err(|index| lo + index) 28 | } 29 | 30 | #[cfg(test)] 31 | mod base { 32 | use super::*; 33 | 34 | sorted_no_duplicate_cases!(exponential_search); 35 | } 36 | -------------------------------------------------------------------------------- /src/searching/interpolation_search/README.md: -------------------------------------------------------------------------------- 1 | # 內插搜尋 Interpolation Search 2 | 3 | 內插搜尋 Interpolation search 為[二元搜尋][binary-search]的變種,差別在於二分點的選擇方法,二元搜尋選擇中間的元素作為二分點,而內插搜尋則名副其實,以內插法找尋二分點。內插法有許多種類,本次搜尋演算法選擇使用常見的[線性內插(linear interpolation)][wiki-lerp]實作。 4 | 5 | 內插搜尋的特色如下: 6 | 7 | - 資料需要是可計算[內插(interpolation)][wiki-interp]的數值資料。 8 | - 對資料分佈敏感,資料均勻分佈時,效能勝過二元搜尋。 9 | - 資料分佈不均勻時,最差複雜度高達 $O(n)$。 10 | 11 | [wiki-interp]: https://en.wikipedia.org/wiki/Interpolation 12 | [wiki-lerp]: https://en.wikipedia.org/wiki/Linear_interpolation 13 | [binary-search]: ../binary_search 14 | 15 | ## 步驟 16 | 17 | 1. 確認資料已經排好序。 18 | 2. 利用第一個元素 a 與最後的元素 b,以及搜尋上下界 hi 與 lo 位置,作為兩個端點。 19 | 3. 利用上述兩點 (lo, a) 與 (hi, b),對搜尋目標計算內插,得到可能的位置。 20 | 1. 若該位置上元素較小,則令其為新搜尋下界 a',重複步驟二到三,繼續求內插。 21 | 2. 若該位置上元素較大,則令其為新搜尋上界 b',重複步驟二到三,繼續求內插。 22 | 3. 若相等,則完成搜尋。 23 | 4. 搜尋停止在 a'、b' 兩元素搜尋位置重疊,以及目標值比下界 a' 小或比上界 b' 大。 24 | 25 | ## 說明 26 | 27 | 迅速說明線性內插法。線性內插法是中學必修的數學概念,給定兩點 $(x_0,y_0)$ 與 $(x_1,y_1)$,欲求在 $[x_0,x_1]$ 區間內直線上 $x'$ 點的 y 值,可以透過斜率公式求解: 28 | 29 | $$ 30 | \frac{y - y_0}{x' - x_0} = \frac{y_1 - y_0}{x_1 - x_0 } 31 | $$ 32 | 33 | 接下來就是小學解方程式的事兒了。 34 | 35 |

36 | 37 |

Cmglee - CC BY-SA 3.0

38 |
39 | 40 | 回到正題,以下用文字解釋內插搜尋。 41 | 42 | 這裡有一個已排序有 14 個元素的序列,我們需要從中找出 **27**。 43 | 44 | ``` 45 | [1, 9, 10, 15, 17, 17, 18, 23, 27, 28, 29, 30, 31, 34] 46 | ``` 47 | 48 | 我們將序列索引當作 x 軸,元素值作為 y 軸。可得已知兩點為 $(0, 1)$ 及 $(13, 34)$。 49 | 50 | 首先,透過斜率公式,計算出在 $y = 27$ 時,$x'$,也就是 27 在序列中可能的位置為 51 | 52 | $$x' = \lfloor 27 / (34 - 1) \cdot (13 - 0) \rfloor = 10$$ 53 | 54 | 查看 `arr[10]` 為 29,比搜尋目標 27 來得大。將 29 當作我們新的上界,搜尋範變成第 [0, 9] 個元素(29 不需列入搜尋),繼續計算內插 55 | 56 | $$x' = \lfloor 27 / (28 - 1) \cdot (9 - 0) \rfloor = 9$$ 57 | 58 | 查看 `arr[9]` 為 28,比搜尋目標 27 來得大。將 28 當作我們新的上界,搜尋範變成第 [0, 8] 個元素(28 不需列入搜尋),繼續計算內插 59 | 60 | $$x' = \lfloor 27 / (27 - 1) \cdot (8 - 0) \rfloor = 8$$ 61 | 62 | 查看 `arr[8]` 為 27,恰恰是搜尋目標 27,搜尋到此結束。 63 | 64 | ## 效能 65 | 66 | | | Complexity | 67 | | ------------ | ---------- | 68 | | Worst | $O(n)$ | 69 | | Best | $O(1)$ | 70 | | Average | $O(n)$ | 71 | | Average | $O(\log \log n)$ on uniform distributed data | 72 | | Worst space | $O(1)$ | 73 | 74 | > $n$:資料筆數 75 | 76 | 線性內差搜尋的最差時間複雜度為 $O(n)$,也就是每次內差的結果都落在邊界旁,搜尋範圍只縮小一個元素。這種情況容易發生在資料依排序呈指數或對數等非線性函數。例如 $y = 2^x$。 77 | 78 | 線性內插搜尋對資料的期望是均勻機率分佈(uniform probability distribution)。想求平均時間複雜度 $O(\log \log n)$ ,須先透過機率密度函數,計算條件機率,一步步縮小範圍,求得平均誤差,最後求得期望值。這部分計算較為複雜,有興趣的朋友可以參考閱讀資料「[Perl, Y., Itai, A., & Avni, H. (1978). Interpolation search—a log log N search.][perl-interp-paper]」。 79 | 80 | ![](https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/Uniform_Distribution_PDF_SVG.svg/320px-Uniform_Distribution_PDF_SVG.svg.png) 81 | 82 | _PDF of uniform distribution by IkamusumeFan - CC BY-SA 3.0_ 83 | 84 | [wiki-uniform-dist]: https://en.wikipedia.org/wiki/Uniform_distribution_(continuous) 85 | 86 | ## 實作 87 | 88 | 內插搜尋的實作共分為幾部分: 89 | 90 | 1. 處理空序列狀況。 91 | 2. 建立迴圈疊代共用的變數。 92 | 3. 計算線性插值的主要迴圈。 93 | 4. 將內插值映射到結果的 `Result`。 94 | 95 | 首先是函式宣告。 96 | 97 | ```rust 98 | pub fn interpolation_search( 99 | arr: &[i32], 100 | target: &i32, 101 | ) -> Result 102 | ``` 103 | 104 | 映入眼簾的是 `i32`,而非泛型參數,為什麼呢?是因為內插搜尋為了計算線性內插,資料僅限定在「數值資料」,而 Rust 並沒有特別一類 **Numeric** 的型別,自己透過 trait 實作又異常繁瑣,因此先以 `i32` 代替。而回傳值的部分,與指數搜尋/二元搜尋一模一樣,回傳的 `Result` 105 | 106 | - 若為 `Ok`,其值代表目標值在序列內的索引位置; 107 | - 若為 `Err`,則是可以將目標值插入序列內又不會破壞排序的位置。 108 | 109 | > 延續數值型別的話題,Rust 社群提供 [num](https://github.com/rust-num/num) crate,定義了各種數值型別與 trait,大整數、複數、虛數、有理數都囊括其中,非常有趣。 110 | 111 | 再來就是第一第二部分,處理空序列與建立共用變數,非常直觀。 112 | 113 | 114 | ```rust 115 | if arr.is_empty() { 116 | return Err(0) 117 | } 118 | 119 | let mut hi = arr.len() - 1; 120 | let mut lo = 0_usize; 121 | 122 | let mut interpolant = 0_usize; 123 | ``` 124 | 125 | - `hi`、`lo` 兩個變數劃定的搜尋範圍上下界。 126 | - `interpolant` 儲存線性插值,代表每次疊代的搜尋位置。 127 | 128 | 接下來就是主要的迴圈,負責疊代計算內插值。分為三個部分,直接看程式碼先。 129 | 130 | ```rust 131 | loop { 132 | let lo_val = arr[lo]; 133 | let hi_val = arr[hi]; 134 | 135 | // 1. 136 | if hi <= lo || *target < lo_val || *target > hi_val { 137 | break 138 | } 139 | 140 | // 2. The linear interpolation part 141 | let offset = (*target - lo_val) * (hi - lo) as i32 / (hi_val - lo_val); 142 | interpolant = lo + offset as usize; 143 | 144 | let mid_val = arr[interpolant]; 145 | 146 | // 3. 147 | if mid_val > *target { 148 | hi = interpolant - 1; 149 | } else if mid_val < *target { 150 | lo = interpolant + 1; 151 | } else { 152 | break 153 | } 154 | } 155 | ``` 156 | 157 | 1. 迴圈的三個終止條件,分別為: 158 | - `hi`、`lo` 兩個變數劃定的搜尋範圍重疊,長度為零。 159 | - 搜尋目標值比上界還大。 160 | - 搜尋目標值比下界還小。 161 | 2. 線性內插的計算方程式,要注意我們是寫 Rust 不是 JavaScript,`i32` 與 `usize` 不能混用,要手動轉型。 162 | 3. 比較插值與目標值。相等則跳出迴圈;若目標大於小於插值,則縮小搜尋範圍。注意,範圍需手動加減一,排除上下界,以免無限迴圈產生。 163 | 164 | 最後一部分則是決定線性插值所得的索引位置是否為目標值,並將該值映射到 `Result` 上。 165 | 166 | ```rust 167 | if *target > arr[hi] { 168 | Err(hi + 1) 169 | } else if *target < arr[lo] { 170 | Err(lo) 171 | } else { 172 | Ok(interpolant) 173 | } 174 | ``` 175 | 176 | 完整的程式碼如下。 177 | 178 | ```rust 179 | pub fn interpolation_search( 180 | arr: &[i32], 181 | target: &i32, 182 | ) -> Result { 183 | // 1. Handle empty sequence. 184 | if arr.is_empty() { 185 | return Err(0) 186 | } 187 | 188 | // 2. Setup variable storing iteration informaion. 189 | let mut hi = arr.len() - 1; 190 | let mut lo = 0_usize; 191 | 192 | let mut interpolant = 0_usize; 193 | 194 | // 3. Main loop to calculate the interpolant. 195 | loop { 196 | let lo_val = arr[lo]; 197 | let hi_val = arr[hi]; 198 | 199 | // 3.1. Three condition to exit the loop 200 | if hi <= lo || *target < lo_val || *target > hi_val { 201 | break 202 | } 203 | 204 | // 3.2. The linear interpolation part 205 | let offset = (*target - lo_val) * (hi - lo) as i32 / (hi_val - lo_val); 206 | interpolant = lo + offset as usize; 207 | 208 | let mid_val = arr[interpolant]; 209 | 210 | // 3.3. Comparison between the interpolant and targert value. 211 | if mid_val > *target { 212 | hi = interpolant - 1; 213 | } else if mid_val < *target { 214 | lo = interpolant + 1; 215 | } else { 216 | break 217 | } 218 | } 219 | 220 | // 4. Determine whether the returning interpolant are equal to target value. 221 | if *target > arr[hi] { 222 | Err(hi + 1) 223 | } else if *target < arr[lo] { 224 | Err(lo) 225 | } else { 226 | Ok(interpolant) 227 | } 228 | } 229 | ``` 230 | 231 | > 如同[二元搜尋][binary-search]與[指數搜尋 232 | ](../exponential_search),未特別處理重複元素的內插搜尋,並無法預期會選擇哪一個元素。 233 | 234 | ## 變形與衍生 235 | 236 | ### Interpolation Search Tree 237 | 238 | Interpolation search tree(IST),姑且稱它「內插搜尋樹」,是一個將內插搜尋結合樹的資料結構。如上述提及,內插搜尋達到 $O(\log \log n)$ 的搜尋時間,但僅適用於均勻機率分佈的資料。而 IST 利用動態內插搜尋,讓 1)內插搜尋樹的搜尋可以使用在更多元的**規律機率分佈**的資料中,且 2)可以達到以下的執行效能: 239 | 240 | - $O(n)$ 空間複雜度。 241 | - 預期有 $O(\log \log n)$ 的平攤增減節點操作時間,最差有 $(O \log n)$。 242 | - 在規律分佈的資料中,預期搜尋時間為 $O(\log \log n)$,最差時間複雜度則為 $O((\log n)^2)$ 243 | - 線性時間的循序存取,而取得前後節點或最小值都是常數時間。 244 | 245 | 更多詳細證明可以閱讀參考資料「[Andersson, A. (1996, October). Faster deterministic sorting and searching in linear space][andersson-paper]」。 246 | 247 | ## 參考資料 248 | 249 | - [Wiki: Interpolation search](https://en.wikipedia.org/wiki/Interpolation_search) 250 | - [Perl, Y., Itai, A., & Avni, H. (1978). Interpolation search—a log log N search. Communications of the ACM, 21(7), 550-553.][perl-interp-paper] 251 | - [Andersson, A. (1996, October). Faster deterministic sorting and searching in linear space. In Foundations of Computer Science, 1996. Proceedings., 37th Annual Symposium on (pp. 135-141). IEEE.][andersson-paper] 252 | - Linear interpolation visualisation SVG By Cmglee [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0), via Wikimedia Commons. 253 | - Probability density function of uniform distribution SVG By IkamusumeFan [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0), via Wikimedia Commons. 254 | 255 | [andersson-paper]: https://people.mpi-inf.mpg.de/~mehlhorn/ftp/DynamicInterpolationSearch.pdf 256 | [perl-interp-paper]: http://www.cs.technion.ac.il/~itai/publications/Algorithms/p550-perl.pdf 257 | -------------------------------------------------------------------------------- /src/searching/interpolation_search/mod.rs: -------------------------------------------------------------------------------- 1 | /// Search in sorted sequences by checking the next position based on an 2 | /// linear interpolation of the search key. 3 | /// 4 | /// # Parameters 5 | /// 6 | /// * `arr`: Slice to search in. 7 | /// * `target`: Object to search for. 8 | /// 9 | /// # Notes 10 | /// 11 | /// Since interpolations only be applied on numeric types, we choose `i32` to 12 | /// avoid overkilling. If you desire a full functional trait of numeric types, 13 | /// [num][1] crate would meet your needs. 14 | /// 15 | /// [1]: https://github.com/rust-num/num 16 | pub fn interpolation_search(arr: &[i32], target: &i32) -> Result { 17 | // 1. Handle empty sequence. 18 | if arr.is_empty() { 19 | return Err(0); 20 | } 21 | 22 | // 2. Setup variable storing iteration informaion. 23 | // hi -> upper bound of search range. 24 | // lo -> lower bound of search range. 25 | // interpolant -> position to probe in the sequence 26 | let mut hi = arr.len() - 1; 27 | let mut lo = 0_usize; 28 | let mut interpolant = 0_usize; 29 | 30 | // 3. Main loop to calculate the interpolant. 31 | loop { 32 | let lo_val = arr[lo]; 33 | let hi_val = arr[hi]; 34 | 35 | // 3.1. Three condition to exit the loop 36 | // a. hi and lo flag overlapping -> all elements are scanned. 37 | // b. target value is less than the lowest value 38 | // c. target value exceeds the highest value 39 | if hi <= lo || *target < lo_val || *target > hi_val { 40 | break; 41 | } 42 | 43 | // 3.2. The linear interpolation part 44 | let offset = (*target - lo_val) * (hi - lo) as i32 / (hi_val - lo_val); 45 | interpolant = lo + offset as usize; 46 | 47 | let mid_val = arr[interpolant]; 48 | 49 | // 3.3. Comparison between the interpolant and targert value. 50 | // New boundaries must step one index further to avoid infinite searching. 51 | if mid_val > *target { 52 | hi = interpolant - 1; 53 | } else if mid_val < *target { 54 | lo = interpolant + 1; 55 | } else { 56 | break; 57 | } 58 | } 59 | 60 | // 4. Determine whether the returning interpolant equals to target value. 61 | // `Result::Err` here maps to a position safe to insert while remains ordering. 62 | if *target > arr[hi] { 63 | Err(hi + 1) 64 | } else if *target < arr[lo] { 65 | Err(lo) 66 | } else { 67 | Ok(interpolant) 68 | } 69 | } 70 | 71 | #[cfg(test)] 72 | mod base { 73 | use super::*; 74 | 75 | sorted_no_duplicate_cases!(interpolation_search); 76 | } 77 | -------------------------------------------------------------------------------- /src/searching/linear_search/README.md: -------------------------------------------------------------------------------- 1 | # 線性搜尋 Linear Search 2 | 3 | 線性搜尋,又稱為循序搜尋(sequential search),是一個在序列中找尋目標的方法。正如字面上的意義,線性搜尋會按照順序疊代序列,挨家挨戶比對每個元素與目標值是否相等,若相等則停止疊代,並回傳搜尋所得結果。 4 | 5 | 線性搜尋乍看之下,是最簡單實作也最 naïve 的實作,效能應該不怎麼好。事實上,在資料量不多時(少於 100 個元素),線性搜尋的效能也不會太差,因為其他搜尋演算法可能需要建立特殊資料結構,就會導致時空間初始開銷暴增,複雜度的常數項成本變大。 6 | 7 | ## 效能 8 | 9 | | | Complexity | 10 | | ------------ | ---------- | 11 | | Worst | $O(n)$ | 12 | | Best | $O(1)$ | 13 | | Average | $O(n)$ | 14 | | Worst space | $O(1)$ | 15 | 16 | 若序列中總共有 $n$ 個元素,則線性搜尋最差的狀況為元素不在序列中,就是全部元素都比較一次,共比較 $n - 1$ 次,最差複雜度為 $O(n)$。 17 | 18 | ## 實作 19 | 20 | 線性搜尋就是用一個 for-loop 解決。要注意的是,`T` 泛型參數至少要實作 `PartialEq` 才能比較。程式碼中使用了疊代器的 [enumerate][rust-iterator-enumerate],建立一個新疊代器,每次疊代產生疊代次數與對應的值。 21 | 22 | ```rust 23 | pub fn linear_search(arr: &[T], target: &T) -> Option 24 | where T: PartialEq 25 | { 26 | for (index, item) in arr.iter().enumerate() { 27 | if item == target { 28 | return Some(index); 29 | } 30 | } 31 | None 32 | } 33 | ``` 34 | 35 | 事實上,若利用 Rust 內建的 [`iterator.position`][rust-iterator-position],程式碼也許會更簡潔。 36 | 37 | ```rust 38 | pub fn linear_search(arr: &[T], obj: &T) -> Option 39 | where T: PartialEq 40 | { 41 | arr.iter().position(|x| x == obj) 42 | } 43 | ``` 44 | 45 | [rust-iterator-enumerate]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.enumerate 46 | [rust-iterator-position]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.position 47 | 48 | ## 參考資料 49 | 50 | [Wiki: Linear search](https://en.wikipedia.org/wiki/Linear_search) 51 | -------------------------------------------------------------------------------- /src/searching/linear_search/mod.rs: -------------------------------------------------------------------------------- 1 | /// Linear search. 2 | pub fn linear_search(arr: &[T], target: &T) -> Option 3 | where 4 | T: PartialEq, 5 | { 6 | for (index, item) in arr.iter().enumerate() { 7 | if item == target { 8 | return Some(index); 9 | } 10 | } 11 | None 12 | } 13 | 14 | #[cfg(test)] 15 | mod base { 16 | use super::*; 17 | 18 | base_cases!(linear_search); 19 | } 20 | -------------------------------------------------------------------------------- /src/searching/mod.rs: -------------------------------------------------------------------------------- 1 | //! Searching algorithms. 2 | 3 | #[cfg(test)] 4 | #[macro_use] 5 | mod test_cases; 6 | 7 | mod linear_search; 8 | pub use self::linear_search::linear_search; 9 | 10 | mod binary_search; 11 | pub use self::binary_search::binary_search; 12 | 13 | mod interpolation_search; 14 | pub use self::interpolation_search::interpolation_search; 15 | 16 | mod exponential_search; 17 | pub use self::exponential_search::exponential_search; 18 | -------------------------------------------------------------------------------- /src/searching/test_cases.rs: -------------------------------------------------------------------------------- 1 | /// Test cases for searching algorithms accepting an arbitrary i32 array. 2 | macro_rules! base_cases { 3 | ($algo:ident) => { 4 | #[test] 5 | fn empty() { 6 | let arr = &[]; 7 | let target = &0; 8 | let expected = None; 9 | assert_eq!($algo(arr, target), expected); 10 | } 11 | 12 | #[test] 13 | fn one_element() { 14 | let arr = &[0]; 15 | let target = &0; 16 | let expected = Some(0); 17 | assert_eq!($algo(arr, target), expected); 18 | } 19 | 20 | #[test] 21 | fn two_elements() { 22 | let arr = &[0, 1]; 23 | let target = &0; 24 | let expected = Some(0); 25 | assert_eq!($algo(arr, target), expected); 26 | 27 | let arr = &[0, 1]; 28 | let target = &1; 29 | let expected = Some(1); 30 | assert_eq!($algo(arr, target), expected); 31 | 32 | let arr = &[1, 1]; 33 | let target = &1; 34 | let expected = Some(0); 35 | assert_eq!($algo(arr, target), expected); 36 | } 37 | 38 | #[test] 39 | fn three_elements() { 40 | let arr = &[0, 1, 2]; 41 | let target = &1; 42 | let expected = Some(1); 43 | assert_eq!($algo(arr, target), expected); 44 | 45 | let arr = &[0, 1, 2]; 46 | let target = &2; 47 | let expected = Some(2); 48 | assert_eq!($algo(arr, target), expected); 49 | } 50 | 51 | #[test] 52 | fn duplicate() { 53 | let arr = &[1, 5, 3, 3, 4]; 54 | let target = &3; 55 | let expected = Some(2); 56 | assert_eq!($algo(arr, target), expected); 57 | } 58 | 59 | #[test] 60 | fn not_found() { 61 | let arr = &[1, 2, 3]; 62 | let target = &888; 63 | let expected = None; 64 | assert_eq!($algo(arr, target), expected); 65 | } 66 | 67 | #[test] 68 | fn random() { 69 | // Similar behavior as `std::slice::binary_search`. 70 | // 71 | // Test for 72 | // 73 | // - random array lengh 74 | // - random elements 75 | // - random target 76 | use rand; 77 | 78 | for _ in 0..100 { 79 | let len = rand::random::() % 500; 80 | let mut arr: Vec<_> = (0..len).map(|_| rand::random::() % 2000).collect(); 81 | arr.sort_unstable(); 82 | arr.dedup(); 83 | 84 | (0..50).for_each(|_| { 85 | let target = rand::random::() % 4000 - 1000; 86 | assert_eq!(arr.binary_search(&target).ok(), $algo(&arr, &target),) 87 | }) 88 | } 89 | } 90 | }; 91 | } 92 | 93 | /// Test cases for binary search algorithm and its variants. 94 | /// Accepting a sorted i32 array without any duplicate elements. 95 | macro_rules! sorted_no_duplicate_cases { 96 | ($algo:ident) => { 97 | #[test] 98 | fn empty() { 99 | let arr = &[]; 100 | let target = &0; 101 | let expected = Err(0); 102 | assert_eq!($algo(arr, target), expected); 103 | } 104 | 105 | #[test] 106 | fn one_element() { 107 | let arr = &[0]; 108 | let target = &0; 109 | let expected = Ok(0); 110 | assert_eq!($algo(arr, target), expected, "0 -> Ok(0)"); 111 | 112 | let arr = &[0]; 113 | let target = &1; 114 | let expected = Err(1); 115 | assert_eq!($algo(arr, target), expected, "1 -> Err(1)"); 116 | } 117 | 118 | #[test] 119 | fn two_elements() { 120 | let arr = &[0, 2]; 121 | let target = &0; 122 | let expected = Ok(0); 123 | assert_eq!($algo(arr, target), expected); 124 | 125 | let target = &1; 126 | let expected = Err(1); 127 | assert_eq!($algo(arr, target), expected); 128 | 129 | let target = &2; 130 | let expected = Ok(1); 131 | assert_eq!($algo(arr, target), expected); 132 | } 133 | 134 | #[test] 135 | fn multiple() { 136 | let arr = &[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]; 137 | let target = &0; 138 | let expected = Ok(0); 139 | assert_eq!($algo(arr, target), expected); 140 | 141 | let target = &2; 142 | let expected = Ok(1); 143 | assert_eq!($algo(arr, target), expected); 144 | 145 | let target = &4; 146 | let expected = Ok(2); 147 | assert_eq!($algo(arr, target), expected); 148 | 149 | let target = &6; 150 | let expected = Ok(3); 151 | assert_eq!($algo(arr, target), expected); 152 | 153 | let target = &1; 154 | let expected = Err(1); 155 | assert_eq!($algo(arr, target), expected); 156 | 157 | let target = &3; 158 | let expected = Err(2); 159 | assert_eq!($algo(arr, target), expected); 160 | 161 | let target = &5; 162 | let expected = Err(3); 163 | assert_eq!($algo(arr, target), expected); 164 | 165 | let target = &7; 166 | let expected = Err(4); 167 | assert_eq!($algo(arr, target), expected); 168 | 169 | let target = &20; 170 | let expected = Err(10); 171 | assert_eq!($algo(arr, target), expected); 172 | 173 | let target = &-1; 174 | let expected = Err(0); 175 | assert_eq!($algo(arr, target), expected); 176 | } 177 | 178 | #[test] 179 | fn not_found() { 180 | let arr = &[1, 2, 3]; 181 | let target = &888; 182 | let expected = Err(3); 183 | assert_eq!($algo(arr, target), expected); 184 | } 185 | 186 | #[test] 187 | fn random() { 188 | // Same behavior as `std::slice::binary_search`. 189 | // 190 | // Test for 191 | // 192 | // - random array lengh 193 | // - random elements 194 | // - random target 195 | use rand; 196 | 197 | for _ in 0..100 { 198 | let len = rand::random::() % 500; 199 | let mut arr: Vec<_> = (0..len).map(|_| rand::random::() % 2000).collect(); 200 | arr.sort_unstable(); 201 | arr.dedup(); 202 | 203 | (0..50).for_each(|_| { 204 | let target = rand::random::() % 4000 - 1000; 205 | assert_eq!(arr.binary_search(&target), $algo(&arr, &target),) 206 | }) 207 | } 208 | } 209 | }; 210 | } 211 | -------------------------------------------------------------------------------- /src/sorting/bubble_sort/README.md: -------------------------------------------------------------------------------- 1 | # 氣泡排序 Bubble sort 2 | 3 | Bubble sort 是最簡單的排序法之一,由於排序時每個元素會如同泡泡般,一個一個浮出序列頂部,因而得名。由於其簡單好理解,名稱又有趣,常作為第一個學習的入門排序法。不過其效率不彰,甚至不如同為 quardratic time 的 insertion sort。Bubble sort 的原理很平凡,就是相鄰兩兩元素互相比較,如果大小順序錯了,就置換位置。再往下一個 pair 比較。 4 | 5 | Bubble sort 的特性如下: 6 | 7 | - 又稱為 **sinking sort**。 8 | - **穩定排序**:相同鍵值的元素,排序後相對位置不改變。 9 | - **原地排序**:不需額外花費儲存空間來排序。 10 | 11 | ## 步驟 12 | 13 | 1. 比較兩個相鄰元素,若首個元素比次個元素大,置換兩者的位置。 14 | 2. 依序對相鄰元素執行步驟一,直到抵達序列頂端,此時頂端元素排序完成。 15 | 3. 重複步驟 1 - 2 的整個序列疊代,直到任何一次疊代沒有執行元素置換。 16 | 17 | ![](https://upload.wikimedia.org/wikipedia/commons/c/c8/Bubble-sort-example-300px.gif) 18 | _Swfung8 - CC BY-SA 3.0_ 19 | 20 | ## 說明 21 | 22 | 給定一組序列 `[5, 3, 8, 7, 2]`,以 bubble sort 遞增排序。以 ASCII diagram 表示: 23 | 24 | **第一次疊代** 25 | 26 | ```bash 27 | * * * * 28 | [5, 3, 8, 7, 4] -> [3, 5, 8, 7, 4] # 置換 3 與 5 29 | 30 | * * * * 31 | [3, 5, 8, 7, 4] -> [3, 5, 8, 7, 4] # 不需置換 32 | 33 | * * * * 34 | [3, 5, 8, 7, 4] -> [3, 5, 7, 8, 4] # 置換 7 與 8 35 | 36 | * * * * 37 | [3, 5, 7, 8, 4] -> [3, 5, 7, 4, 8] # 置換 4 與 8,8 已排好序 38 | ``` 39 | 40 | **第二次疊代** 41 | 42 | ```bash 43 | * * * * 44 | [3, 5, 7, 4, 8] -> [3, 5, 7, 4, 8] # 不需置換 45 | 46 | * * * * 47 | [3, 5, 7, 4, 8] -> [3, 5, 7, 4, 8] # 不需置換 48 | 49 | * * * * 50 | [3, 5, 7, 4, 8] -> [3, 5, 4, 7, 8] # 置換 4 與 7 51 | 52 | * * * * 53 | [3, 5, 4, 7, 8] -> [3, 5, 4, 7, 8] # 不需置換 54 | ``` 55 | > naïve bubble sort 會跑完整個序列,即是已排序完成。 56 | 57 | **第三次疊代** 58 | 59 | ```bash 60 | * * * * 61 | [3, 5, 4, 7, 8] -> [3, 5, 4, 7, 8] # 不需置換 62 | 63 | * * * * 64 | [3, 5, 4, 7, 8] -> [3, 4, 5, 7, 8] # 置換 4 與 5 65 | 66 | * * * * 67 | [3, 5, 4, 7, 8] -> [3, 4, 5, 7, 8] # 不需置換 68 | 69 | * * * * 70 | [3, 5, 4, 7, 8] -> [3, 4, 5, 7, 8] # 不需置換 71 | ``` 72 | 73 | **第四次疊代** 74 | 75 | ```bash 76 | * * * * 77 | [3, 4, 5, 7, 8] -> [3, 4, 5, 7, 8] # 不需置換 78 | 79 | * * * * 80 | [3, 4, 5, 7, 8] -> [3, 4, 5, 7, 8] # 不需置換 81 | 82 | * * * * 83 | [3, 4, 5, 7, 8] -> [3, 4, 5, 7, 8] # 不需置換 84 | 85 | * * * * 86 | [3, 4, 5, 7, 8] -> [3, 4, 5, 7, 8] # 不需置換 87 | ``` 88 | 89 | 很簡單的排序法! 90 | 91 | ## 效能 92 | 93 | | | Complexity | 94 | | ------------ | ------------- | 95 | | Worst | $O(n^2) $ | 96 | | Best | $O(n) $ | 97 | | Average | $O(n^2) $ | 98 | | Worst space | $O(1) $ auxiliary | 99 | 100 | ### Time complexity 101 | 102 | Bubble sort 總共需要 $n - 1 $ 次疊代,每次疊代至少需要執行 $n - 1 - i $ 置換( $i $ 為第幾次疊代),總共需要疊代 103 | 104 | $$\sum_{i=0}^{n-1} (n - i - 1) = n^2 - \sum_{i=0}^{n-1}i - n = n^2 - \frac{n(n - 1)}{2} - n = \frac{n^2}{2} - \frac{n}{2}$$ 105 | 106 | 次,因此,時間複雜度為 $O(n^2) $。 107 | 108 | Bubble sort 在已排序完成的序列上,只需要疊代序列一次,發現完全沒有置換任何元素,即停止排序,可達到最佳時間複雜度。 109 | 110 | ## 實作 111 | 112 | Bubble sort 簡單實作如下: 113 | 114 | ```rust 115 | pub fn bubble_sort(arr: &mut [i32]) { 116 | let mut swapped = true; // 1 117 | while swapped { 118 | swapped = false; 119 | for i in 1..arr.len() { // 2 120 | if arr[i - 1] > arr[i] { 121 | arr.swap(i - 1, i); 122 | swapped = true // 3 123 | } 124 | } 125 | } 126 | } 127 | ``` 128 | 129 | 1. 建立一個旗標,標誌該次疊代是否有元素置換。 130 | 2. 內層迴圈依序比較兩兩相鄰元素。 131 | 3. 若有任何置換動作,將旗標標誌為「已置換(`true`)」。 132 | 133 | 倘若記錄已排好序的元素位置,雖然複雜度仍是 $O(n^2) $,但如此以來,每次疊代都可少一次元素比較,對比較操作成本高的語言或實作來說,仍不失為最佳化的方法。程式碼如下: 134 | 135 | ```rust 136 | pub fn bubble_sort_optimized(arr: &mut [i32]) { 137 | let mut new_len: usize; 138 | let mut len = arr.len(); // 1 139 | loop { 140 | new_len = 0; 141 | for i in 1..len { 142 | if arr[i - 1] > arr[i] { 143 | arr.swap(i - 1, i); 144 | new_len = i; // 2 145 | } 146 | } 147 | if new_len == 0 { // 3 148 | break; 149 | } 150 | len = new_len; // 4 151 | } 152 | } 153 | ``` 154 | 155 | 1. 將當前的序列長度記錄到 `len`。 156 | 2. 內層迴圈負責比較、置換,以及記錄未排序部分的序列長度到 `new_len`。 157 | 3. 若未排序部分 `new_len` 為零,代表排序完成。 158 | 4. 外層迴圈將新長度值 `new_len` 賦予 `len`,下一次疊代就可少做一次比較。 159 | 160 | ## 參考資料 161 | 162 | - [Wiki: Bubble sort](https://en.wikipedia.org/wiki/Bubble_sort) 163 | - Sorting GIF was created by Swfung8 (Own work) [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0) via Wikimedia Commons. 164 | -------------------------------------------------------------------------------- /src/sorting/bubble_sort/mod.rs: -------------------------------------------------------------------------------- 1 | /// Bubble sort 2 | pub fn bubble_sort(arr: &mut [i32]) { 3 | let mut swapped = true; 4 | while swapped { 5 | // No swap means array is sorted. 6 | swapped = false; 7 | for i in 1..arr.len() { 8 | if arr[i - 1] > arr[i] { 9 | arr.swap(i - 1, i); 10 | swapped = true 11 | } 12 | } 13 | } 14 | } 15 | 16 | /// Optimized bubble sort 17 | /// 18 | /// Memorize last swapped index to avoid unnecessary check. 19 | pub fn bubble_sort_optimized(arr: &mut [i32]) { 20 | let mut new_len: usize; 21 | let mut len = arr.len(); 22 | loop { 23 | new_len = 0; 24 | for i in 1..len { 25 | if arr[i - 1] > arr[i] { 26 | arr.swap(i - 1, i); 27 | new_len = i; 28 | } 29 | } 30 | if new_len == 0 { 31 | break; 32 | } 33 | len = new_len; 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod base { 39 | use super::*; 40 | base_cases!(bubble_sort); 41 | } 42 | 43 | #[cfg(test)] 44 | mod optimized { 45 | use super::*; 46 | base_cases!(bubble_sort_optimized); 47 | } 48 | -------------------------------------------------------------------------------- /src/sorting/bucket_sort/README.md: -------------------------------------------------------------------------------- 1 | # 桶排序 Bucket sort 2 | 3 | [Bucket sort][wiki-bucket-sort],是一個非比較排序。原理是建立一些桶子,每個桶子對應一資料區間,在將待排序資料分配到不同的桶中,桶子內部各自排序。由於並非[比較排序][wiki-comparison-sort],使用 Bucket sort 需要事先知道資料的範圍與分佈,才能決定桶子對應的區間。 4 | 5 | Bucket sort 基本特性如下: 6 | 7 | - 又稱 **bin sort**。 8 | - **穩定排序**:相同鍵值的元素,排序後相對位置不改變。 9 | - **分配式排序**:不透過兩兩比較,而是分析鍵值分佈來排序。特定情況下可達線性執行時間。 10 | - **預期分佈**:資料為**均勻分佈**。 11 | 12 | ## 步驟 13 | 14 | 假設要排序 $n $ 個元素的陣列,這些元素的值平均散落在某個**已知的預期範圍內**,例如 1 到 100。 15 | 16 | 1. **Create buckets**:建立 $k $ 個桶子(bucket)的陣列。每個桶子**對應預期範圍的某區間**,如第一個桶子放 1 到 10,第二個放 11 到 20。 17 | 2. **Scatter**:將每個元素依照該值放入對應的桶子中。 18 | 3. **Inner sort**:排序所有非空的桶子。 19 | 4. **Gather**:依序走訪所有桶子,將桶內的元素放回原本的陣列中。 20 | 21 | ## 說明 22 | 23 | 以下用 ASCII diagram 視覺化解釋: 24 | 25 | 這裡有一些整數,落在 1 至 100 之間。我們有 $n = 10 $ 的陣列要排序。 26 | 27 | ``` 28 | Original array 29 | 30 | +-------------------------------------------------+ 31 | | 6 | 28 | 96 | 14 | 74 | 37 | 9 | 71 | 91 | 36 | 32 | +-------------------------------------------------+ 33 | ``` 34 | 35 | **1. Create buckets**:建立一定數量的桶子,這裡我們建立與原始陣列相同數量的桶子(10)。每個桶子對應 $n - 1 * 10 $ 到 $n * 10 $ 的區間。 36 | 37 | ``` 38 | Bucket array 39 | 40 | +-------------------------------------------------+ 41 | | | | | | | | | | | | 42 | +-------------------------------------------------+ 43 | ^ ^ 44 | | | 45 | | | 46 | | holds values in range 11 to 20 47 | holds values in range 1 to 10 48 | ``` 49 | 50 | **2. Scatter**:將原始陣列中的元素,放入對應的桶中。 51 | 52 | ``` 53 | Bucket array 54 | 55 | 6,9 14 28 37,36 74,71 96,91 56 | | | | | | | 57 | +-v----v----v----v-------------------v---------v--+ 58 | | | | | | | | | | | | 59 | +-------------------------------------------------+ 60 | ``` 61 | 62 | **3. Inner sort**:排序所有非空桶子中的元素,桶內排序可用任意排序法,通常選用「insertion sort」,可確保排序穩定性,並降低額外開銷。 63 | 64 | ``` 65 | Bucket array 66 | 67 | sort sort sort sort sort sort 68 | --- -- -- ----- ----- ----- 69 | 6,9 14 28 36,37 71,74 91,96 70 | | | | | | | 71 | +-v----v----v----v-------------------v---------v--+ 72 | | | | | | | | | | | | 73 | +-------------------------------------------------+ 74 | ``` 75 | 76 | **4. Gather**:排序完後,再將所有桶中元素依序放回原始的陣列。 77 | ``` 78 | Original array 79 | +-------------------------------------------------+ 80 | | 6 | 9 | 14 | 28 | 36 | 37 | 71 | 74 | 91 | 96 | 81 | +-------------------------------------------------+ 82 | ``` 83 | 84 | ## 效能 85 | 86 | | | Complexity | 87 | | ------------ | --------------- | 88 | | Worst | $O(n^2) $ | 89 | | Best | $O(n + k) $ | 90 | | Average | $O(n + k) $ | 91 | | Worst space | $O(n + k) $ auxiliary | 92 | 93 | > $k $ = 桶子的數量(number of buckets) 94 | > $n $ = 資料筆數 95 | 96 | ### Worst case 97 | 98 | Bucket sort 是一個分配式排序法,對資料分佈有既定的預期:「**所有元素平均分佈在每個 bucket 的區間內**」。可想而知,最差的狀況是所有元素都聚集(clustering)在同一個 bucket 中,整個 bucket sort 的會退化成單一一個 inner sort 的複雜度。而桶內排序通常選用 insertion sort(最差 $O(n^2) $),所以最差的時間複雜度為「 $O(n^2) $」。 99 | 100 | ### Best case 101 | 102 | 最佳的狀況則是完全符合預期的平均分佈,一個蘿蔔一個坑,每個桶內排序的最佳時間複雜度為 $O(n / k) $,再乘上桶子總數 $k $,僅需 $O(k \cdot (n / k)) = O(n) $。計算結果看起來非常合理,但實際上最佳時間複雜度為 $O(n + k) $,為什麼呢? 103 | 104 | 無庸置疑,桶內排序最佳時間複雜度為 $O(n / k) $,但別忘了這是省略常數項過後式子,進行符號運算時,較精確的表達是 $c_0 O(n / k) + c_1 $,對於實作層面的常數 $c_0 $ 和 $c_1 $ 則予以保留。 105 | 106 | 當我們乘上 $k $,試著算出總運算量時, 107 | 108 | $$k \cdot (c_0(n / k) + c_1) $$ 109 | 110 | 會得到: 111 | 112 | $$ c_0n + c_1k $$ 113 | 114 | 可以得知,整個計算與 $k $ 有關,所以需要耗時 $O(n + k) $。 115 | 116 | 撇開數學,我們從 pseudo code 來看。最佳情況下,將所有元素蒐集回陣列的步驟(Gather)如下: 117 | 118 | ``` 119 | for (each bucket b in all k buckets) 120 | for (each element x in b) 121 | append x to the array 122 | ``` 123 | 124 | 最外層的迴圈依桶子數 $k $ 而定,至少需要執行 $k $ 次,複雜度為 $O(k) $。內層的迴圈則是每個桶內的元素都會執行,而我們的資料時均勻分布,因此執行時間與元素總數 $n $ 相關,為 $O(n) $。兩者加起來就是我們所說的 $O(n + k) $ 的最佳複雜度。 125 | 126 | **那 $k $ 究竟會是多少,影響會比 $n $ 大嗎?** 127 | 128 | 端看桶子總數而定,若桶子總數很大,比元素個數 $n $ 大得多,則桶子總數對執行時間的影響恐較劇烈,就算大多數為空桶子,仍須挨家挨戶查看是否需要執行桶內排序。 129 | 130 | ### Space Complexity 131 | 132 | Bucket sort 須額外建立 $k $ 個桶子,每個桶子需要配置長度為 $n $ 的 array,因此空間複雜度為 $O(n \cdot k) $。如果以 dynamic array 實作 bucket,並考慮平攤分析(Amortized analysis),則空間複雜度降至 $O(n + k) $,這也是大多數人接受的分析結果,畢竟不會有人無聊到預先配置 $n \cdot k $ 個 empty bucket。 133 | 134 | ## 實作 135 | 136 | ### Bucket 137 | 138 | Bucket sort 有許多種各異的實作法,差異最大之處就是桶子 bucket 這部分。 139 | 140 | ```rust 141 | /// Bucket to store elements. 142 | struct Bucket { 143 | hash: H, 144 | values: Vec, 145 | } 146 | 147 | impl Bucket { 148 | /// Create a new bucket and insert its first value. 149 | /// 150 | /// * `hash` - Hash value generated by hasher param of `bucket_sort`. 151 | /// * `value` - Value to be put in the bucket. 152 | pub fn new(hash: H, value: T) -> Bucket { 153 | Bucket { 154 | hash: hash, 155 | values: vec![value], 156 | } 157 | } 158 | } 159 | ``` 160 | 161 | 這裡的桶子實作兩個 struct fields: 162 | 163 | - `values`:使用 [`Vec`][vec] 儲存對應範圍內的元素 164 | - `hash`:Bucket Sort 主函式有一個 `hasher` 函式,會計算出對應各個桶子的雜湊值,因此要確保桶子的雜湊值有唯一性。 165 | 166 | [vec]: https://doc.rust-lang.org/stable/std/vec/struct.Vec.html 167 | 168 | ### Sorting 169 | 170 | 接下來就是排序主函式。依照慣例,先看看函式的宣告(function signature)。 171 | 172 | ```rust 173 | pub fn bucket_sort(arr: &mut [T], hasher: F) 174 | where H: Ord, 175 | F: Fn(&T) -> H, 176 | T: Ord + Clone, 177 | ``` 178 | 179 | 這個 `bucket_sort` 函式使用了不少泛型: 180 | 181 | - `H`:`hasher` 函式的回傳型別,用來辨識不同的桶子。 182 | - `F`:`hasher` 函式自身,只需要一個參數 `&T`,回傳一個 `H`。 183 | - `T`:欲排序資料的型別。 184 | 185 | 函式自身稍微複雜一點,但仍不脫離[四步驟](#步驟):Create buckets、Scatter、Inner sort,還有 Gather。 186 | 187 | ```rust 188 | pub fn bucket_sort() { 189 | // ... 190 | 191 | // 1. Create buckets. 192 | let mut buckets: Vec> = Vec::new(); 193 | 194 | // 2. Scatter 195 | for value in arr.iter() { 196 | let hash = hasher(&value); // 2.1. 197 | 198 | let value = value.clone(); 199 | // 2.2. 200 | match buckets.binary_search_by(|bucket| bucket.hash.cmp(&hash)) { 201 | // If exists, push the value to the bucket. 202 | Ok(index) => buckets[index].values.push(value), 203 | // If none, create and new bucket and insert value in. 204 | Err(index) => buckets.insert(index, Bucket::new(hash, value)), 205 | } 206 | } 207 | 208 | // 3. Inner sort and gather 209 | let ret = buckets.into_iter().flat_map(|mut bucket| { 210 | bucket.values.sort(); // 3.1. 211 | bucket.values 212 | }).collect::>(); // 3.2. 213 | 214 | arr.clone_from_slice(&ret); // 4 Copy to original array 215 | } 216 | ``` 217 | 218 | 1. 一般來說,第一步會配置完所有桶子,但這裡實作僅建立儲存桶子們的容器 `buckets`,這是由於實作了 `hasher` 函式,元素對應桶子的邏輯交由外部決定,因此桶子不需事先配置,而是交給第二步驟時 **on-the-fly** 建立。 219 | 2. 疊代輸入的 `arr`,將元素散佈到桶子中。 220 | 1. 使用元素值 `value` 取得雜湊值。 221 | 2. 從一堆桶子內 `buckets` 尋找對應雜湊值的桶子,如有對應桶子,則將待排序元素插入桶中;若無對應桶子,則馬上建立桶子,並插入待排序元素。 222 | 3. 由於桶子們 `buckets` 是一個二維陣列集合,我們使用 `flat_map` 將之壓平。 223 | 1. 使用 Rust 內建 sort(Timsort 的變形)作為我們 inner sort 的實作,將桶內所有元素排好序 224 | 2. 別忘了 Rust 的 Iterator 很 lazy,記得要使用 `collect` 蒐集 iterator 實作後的結果。 225 | 4. 由於要模擬 in-place 原地排序法的特性,將排序好的資料再次拷貝到 `arr` 上。這也是為什麼函式元素泛型 `T` 需要 `Clone` trait 的原因了。 226 | 227 | 有關於步驟 2.2.,這部分可以用 `HashMap` 的變形 [IndexMap][index-map](一個保存插入順序的有序 HashMap)保存雜湊值對應桶子的資訊,使得外界更容易依雜湊值找到桶子。但為了保持範例程式的簡潔,決定不引入第三方的 crate(Rust 語言第三方模組的代稱),且 `binary_search_by` 的複雜度為 $O(\log n) $,對 Bucket sort 最差複雜度並無影響。 228 | 229 | [index-map]: https://github.com/bluss/indexmap 230 | 231 | ## 參考資料 232 | 233 | - [Wiki: Bucket sort][wiki-bucket-sort] 234 | - [Wiki: Amortized analysis][wiki-amortized-analysis] 235 | - [How is the complexity of bucket sort is O(n+k) if we implement buckets using linked lists?][stackoverflow-bucket-sort-analysis] 236 | - [Bucket sort in Rust][bucket-sort-in-rust] 237 | 238 | [wiki-bucket-sort]: https://en.wikipedia.org/wiki/Bucket_sort 239 | [wiki-amortized-analysis]: https://en.wikipedia.org/wiki/Amortized_analysis 240 | [wiki-comparison-sort]: https://en.wikipedia.org/wiki/Comparison_sort 241 | [stackoverflow-bucket-sort-analysis]: https://stackoverflow.com/questions/7311415 242 | [bucket-sort-in-rust]: https://codereview.stackexchange.com/questions/145113/bucket-sort-in-rust 243 | -------------------------------------------------------------------------------- /src/sorting/bucket_sort/mod.rs: -------------------------------------------------------------------------------- 1 | /// Bucket sort 2 | /// 3 | /// * `arr` - Collection of value to be sorted in place. 4 | /// * `hasher` - Function hashing to map elements to correspoding buckets. 5 | /// 6 | /// Ref: https://codereview.stackexchange.com/a/145124 7 | pub fn bucket_sort(arr: &mut [T], hasher: F) 8 | where 9 | H: Ord, 10 | F: Fn(&T) -> H, 11 | T: Ord + Clone, 12 | { 13 | // 1. Create buckets. 14 | let mut buckets: Vec> = Vec::new(); 15 | 16 | // 2. Iterate all elements. 17 | for value in arr.iter() { 18 | // 2.1 Create hasher mapping to certain bucket. 19 | let hash = hasher(&value); 20 | 21 | // 2.2 Search if the bucket with same hash exists. 22 | let value = value.clone(); 23 | match buckets.binary_search_by(|bucket| bucket.hash.cmp(&hash)) { 24 | // If exists, push the value to the bucket. 25 | Ok(index) => buckets[index].values.push(value), 26 | // If none, create and new bucket and insert value in. 27 | Err(index) => buckets.insert(index, Bucket::new(hash, value)), 28 | } 29 | } 30 | 31 | // 3. Iterate all buckets and flatten their internal collections. 32 | let ret = buckets 33 | .into_iter() 34 | .flat_map(|mut bucket| { 35 | bucket.values.sort(); // We use built-in sorting here. 36 | bucket.values 37 | }) 38 | .collect::>(); 39 | 40 | // 4. Clone back to original array. 41 | arr.clone_from_slice(&ret); 42 | } 43 | 44 | /// Bucket to store elements. 45 | struct Bucket { 46 | hash: H, 47 | values: Vec, 48 | } 49 | 50 | impl Bucket { 51 | /// Create a new bucket and insert its first value. 52 | /// 53 | /// * `hash` - Hash value generated by hasher param of `bucket_sort`. 54 | /// * `value` - Value to be put in the bucket. 55 | pub fn new(hash: H, value: T) -> Bucket { 56 | Bucket { 57 | hash, 58 | values: vec![value], 59 | } 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod base { 65 | use super::*; 66 | fn bucket_sort_(arr: &mut [i32]) { 67 | bucket_sort(arr, |int| int / 4); 68 | } 69 | base_cases!(bucket_sort_); 70 | } 71 | 72 | #[cfg(test)] 73 | mod stability { 74 | use super::*; 75 | fn bucket_sort_(arr: &mut [(i32, i32)]) { 76 | bucket_sort(arr, |t| t.0 / 4); 77 | } 78 | stability_cases!(bucket_sort_); 79 | } 80 | -------------------------------------------------------------------------------- /src/sorting/counting_sort/README.md: -------------------------------------------------------------------------------- 1 | # 計數排序 Counting sort 2 | 3 | [Counting sort][wiki-counting-sort] 是一個特殊的整數排序法,被視為 [Bucket sort](../bucket_sort) 的特例。原理是在已知整數範圍內,計算每個鍵值出現次數,並用額外的陣列保存(Count array)。最後將 Count array 的元素值作為排序資料的新 index。 4 | 5 | Counting sort 基本特性如下: 6 | 7 | - **非原地排序**:額外花費較大量、非固定的空間來排序。 8 | - **穩定排序**:相同鍵值的元素,排序後相對位置不改變。 9 | - **整數排序**:以整數作為排序的鍵值。 10 | - **分配式排序**:不透過兩兩比較,而是分析鍵值分佈來排序。特定情況下可達線性執行時間。 11 | - **線型執行時間**:當輸入資料量 **n** 與已知範圍上下界之差值相近,執行時間接近線型(**O(n)**) 12 | - **預期分佈**:預期輸入資料是落在已知範圍內的整數(例如 0 到 k)。 13 | - **適用範圍**:僅適用於小範圍整數(額外空間需求大)。 14 | 15 | ## 步驟 16 | 17 | 1. **Count occurrence**:計算每個 key 的出現次數。 18 | 2. **Prefix sum as start index**:計算前綴和(Prefix sum),並作為該元素的 start index。 19 | 3. **Copy output**:利用步驟二的前綴和,遍歷輸入資料,取得元素排序後的索引。 20 | 21 | ## 說明 22 | 23 | 這裡有資料需要透過正整數的 key 來排序。key 的範圍在 0 - 9 之間,格式為 `(key, value)`。 24 | 25 | ``` 26 | Input: (1, A) (5, B) (8, C) (2, D) (2, E) (9, F) 27 | ``` 28 | 29 | **1. Count occurrence**:首先,先計算每個 key 的出現頻率,儲存在額外的 count array 中。 30 | 31 | ``` 32 | Key : 0 1 2 3 4 5 6 7 8 9 33 | Count: 0 1 2 0 0 1 0 0 1 1 34 | ``` 35 | 36 | **2. Prefix sum as start index**:再計算 prefix sum,也就是將當前 index 前累計的 key 數量加總。例如 **key 5** 的 prefix sum **1 + 2 = 3**。 37 | 38 | 這裡的 prefix sum 等同於每筆資料排序後的位置(index)。例如排序後,**8** 位於陣列第四位。 39 | 40 | ``` 41 | Key : 0 1 2 3 4 5 6 7 8 9 42 | Prefix Sum: 0 0 1 3 3 3 4 4 4 5 43 | ``` 44 | 45 | **3. Copy output**:透過 key 與 prefix sum 的映射關係,找到原始資料對應的位置。 46 | 47 | 實作上,每筆資料找到對應的 start index(prefix sum) 後,要將**該 index 之值 +1**,使得重複的元素可取得正確的 index offset(對唯一的 key 沒有影響)。 48 | 49 | ``` 50 | (1, A) 51 | --> prefix sum 為 0,寫入 array[0],並將 prefix sum + 1 52 | 53 | +--------+--------+--------+--------+--------+--------+ 54 | | (1, A) | | | | | | 55 | +--------+--------+--------+--------+--------+--------+ 56 | 57 | (5, B) 58 | --> prefix sum 為 3,寫入 array[3],並將 prefix sum + 1 59 | 60 | +--------+--------+--------+--------+--------+--------+ 61 | | (1, A) | | | (5, B) | | | 62 | +--------+--------+--------+--------+--------+--------+ 63 | 64 | (8, C) 65 | --> prefix sum 為 4,寫入 array[4],並將 prefix sum + 1 66 | 67 | +--------+--------+--------+--------+--------+--------+ 68 | | (1, A) | | | (5, B) | (8, C) | | 69 | +--------+--------+--------+--------+--------+--------+ 70 | 71 | (2, D) 72 | --> prefix sum 為 1,寫入 array[1],並將 prefix sum + 1 73 | 74 | +--------+--------+--------+--------+--------+--------+ 75 | | (1, A) | (2, D) | | (5, B) | (8, C) | | 76 | +--------+--------+--------+--------+--------+--------+ 77 | 78 | (2, E) 79 | --> prefix sum 為 2(前一步驟 + 1),寫入 array[2],並將 prefix sum + 1 80 | 81 | +--------+--------+--------+--------+--------+--------+ 82 | | (1, A) | (2, D) | (2, E) | (5, B) | (8, C) | | 83 | +--------+--------+--------+--------+--------+--------+ 84 | 85 | (9, F) 86 | --> prefix sum 為 5,寫入 array[5],並將 prefix sum + 1 87 | 88 | +--------+--------+--------+--------+--------+--------+ 89 | | (1, A) | (2, D) | (2, E) | (5, B) | (8, C) | (9, F) | 90 | +--------+--------+--------+--------+--------+--------+ 91 | ``` 92 | 93 | 這樣就完成排序了。此外,觀察 **(2, D)** 與 **(2, E)** 排序前後的位置,會發現 counting sort 是個實實在在的穩定排序,很棒。 94 | 95 | ## 效能 96 | 97 | | | Complexity | 98 | | ------------ | --------------- | 99 | | Worst | $O(n + k) $ | 100 | | Best | $O(n + k) $ | 101 | | Average | $O(n + k) $ | 102 | | Worst space | $O(n + k) $ auxiliary | 103 | 104 | > k 為資料已知範圍上下界之差。 105 | 106 | ### Time Complexity 107 | 108 | Counting sort 沒有用到任何遞迴,可以直觀地分析複雜度。在步驟一,建立 count array 與步驟三輸出排序結果,都需要遍歷 $n $ 個輸入的資料,因此複雜度為 $O(n) $;步驟二計算 prefix sum,以及 count array 自身的初始化則需執行 $k + 1 $ 次(給定的資料範圍),這部分的複雜度為 $O(k) $。由於 $n $ 與 $k $ 的權重會因輸入資料及實作的不同而有所改變,我們無法捨棄任何一個因子,可得知 counting sort 的複雜度為 $O(n + k) $。 109 | 110 | ### Space complexity 111 | 112 | Counting sort 並非 in-place sort,排序後的結果會另外輸出為新的記憶體空間,因此 $O(n) $ 的額外(auxiliary)空間複雜度絕對免不了。再加上需要長度為 $k $ 的 count array 保存每個 key 的出現次數,因此需再加上 $O(k) $。除了原始的輸入 array,總共需花費 $O(n + k) $ 的額外空間複雜度。 113 | 114 | > 如果欲排序資料就是整數鍵值自身,可以將「計算前綴和」與「複製輸出」兩步驟最佳化,直接覆寫原始陣列,額外空間複雜度會下降至 $O(k) $,但也因此成為不穩定排序法。 115 | 116 | ## 實作 117 | 118 | 由於 Counting sort 屬於分布式排序(Distribution sort),這裡使用泛型,以彰顯分布式排序的特色。 119 | 120 | ### Function Signature 121 | 122 | 首先,我們先看函式如何宣告(function signature)。 123 | 124 | ```rust 125 | pub fn counting_sort(arr: &mut [T], min: usize, max: usize, key: F) 126 | where F: Fn(&T) -> usize, 127 | T: Clone, 128 | ``` 129 | 130 | 這裡使用了四個參數: 131 | 132 | - `arr`:待排序陣列。 133 | - `min`、`max`:整數排序的上下界。 134 | - `key`:由於資料不一定是整數,需要一個 function 從資料擷取鍵值做排序。 135 | 136 | 另外,也使用兩個泛型型別: 137 | 138 | - `F`:`key` extactor 的型別,回傳的 `usize` 必須落在 `[min, max)` 之間。 139 | - `T`:陣列元素的型別,實作 `Clone` 是由於 Counting sort 需要將 output 再複製回原本的參數 `arr` 上,達成「偽」原地排序。 140 | 141 | ### Prefix Sums Array 142 | 143 | 再來,了解如何建立一個元素出現次數的陣列。 144 | 145 | ```rust 146 | fn counting_sort() { 147 | // ... 148 | 149 | let mut prefix_sums = { 150 | // 1. Initialize the count array with default value 0. 151 | let len = max - min; 152 | let mut count_arr = Vec::with_capacity(len); 153 | count_arr.resize(len, 0); 154 | 155 | // 2. Scan elements to collect counts. 156 | for value in arr.iter() { 157 | count_arr[key(value)] += 1; 158 | } 159 | 160 | // 3. Calculate prefix sum. 161 | count_arr.into_iter().scan(0, |state, x| { 162 | *state += x; 163 | Some(*state - x) 164 | }).collect::>() 165 | }; 166 | // ... 167 | } 168 | ``` 169 | 170 | 1. 建立一個長度為上下界之差的 count array。注意,這裡使用了 `Vec.resize`,因為 Rust initialize 空的 `Vec` 時並不會插入 0 或其他預設值。 171 | 2. 遍歷整個輸入資料,利用 `key` function 取出每筆資料的鍵值,出現一次就 +1。 172 | 3. 利用 Iterator 上的 `scan` method 計算每個鍵值的 prefix sum。需要注意的是,每個元素對應的 prefix sum 不包含自身,例如 key 3 的計算結果就是 key 1 與 key 2 的出現總次數,如此一來,prefix sum 才會直接對應到排序後的位置。 173 | 174 | ### Prefix Sums as Start Index 175 | 176 | 最後一步就是將 prefix sum 當作每個 element 的正確位置,把資料重頭排序。 177 | 178 | ```rust 179 | fn counting_sort() { 180 | // ... 181 | 182 | for value in arr.to_vec().iter() { // 1 183 | let index = key(value); 184 | arr[prefix_sums[index]] = value.clone(); // 2 185 | prefix_sums[index] += 1; // 3 186 | } 187 | } 188 | ``` 189 | 190 | 1. 將輸入資料透過 `to_vec` 複製起來疊代,需要複製 `arr` 是因為之後要直接在 `arr` 插入新值,需要另一份原始輸入的拷貝。 191 | 2. 利用 `key` 擷取鍵值後,把資料複製給 `arra` 上對應 `prefix_sums[index]` 的位置。 192 | 3. 將該 `prefix_sums[index]` 的值加一,以便元素重複時,可以正常複製到下一個位置。 193 | 194 | 完成了!這裡再貼一次完整的程式碼。 195 | 196 | 197 | ```rust 198 | pub fn counting_sort(arr: &mut [T], min: usize, max: usize, key: F) 199 | where F: Fn(&T) -> usize, 200 | T: Clone, 201 | { 202 | let mut prefix_sums = { 203 | // 1. Initialize the count array with default value 0. 204 | let len = max - min; 205 | let mut count_arr = Vec::with_capacity(len); 206 | count_arr.resize(len, 0); 207 | 208 | // 2. Scan elements to collect counts. 209 | for value in arr.iter() { 210 | count_arr[key(value)] += 1; 211 | } 212 | 213 | // 3. Calculate prefix sum. 214 | count_arr.into_iter().scan(0, |state, x| { 215 | *state += x; 216 | Some(*state - x) 217 | }).collect::>() 218 | }; 219 | 220 | // 4. Use prefix sum as index position of output element. 221 | for value in arr.to_vec().iter() { 222 | let index = key(value); 223 | arr[prefix_sums[index]] = value.clone(); 224 | prefix_sums[index] += 1; 225 | } 226 | } 227 | ``` 228 | 229 | ## 參考資料 230 | 231 | - [Wiki: Counting sort][wiki-counting-sort] 232 | - [Growing with the web: Counting sort][growingwiththeweb] 233 | 234 | [wiki-counting-sort]: https://en.wikipedia.org/wiki/Counting_sort 235 | [growingwiththeweb]: http://www.growingwiththeweb.com/2014/05/counting-sort.html 236 | -------------------------------------------------------------------------------- /src/sorting/counting_sort/mod.rs: -------------------------------------------------------------------------------- 1 | /// Counting sort 2 | /// 3 | /// * `arr` - Collection of value to be sorted in place. 4 | /// * `min` - Lower bound of the integer range. 5 | /// * `max` - Upper bound of the integer range. 6 | /// * `key` - Function extracting key witn the integer range from elements. 7 | pub fn counting_sort(arr: &mut [T], min: usize, max: usize, key: F) 8 | where 9 | F: Fn(&T) -> usize, 10 | T: Clone, 11 | { 12 | let mut prefix_sums = { 13 | // 1. Initialize the count array with default value 0. 14 | let len = max - min; 15 | let mut count_arr = Vec::with_capacity(len); 16 | count_arr.resize(len, 0); 17 | 18 | // 2. Scan elements to collect counts. 19 | for value in arr.iter() { 20 | count_arr[key(value)] += 1; 21 | } 22 | 23 | // 3. Calculate prefix sum. 24 | count_arr 25 | .into_iter() 26 | .scan(0, |state, x| { 27 | *state += x; 28 | Some(*state - x) 29 | }) 30 | .collect::>() 31 | }; 32 | 33 | // 4. Use prefix sum as index position of output element. 34 | for value in arr.to_vec().iter() { 35 | let index = key(value); 36 | arr[prefix_sums[index]] = value.clone(); 37 | prefix_sums[index] += 1; 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod base { 43 | use super::*; 44 | fn counting_sort_(arr: &mut [i32]) { 45 | counting_sort(arr, 1, 10, |int| *int as usize); 46 | } 47 | base_cases!(counting_sort_); 48 | } 49 | 50 | #[cfg(test)] 51 | mod stability { 52 | use super::*; 53 | fn counting_sort_(arr: &mut [(i32, i32)]) { 54 | counting_sort(arr, 1, 10, |t| t.0 as usize); 55 | } 56 | stability_cases!(counting_sort_); 57 | } 58 | -------------------------------------------------------------------------------- /src/sorting/heapsort/README.md: -------------------------------------------------------------------------------- 1 | # 堆積排序 Heapsort 2 | 3 | Heapsort(堆積排序)可以看作是 [selection sort][selection-sort] 的變形,同樣會將資料分為 sorted pile 與 unsorted pile,並在 unsorted pile 中尋找最大值(或最小值),加入 sorted pile 中。 4 | 5 | 和 selection sort 不同之處是,heapsort 利用[堆積(heap)][wiki-heap]這種半排序(partially sorted)的資料結構輔助並加速排序。 6 | 7 | Heapsort 的特性如下: 8 | 9 | - 使用 [heap][wiki-heap] 資料結構輔助,通常使用 [binary heap][wiki-binary-heap]。 10 | - **不穩定排序**:排序後,相同鍵值的元素相對位置可能改變。 11 | - **原地排序**:不需額外花費儲存空間來排序。 12 | - **較差的 CPU 快取**:heap 不連續存取位址的特性,不利於 [CPU 快取][wiki-cpu-cache]。 13 | 14 | [selection-sort]: ../selection_sort 15 | [wiki-binary-heap]: https://en.wikipedia.org/wiki/Binary_heap 16 | [wiki-cpu-cache]: https://en.wikipedia.org/wiki/CPU_cache 17 | 18 | ## 步驟 19 | 20 | Heapsort 的演算法分為兩大步驟: 21 | 22 | 1. 將資料轉換為 heap 資料結構(遞增排序用 max-heap, 遞減排序選擇 min-heap)。 23 | 2. 逐步取出最大/最小值,並與最後一個元素置換。具體步驟如下: 24 | 1. 交換 heap 的 root 與最後一個 node,縮小 heap 的範圍(排序一筆資料,故 heap 長度 -1)。 25 | 2. 更新剩下的資料,使其滿足 heap 的特性,稱為 heap ordering property。 26 | 3. 重複前兩個步驟,直到 heap 中剩最後一個未排序的資料。 27 | 28 | 透過 GIF 動畫感受一下 heapsort 的威力吧! 29 | 30 | ![](https://upload.wikimedia.org/wikipedia/commons/1/1b/Sorting_heapsort_anim.gif) 31 | 32 | ## 說明 33 | 34 | 在開始之前,定義幾個 heap 常用名詞: 35 | 36 | - **Heap ordering property**:一個 heap 必須要滿足的條件。以 heap 種類不同有幾種變形。 37 | - **min-heap property**:每個結點皆大於等於其父節點的值,且最小值在 heap root。 38 | - **max-heap property**:每個結點皆小於等於其父節點的值,且最大值在 heap root。 39 | 40 | 而 heapsort 主要分為兩個部分: 41 | 42 | 1. **Heapify**:將陣列轉換為 heap 資料結構(heapify)。 43 | 2. **Sorting**:不斷置換 heap root 與最後一個元素來排序,並修正剩餘未排序資料使其符合 heap order。 44 | 45 | 這裡有一個未排序的序列,將以遞增方向排序之。 46 | 47 | ``` 48 | [17, 20, 2, 1, 3, 21] 49 | ``` 50 | 51 | 首先,將資料轉換為 heap 資料結構,這個步驟即時 **heapify**。由於是遞增排序,我們採用 max-heap(最大元素在 root)。 52 | 53 | ``` 54 | [21, 20, 17, 1, 3, 2] 55 | ``` 56 | 57 | 對應的二元樹(binary tree)的圖形如下: 58 | 59 | 60 | 61 | 再來就是**排序的部分**,Max-heap 會將最大的元素擺在 root 的位置,我們先將最後一個 node 與 root 進行交換,完成第一個排序步驟。 62 | 63 | > 若不熟悉 heap,可以閱讀 [Wiki 的介紹][wiki-heap],其實 heap 就是用陣列實作的二元樹。 64 | 65 | ``` 66 | [21, 20, 17, 1, 3, 2] 67 | * * 68 | (swap) --> 69 | 70 | unsorted | sorted 71 | [2, 20, 17, 1, 3 | 21] 72 | ``` 73 | 74 | 接下來,將未排序的資料區塊重整為符合 max-heap 的結構。 75 | 76 | ``` 77 | [2, 20, 17, 1, 3 | 21] 78 | 79 | (sift down) --> 80 | 81 | [20, 3, 17, 1, 2 | 21] 82 | ``` 83 | 84 | 有沒有看出一些端倪? 85 | 86 | 只要不斷將 root 和最後一個 node 交換,並將剩餘資料修正至滿足 heap ordering,就完成排序了。 87 | 88 | ``` 89 | [20, 3, 17, 1, 2 | 21] 90 | * * 91 | (swap) --> 92 | 93 | [2, 3, 17, 1 | 20, 21] 94 | 95 | (sift down)--> 96 | 97 | [17, 3, 2, 1 | 20, 21] 98 | * * 99 | (swap) --> 100 | 101 | [1, 3, 2 | 17, 20, 21] 102 | 103 | (sift down)--> 104 | 105 | [3, 1, 2 | 17, 20, 21] 106 | * * 107 | (swap) --> 108 | 109 | [1, 2 | 3, 17, 20, 21] 110 | 111 | (Done!) 112 | ``` 113 | 114 | 以上便是 heapsort 演算法的簡單流程,是不是和 selection sort 非常相似呢! 115 | 116 | ## 效能 117 | 118 | | | Complexity | 119 | | ------------ | ------------------ | 120 | | Worst | $O(n \log n) $ | 121 | | Best | $O(n \log n) $ | 122 | | Average | $O(n \log n) $ | 123 | | Worst space | $O(1) $ auxiliary | 124 | 125 | Heapsort 最佳、最差、平均的時間複雜度皆為 $O(n \log n) $,同樣分為兩部分簡單解釋。 126 | 127 | ### Build heap (heapify) 128 | 129 | 建立一個 binary heap 有兩種方法,一種是一個個元素慢慢加入 heap 來建立;另一種則是給定隨意的序列,再透過 heapify 演算法修正序列為有效的 heap。一般來說 heapsort 常用實作後者。 130 | 131 | **Heapify** 是指將序列修正至符合 heap ordering 的序列。給定一個元素,假定其為非法的 heap order,而該元素之後的 subtree 視為符合 heap ordering property。欲修正這個在錯誤位置的元素,必須透過與其 children node 置換往下篩,這個往下篩的過程就稱為 **sift down**,在[實作](#實作)一節會詳細解釋,這邊只要知道 sift down 會不斷將該元素與其 child node 比較,若不符合 heap order 則與 child node 置換,並繼續疊代每一個 level。所以 sift down 的時間複雜度為 $O(\lceil {\log_2(n)} \rceil) = O(\log n) $, $n $ 為陣列元素個數。 132 | 133 | Heapify 從最末個元素開始反向疊代,每個元素都呼叫 `sift_down` 調整 heap 符合 heap ordering。總共要做 $n $ 次 `sift_down` 操作,但由於最後一層所以 leaf 已符合 heap order(因為沒有 child node),我們的迴圈可以跳過所有 leaf node 直接從非 leaf node 開始,因此複雜度為 134 | 135 | $$\lfloor n / 2 \rfloor \cdot O(\log n) = O(n \log n)$$ 136 | 137 | > 實際上,build heap 步驟的複雜度可達到 $O(n) $,可以看看 UMD 演算法課程 [Lecture note 的分析][umd-algo-analysis]。 138 | 139 | [umd-algo-analysis]: http://www.cs.umd.edu/~meesh/351/mount/lectures/lect14-heapsort-analysis-part.pdf 140 | 141 | ### Sorting (sift down) 142 | 143 | 講完了 heapify,就換到排序部分,所謂的排序其實就是利用 max-heap(或 min-heap)的最大值(最小值)會在首個元素的特性,與最後一個元素置換,完成排序,並將剩餘的部分透過 **sift down** 修正符合 heap order。所以總共需要做 $n $ 次 sift down,複雜度為 $O(n \log n) $。 144 | 145 | ### Sum up 146 | 147 | 綜合這兩部分,可以看出 Sorting part 對複雜度有決定性影響,最佳複雜度為 $O(n \log n) $。 148 | 149 | ## 實作 150 | 151 | Heapsort 的實作相對簡單,只需要不斷呼叫 heap 內部的 `sift_down` 方法就可以完成排序。整個演算法架構如下: 152 | 153 | ```rust 154 | pub fn heapsort(arr: &mut [i32]) { 155 | // -- Heapify part -- 156 | // This procedure would build a valid max-heap. 157 | // (or min-heap for sorting descendantly) 158 | let end = arr.len(); 159 | for start in (0..end / 2).rev() { // 1 160 | sift_down(arr, start, end - 1); 161 | } 162 | 163 | // -- Sorting part -- 164 | // Iteratively sift down unsorted part (the heap). 165 | for end in (1..arr.len()).rev() { // 2 166 | arr.swap(end, 0); // 3 167 | sift_down(arr, 0, end - 1); // 4 168 | } 169 | } 170 | ``` 171 | 172 | 1. 這部分是 heapify,從最小 non-leaf node 開始(`end` / 2),修正序列至滿足 heap order,再反向疊代做 heapify。 173 | 2. 這部分負責排序,每次疊代都將排序 heap 的 root 元素,步驟如 3 - 4: 174 | 3. 不斷將 max-heap 中最大值(在 root 上)與 heap 最後一個元素 `end` 置換, 175 | 4. 並利用 `sift_down` 將序列修正至 max-heap 資料結構,依照定義,此時 unsorted pile 首個元素成為 max-heap root,是最大值。 176 | 177 | Heapsort 全靠 `sift_down` 神救援,那 `sift_down` 到底有什麼神奇魔力,一探究竟吧! 178 | 179 | ```rust 180 | fn sift_down(arr: &mut [i32], start: usize, end: usize) { 181 | let mut root = start; 182 | loop { 183 | let mut child = root * 2 + 1; // Get the left child // 1 184 | if child > end { 185 | break; 186 | } 187 | if child + 1 <= end && arr[child] < arr[child + 1] { // 2 188 | // Right child exists and is greater. 189 | child += 1; 190 | } 191 | 192 | if arr[root] < arr[child] { // 3 193 | // If child is greater than root, swap'em! 194 | arr.swap(root, child); 195 | root = child; 196 | } else { 197 | break; 198 | } 199 | } 200 | } 201 | ``` 202 | 203 | `sift_down` 的功能是將 node 往下移。通常用在 heap 刪除或取代 node 時,將序列修正為有效的 heap。 這裡實作的版本有三個參數: 204 | 205 | - `arr`:欲修正為符合 heap 定義的序列。 206 | - `start`:欲往下移動的 node index,可視為需要被修正的元素。 207 | - `end`:此 node 以內(包含)的序列都會被修正為有效的 heap。 208 | 209 | `sift_down` 有些假設條件:從 `start` index 出發的子樹,除了 `start` 本身以外,其他皆符合 heap ordering。 210 | 211 | 再來看看 `sift_down` 實作內容,`loop` 中幹的活就是不斷將 `start` index 上的元素與其子樹比較,若不符合 heap ordering,則兩者置換。 212 | 213 | 1. **是否有子結點**:依照 binary heap 的定義找出 root 的左子樹(left substree),若左子樹的 index `child` 比 `end` 還大,表示沒有 heap 沒有子結點,停止疊代。 214 | 2. **檢查右子樹值較大**:若 root 下有右子樹且較大,我們會標記右子樹,並在下一步對右子樹進行處理。 215 | 3. **置換**:若 `root` 元素比 `child` 的元素小,則置換兩者,並將 `child` 設置為下個疊代的 `root`,繼續檢查最初的 `start` 元素是否滿足 heap ordering。 216 | 217 | 以上就是簡單的 `sift_down` 實作,也是整個 heapsort 的精髓。 218 | 219 | ## 參考資料 220 | 221 | - [Wiki: Heap][wiki-heap] 222 | - [Wiki: Heapsort](https://en.wikipedia.org/wiki/Heapsort) 223 | - [CMSC 351 Algorithms, Fall, 2011, University of Maryland.](www.cs.umd.edu/~meesh/) 224 | - Sorting GIF by RolandH [CC BY-SA-3.0](http://creativecommons.org/licenses/by-sa/3.0/) via Wikimedia Commons. 225 | 226 | [wiki-heap]: https://en.wikipedia.org/wiki/Heap_(data_structure) 227 | -------------------------------------------------------------------------------- /src/sorting/heapsort/mod.rs: -------------------------------------------------------------------------------- 1 | /// Heapsort. 2 | pub fn heapsort(arr: &mut [i32]) { 3 | // -- Heapify part -- 4 | // This procedure would build a valid max-heap. 5 | // (or min-heap for sorting descendantly) 6 | let end = arr.len(); 7 | for start in (0..end / 2).rev() { 8 | // Skip leaf nodes (end / 2). 9 | sift_down(arr, start, end - 1); 10 | } 11 | 12 | // -- Sorting part -- 13 | // Iteratively sift down unsorted part (the heap). 14 | for end in (1..arr.len()).rev() { 15 | arr.swap(end, 0); 16 | sift_down(arr, 0, end - 1); 17 | } 18 | } 19 | 20 | /// Internal function for heap to fix itself to conform to heap definition. 21 | /// Precondiition: all elements below `start` are in heap order 22 | /// expect `start` itself. 23 | fn sift_down(arr: &mut [i32], start: usize, end: usize) { 24 | let mut root = start; 25 | loop { 26 | let mut child = root * 2 + 1; // Get the left child 27 | if child > end { 28 | break; 29 | } 30 | if child < end && arr[child] < arr[child + 1] { 31 | // Right child exists and is greater. 32 | child += 1; 33 | } 34 | 35 | if arr[root] < arr[child] { 36 | // If child is greater than root, swap'em! 37 | arr.swap(root, child); 38 | root = child; 39 | } else { 40 | break; 41 | } 42 | } 43 | } 44 | 45 | #[cfg(test)] 46 | mod base { 47 | use super::*; 48 | base_cases!(heapsort); 49 | } 50 | -------------------------------------------------------------------------------- /src/sorting/heapsort/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/rust-algorithm-club/1954301f21d44c18ea44864a776a3f6fd72eaefe/src/sorting/heapsort/tree.png -------------------------------------------------------------------------------- /src/sorting/insertion_sort/README.md: -------------------------------------------------------------------------------- 1 | # 插入排序 Insertion Sort 2 | 3 | Insertion sort 是最簡單的排序法之一,比起 quicksort 等高效的排序法,對大資料的處理效能較不理想。其演算法是將欲排序元素直接插入正確位置,因而得名。 4 | 5 | Insertion sort 基本特性如下: 6 | 7 | - 實作簡單易理解。 8 | - 資料量少時較高效,且比其他 $O(n^2) $ 的排序法高效(selection sort/bubble sort)。 9 | - **自適應排序**:可根據當前資料排序情形加速排序,資料越接近排序完成,效率越高。 10 | - **穩定排序**:相同鍵值的元素,排序後相對位置不改變。 11 | - **原地排序**:不需額外花費儲存空間來排序。 12 | - **即時演算法**:可處理逐步輸入的資料,不需等資料完全備妥。 13 | 14 | ## 步驟 15 | 16 | 將序列分為未排序與部分排序兩個區域。 17 | 18 | ![](https://upload.wikimedia.org/wikipedia/commons/3/32/Insertionsort-before.png) 19 | 20 | 1. **取第一個元素**,將該元素視為已排序。 21 | 2. **取出下一元素**,該元素將插入序列的部分排序區域。 22 | 3. **尋找正確位置**:若部分排序元素比新元素大,則互換位置。並重複步驟 2 - 3,直到部分排序元素小於等於新元素。 23 | 4. **插入元素**:將新元素**插入**最後的位置。 24 | 5. 重複步驟 2 - 4,直到排序完成。 25 | 26 | 簡而言之,即是每次取一個元素,尋找並插入該元素在部分排序區域的排序位置,再逐步把序列單邊排序完成。 27 | 28 | ![](https://upload.wikimedia.org/wikipedia/commons/d/d9/Insertionsort-after.png) 29 | 30 | Insertion sort 非常簡單,看動畫就能明瞭。 31 | ![](https://upload.wikimedia.org/wikipedia/commons/0/0f/Insertion-sort-example-300px.gif) 32 | 33 | ## 效能 34 | 35 | | | Complexity | 36 | | ------------ | ------------- | 37 | | Worst | $O(n^2) $ | 38 | | Best | $O(n) $ | 39 | | Average | $O(n^2) $ | 40 | | Worst space | $O(1) $ auxiliary | 41 | 42 | 最佳時間複雜度發生在資料已完成排序的狀況下,insertion sort 只需執行最外層的迴圈 $n $ 次。 43 | 44 | 最差時間複雜度發生在資料完全相反時,insertion sort 每取得一個新元素是,都需將資料插入序列最前面,,因此所需的操作如下( $c $ 為任意常數): 45 | 46 | $$ c \cdot 1 + c \cdot 2 + c \cdot 3 \cdots + c \cdot (n - 1) = \frac{c(n - 1 + 1)(n - 1)}{2}$$ 47 | 48 | 最後等於 49 | 50 | $$\frac{cn^2}{2} - \frac{cn}{2}$$ 51 | 52 | 捨去低次項,得到時間複雜度為 $O(n^2) $。 53 | 54 | ## 實作 55 | 56 | 簡單實作的程式碼如下: 57 | 58 | ```rust 59 | pub fn insertion_sort(arr: &mut [i32]) { 60 | for i in 1..arr.len() { // 1 61 | let mut j = i; 62 | while j > 0 && arr[j - 1] > arr[j] { // 2 63 | arr.swap(j - 1, j); 64 | j -= 1; 65 | } 66 | } 67 | } 68 | ``` 69 | 70 | 1. 外層迴圈疊代整個序列。並取出 index `i`,`arr[i]` 是待排序的元素,index 比 `i` 小的元素則組成已排序的部分序列。 71 | 2. 內層迴圈負責元素比較,決定待排序元素該從何處插入,若前一個元素比待排元素大,則置換兩元素,並繼續往下尋找正確的插入點。直到 `j == 0` 或待排元素比任何已排序元素都大為止。 72 | 73 | ## 變形 74 | 75 | ### Binary Insertion Sort 76 | 77 | 在一般演算法討論中,通常以簡單的型別如 `i32` 來探討並實作。在真實世界中,做哪種操作,用哪種語言,都會影響到實際效能。例如 Python 的比較操作相對於置換元素,成本高出不少,是因為每個物件在 Python 的比較需動態檢查是否實作 `__lt__` `__gt__` 等方法才能進行比較。所以 Python 排序法實作就要特別注意減少比較操作的次數。 78 | 79 | Binary insertion sort 的目的就是減少內層迴圈的比較次數。在內層迴圈開始之前,使用 [binary search][wiki-binary-search] 搜尋新元素應要插入哪個位置,最多僅需 $\log_2n $ 次比較。但 binary insertion sort 的複雜度依舊是 $O(n^2) $,因為除了比較之外,仍需置換(swap)、賦值(assign)等基礎操作。 80 | 81 | Binary insertion sort 的程式碼和一般的 insertion sort 差不了多少,我們這裡使用 `slice` 內建的 `binary_search` 來找尋插入點。 82 | 83 | ```rust 84 | pub fn binary_insertion_sort(arr: &mut [i32]) { 85 | for i in 1..arr.len() { 86 | let val = arr[i]; 87 | let mut j = i; 88 | let pos = match arr[..i].binary_search(&val) { // 1 89 | Ok(pos) => pos, // 2 90 | Err(pos) => pos, 91 | }; 92 | while j > pos { // 3 93 | arr.swap(j - 1, j); 94 | j -= 1; 95 | } 96 | } 97 | } 98 | ``` 99 | 100 | 1. 先限制 `binary_search` 範圍,取出 sorted pile `arr[..i]`。再對 slice 執行 `binary_search`。 101 | 2. `binary_search` 回傳一個 `Result` 型別,找到時回傳 `Ok(index 值)`,找無時回傳 `Err(不影響排序穩定度的插入點)`,這個 `Err` 的設計巧妙解決新值插入的問題。 102 | 3. 和普通 insertion sort 雷同,從插入點至 sorted pile 疊代到末端以進行排序,省下不少比較操作。 103 | 104 | [wiki-binary-search]: https://en.wikipedia.org/wiki/Binary_search 105 | 106 | ## 參考資料 107 | 108 | - [Wiki: Insertion sort](https://en.wikipedia.org/wiki/Insertion_sort) 109 | - [CPython: listsort note](https://github.com/python/cpython/blob/15f44ab043b37c064d6891c7864205fed9fb0dd1/Objects/listsort.txt#L686-L703) 110 | - Sorting GIF by Swfung8 (Own work) [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0) via Wikimedia Commons. 111 | -------------------------------------------------------------------------------- /src/sorting/insertion_sort/mod.rs: -------------------------------------------------------------------------------- 1 | /// Insertion sort. 2 | pub fn insertion_sort(arr: &mut [i32]) { 3 | for i in 1..arr.len() { 4 | let mut j = i; 5 | while j > 0 && arr[j - 1] > arr[j] { 6 | arr.swap(j - 1, j); 7 | j -= 1; 8 | } 9 | } 10 | } 11 | 12 | /// Binary insertion sort. 13 | /// 14 | /// Binary insertion sort is a insertion sort variant that utilizes binary 15 | /// search to reduce comparisons in a normal insertion sort. 16 | pub fn binary_insertion_sort(arr: &mut [i32]) { 17 | for i in 1..arr.len() { 18 | let val = arr[i]; 19 | let mut j = i; 20 | let pos = arr[..i].binary_search(&val).unwrap_or_else(|pos| pos); 21 | // Swap all elements until specific position. 22 | while j > pos { 23 | arr.swap(j - 1, j); 24 | j -= 1; 25 | } 26 | } 27 | } 28 | 29 | #[cfg(test)] 30 | mod base { 31 | use super::*; 32 | base_cases!(insertion_sort); 33 | } 34 | 35 | #[cfg(test)] 36 | mod binary_insertion { 37 | use super::*; 38 | base_cases!(binary_insertion_sort); 39 | } 40 | -------------------------------------------------------------------------------- /src/sorting/introsort/README.md: -------------------------------------------------------------------------------- 1 | # 內省排序 Introsort 2 | 3 | 🚧 🚧 🚧 TBD 🚧 🚧 🚧 4 | -------------------------------------------------------------------------------- /src/sorting/introsort/mod.rs: -------------------------------------------------------------------------------- 1 | /// Introsort. 2 | pub fn introsort(_arr: &mut [i32]) { 3 | unimplemented!() 4 | } 5 | -------------------------------------------------------------------------------- /src/sorting/mergesort/README.md: -------------------------------------------------------------------------------- 1 | # 合併排序 Mergesort 2 | 3 | Mergesort 是一個泛用且高效穩定的排序法,最佳與最差時間複雜都是 $O(n \log n) $。Mergesort 可謂著名「Divide and Conquer」手法的經典案例,先將序列分成更小的子序列(Divide),一個個排序後(Conquer),再合併已排序的子序列(Combine)。 4 | 5 | - **高效穩定**:最佳、平均,與最差時間複雜度皆為 $O(n \log n) $。 6 | - **穩定排序**:相同鍵值的元素,排序後相對位置不改變。 7 | - **非原地排序**:除了資料本身,仍需額外花費儲存空間來排序。 8 | - **分治演算法**:將主問題化作數個子問題,各個擊破。 9 | 10 | 11 | ## 步驟 12 | 13 | Mergesort 演算法分為以下步驟: 14 | 15 | 1. **Divide**:將含有 n 個元素的序列分割成含有 n / 2 個子序列。 16 | 2. **Conquer**:排序分割後的兩個子序列。 17 | 3. **Combine**:合併排序完成的兩子序列,成為一個排好序的序列。 18 | 19 | 其中,Conquer 步驟中的「排序」可以不斷遞迴 Mergesort 自身,因此需要停止遞迴的條件(base case),我們將條件設定為「子序列的長度小於 2」,因為長度為 1 的序列可視為已完成排序。 20 | 21 | 將 Mergesort 視覺化排序如下: 22 | 23 | ![mergsort](https://upload.wikimedia.org/wikipedia/commons/c/c5/Merge_sort_animation2.gif) 24 | 25 | ## 說明 26 | 27 | 以 ASCII diagram 圖解 Mergesort。 28 | 29 | 先將原始序列分割成數個長度為一的子序列。 30 | 31 | ``` 32 | Split array into length 1 subarray. 33 | 34 | [8, 7, 1, 2, 4, 6, 5, 3] 35 | | 36 | [8, 7, 1, 2] | [4, 6, 5, 3] 37 | | 38 | [8, 7] [1, 2] | [4, 6] [5, 3] 39 | | 40 | [8] [7] [1] [2] | [4] [6] [5] [3] 41 | V 42 | split 43 | ``` 44 | 45 | 再將子序列依序合併成一個排好序的大序列。 46 | 47 | ``` 48 | Recursively merge subarray respecting the order. 49 | 50 | Merge 51 | | 52 | [8] [7] [1] [2] | [4] [6] [5] [3] 53 | | 54 | [7, 8] [1, 2] | [4, 6] [3, 5] 55 | | 56 | [1, 2, 7, 8] | [3, 4, 5, 6] 57 | V 58 | [1, 2, 3, 4, 5, 6, 7, 8] 59 | ``` 60 | 61 | ## 效能 62 | 63 | | | Complexity | 64 | | ------------ | ------------------ | 65 | | Worst | $O(n \log n) $ | 66 | | Best | $O(n \log n) $ | 67 | | Average | $O(n \log n) $ | 68 | | Worst space | $O(n) $ auxiliary | 69 | 70 | ### Time Complexity 71 | 72 | 透過遞迴關係式,很容易計算 Mergesort 的時間複雜度。假設排序長度為 $n $ 的序列最多需要 $T(n) $ 時間。可以觀察到,如果序列只有一個元素,Mergesort 僅需要常數時間就可以完成排序,寫成 $T(n) = 1 $。 73 | 74 | 如果 $n > 2 $,Mergesort 會將序列分為 $\lceil \frac{n}{2} \rceil $ 部分,以及 $\lfloor \frac{n}{2} \rfloor $ 部分。我們可以將排序前者寫成 $T(\lceil \frac{n}{2} \rceil) $,而後者花費時間為 $ T(\lfloor \frac{n}{2} \rfloor) $。 75 | 76 | 最後,合併兩個子序列僅需 $n $ 個操作。可得下列遞迴關係式。 77 | (為了方便計算,把 floor 和 ceil 捨去) 78 | 79 | $$ 80 | T(n) = 81 | \begin{cases} 82 | 1 & \text{if } n = 1, \\\\ 83 | 2T(\frac{n}{2}) + n & \text{otherwise.} 84 | \end{cases} 85 | $$ 86 | 87 | 根據 [Master Theorem](master-theorem),可得複雜度為 $O(n \log n) $。 88 | 89 | [master-theorem]: https://en.wikipedia.org/wiki/Master_theorem_(analysis_of_algorithms) 90 | 91 | ### Space Complexity 92 | 93 | Mergesort 的缺點之一就是在合併子序列時,需要額外的空間依序插入排序資料;若是遞迴版本的 Mergesort 還需額外加上遞迴花費的 call stack 空間,因此額外空間複雜度為 $O(n) + O(\log n) = O(n) $(以陣列實作)。 94 | 95 | ## 實作 96 | 97 | 一般來說,Divide and Conquer 有兩種設計、解決問題的技巧:Top-down(自上而下)與 Buttom-up(自下而上)。前者是先對問題有整體的輪廓概念,再逐步針對細節一一處理;後者則是先準備每個問題需要的基礎步驟與元件,再將這些步驟結合,解決整體的問題。 98 | 99 | Mergesort 的實作分為兩部分: 100 | 101 | - `mergesort` 主程式:對外的介面,負責分割序列。對應 Divide 功能。 102 | - `merge`:合併子序列,對應到 Conquer 與 Combine 功能。 103 | 104 | 先來看看如何分割序列。 105 | 106 | ### Top-down split 107 | 108 | 自上而下的解法會不斷以類似 binary search 的方式找中點,進而分割序列。 109 | 110 | ```rust 111 | pub fn mergesort(arr: &mut [i32]) { 112 | let mid = arr.len() / 2; 113 | if mid == 0 { // 1 114 | return; 115 | } 116 | 117 | mergesort(&mut arr[..mid]); // 2 118 | mergesort(&mut arr[mid..]); 119 | 120 | // Create an array to store intermediate result. 121 | let mut ret = arr.to_vec(); // 3 122 | 123 | // Merge the two piles. 124 | merge(&arr[..mid], &arr[mid..], &mut ret[..]); // 4 125 | 126 | // Copy back the result back to original array. 127 | arr.copy_from_slice(&ret); // 5 128 | } 129 | ``` 130 | 131 | 1. 設定遞迴的終止條件(base case),middle index 為 0 表示長度不大於 1。 132 | 2. 利用 Rust 的 [Range Operator][rust-ops-range],可快速分割兩個 `slice`。 133 | 3. 建立一個 `Vec` 儲存排序結果。 134 | 4. 將兩個 `slice` 合併排序至 `ret` vector 中。 135 | 5. 將 `ret` 的結果複製到原始 `arr` 中,使回傳值保有相同起始位址。 136 | 137 | [rust-ops-range]: https://doc.rust-lang.org/std/ops/struct.Range.html 138 | 139 | ### Buttom-up split 140 | 141 | 自下而上的解法則是預定好最小的子序列長度,直接使用 for 迴圈從頭開始逐一擊破。 142 | 143 | ```rust 144 | pub fn mergesort_bottom_up(arr: &mut [i32]) { 145 | let mut width = 1; // 1 146 | // Create an array to store intermediate result. 147 | let mut ret = arr.to_vec(); // 2 148 | let len = arr.len(); 149 | 150 | while width < len { 151 | let mut i = 0; 152 | while i < len { 153 | // Check to avoid upper bound and middle index out of bound. 154 | let upper = ::std::cmp::min(i + 2 * width, len); // 3 155 | let mid = ::std::cmp::min(i + width, len); 156 | 157 | merge(&arr[i..mid], &arr[mid..upper], &mut ret[i..upper]); 158 | 159 | // Copy the merged result back to original array. 160 | arr[i..upper].copy_from_slice(&ret[i..upper]); // 4 161 | 162 | // Increase start index to merge next two subsequences. 163 | i += 2 * width; // 5 164 | } 165 | width *= 2; // 6 166 | } 167 | } 168 | ``` 169 | 170 | 1. 設定最小的子序列長度,這個長度以下的子序列皆視為已排序。 171 | 2. 建立一個 `Vec` 儲存排序結果。 172 | 3. 取最小值,避免下標超出邊界,並且維持除了最後一組,其他子序列長度恆為 `width`。 173 | 4. 複製這部分排序結果 `ret` 到原始的 `arr` 中。 174 | 5. 繼續下兩個子序列的合併步驟。 175 | 6. 將下個疊代的子序列長度加倍,繼續合併。 176 | 177 | ### The merge part 178 | 179 | 無論是 Top-down 還是 Buttom-up 版本的解法,皆免不了 `merge` 這個共同步驟,將子序列合併為較大的序列。 180 | 181 | ```rust 182 | fn merge(arr1: &[i32], arr2: &[i32], ret: &mut [i32]) { 183 | let mut left = 0; // Head of left pile. // 1 184 | let mut right = 0; // Head of right pile. 185 | let mut index = 0; 186 | 187 | // Compare element and insert back to result array. 188 | while left < arr1.len() && right < arr2.len() { // 2 189 | if arr1[left] <= arr2[right] { // 3 190 | ret[index] = arr1[left]; 191 | index += 1; 192 | left += 1; 193 | } else { 194 | ret[index] = arr2[right]; 195 | index += 1; 196 | right += 1; 197 | } 198 | } 199 | 200 | // Copy the reset elements to returned array. 201 | // `memcpy` may be more performant than for-loop assignment. 202 | if left < arr1.len() { // 4 203 | ret[index..].copy_from_slice(&arr1[left..]); 204 | } 205 | if right < arr2.len() { 206 | ret[index..].copy_from_slice(&arr2[right..]); 207 | } 208 | } 209 | ``` 210 | 211 | 1. 建立三個指標,分別給 `arr1`、`arr2` 與回傳陣列 `ret` 使用。 212 | 2. 這部分依序比較兩個子序列,排序較小者先進入回傳 `ret`。直到其中一序列所有元素都進入 `ret` 就停止。 213 | 3. 這邊判斷使用 `<=` 小於等於確保排序穩定(相同鍵值順序不換)。 214 | 4. 將剩餘未進入 `ret` 的元素,依序複製到 `ret` 中。 215 | 216 | > `slice.copy_from_slice` 底層使用 C 的 `memcpy`,比起 for-loop 一個個賦值,直接複製整塊記憶體比較快了。 217 | 218 | ## 變形 219 | 220 | ### Timsort 221 | 222 | 在真實世界資料中,早有許多部分排序的分區(natural run),倘若跳過排序這些分區的步驟,就可減少許多不必要的操作,[Timsort](../timsort) 就是為了完全利用榨乾這些分區的混合排序法。 223 | 224 | ## 參考資料 225 | 226 | - [Wiki: Merge sort](https://en.wikipedia.org/wiki/Merge_sort) 227 | - [CMSC 351 Algorithms, Fall, 2011, University of Maryland.](www.cs.umd.edu/~meesh/) 228 | - Sorting GIF was created By CobaltBlue [CC BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5) via Wikimedia Commons. 229 | -------------------------------------------------------------------------------- /src/sorting/mergesort/mod.rs: -------------------------------------------------------------------------------- 1 | /// Mergesort. 2 | /// 3 | /// - Top-down 4 | /// - Recursive 5 | pub fn mergesort(arr: &mut [i32]) { 6 | let mid = arr.len() / 2; 7 | if mid == 0 { 8 | return; 9 | } 10 | 11 | mergesort(&mut arr[..mid]); 12 | mergesort(&mut arr[mid..]); 13 | 14 | // Create an array to store intermediate result. 15 | let mut ret = arr.to_vec(); 16 | 17 | // Merge the two piles. 18 | merge(&arr[..mid], &arr[mid..], &mut ret[..]); 19 | 20 | // Copy back the result back to original array. 21 | arr.copy_from_slice(&ret); 22 | } 23 | 24 | /// Mergesort bottom-up version. 25 | /// 26 | /// - Buttom-up (for array-based data structure) 27 | /// - Iterative 28 | pub fn mergesort_bottom_up(arr: &mut [i32]) { 29 | let mut width = 1; 30 | // Create an array to store intermediate result. 31 | let mut ret = arr.to_vec(); 32 | let len = arr.len(); 33 | 34 | while width < len { 35 | let mut i = 0; 36 | while i < len { 37 | // Check to avoid upper bound and middle index out of bound. 38 | let upper = ::std::cmp::min(i + 2 * width, len); 39 | let mid = ::std::cmp::min(i + width, len); 40 | 41 | merge(&arr[i..mid], &arr[mid..upper], &mut ret[i..upper]); 42 | 43 | // Copy the merged result back to original array. 44 | arr[i..upper].copy_from_slice(&ret[i..upper]); 45 | 46 | // Increase start index to merge next two subsequences. 47 | i += 2 * width; 48 | } 49 | width *= 2; 50 | } 51 | } 52 | 53 | /// Merge helper. 54 | /// 55 | /// * `arr1` - Left pile to sort. 56 | /// * `arr2` - Right pile to sort. 57 | /// * `ret` - Result array to return 58 | fn merge(arr1: &[i32], arr2: &[i32], ret: &mut [i32]) { 59 | let mut left = 0; // Head of left pile. 60 | let mut right = 0; // Head of right pile. 61 | let mut index = 0; 62 | 63 | // Compare element and insert back to result array. 64 | while left < arr1.len() && right < arr2.len() { 65 | if arr1[left] <= arr2[right] { 66 | ret[index] = arr1[left]; 67 | index += 1; 68 | left += 1; 69 | } else { 70 | ret[index] = arr2[right]; 71 | index += 1; 72 | right += 1; 73 | } 74 | } 75 | 76 | // Copy the reset elements to returned array. 77 | // `memcpy` may be more performant than for-loop assignment. 78 | if left < arr1.len() { 79 | ret[index..].copy_from_slice(&arr1[left..]); 80 | } 81 | if right < arr2.len() { 82 | ret[index..].copy_from_slice(&arr2[right..]); 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | mod base { 88 | use super::*; 89 | base_cases!(mergesort); 90 | } 91 | 92 | #[cfg(test)] 93 | mod bottom_up { 94 | use super::*; 95 | base_cases!(mergesort_bottom_up); 96 | } 97 | -------------------------------------------------------------------------------- /src/sorting/mod.rs: -------------------------------------------------------------------------------- 1 | //! Sorting algorithms. 2 | 3 | #[cfg(test)] 4 | #[macro_use] 5 | mod test_cases; 6 | 7 | mod bubble_sort; 8 | pub use self::bubble_sort::{bubble_sort, bubble_sort_optimized}; 9 | 10 | mod insertion_sort; 11 | pub use self::insertion_sort::{binary_insertion_sort, insertion_sort}; 12 | 13 | mod selection_sort; 14 | pub use self::selection_sort::selection_sort; 15 | 16 | mod shellsort; 17 | pub use self::shellsort::{shellsort, MARCIN_GAPS}; 18 | 19 | mod mergesort; 20 | pub use self::mergesort::{mergesort, mergesort_bottom_up}; 21 | 22 | mod heapsort; 23 | pub use self::heapsort::heapsort; 24 | 25 | mod quicksort; 26 | pub use self::quicksort::{ 27 | quicksort, quicksort_3way, quicksort_hoare, quicksort_manual_tco, quicksort_optimized, 28 | }; 29 | 30 | mod bucket_sort; 31 | pub use self::bucket_sort::bucket_sort; 32 | 33 | mod counting_sort; 34 | pub use self::counting_sort::counting_sort; 35 | 36 | mod radix_sort; 37 | pub use self::radix_sort::radix_sort; 38 | 39 | mod timsort; 40 | pub use self::timsort::timsort; 41 | 42 | mod introsort; 43 | pub use self::introsort::introsort; 44 | 45 | mod pdqsort; 46 | pub use self::pdqsort::pdqsort; 47 | -------------------------------------------------------------------------------- /src/sorting/pdqsort/README.md: -------------------------------------------------------------------------------- 1 | # 模式消除快速排序 Pattern-defeating Quicksort 2 | 3 | 🚧 🚧 🚧 TBD 🚧 🚧 🚧 4 | 5 | - Unstable 6 | - Adopted in libcore in Rust 7 | 8 | ## 參考資料 9 | 10 | - https://news.ycombinator.com/item?id=14661659 11 | - https://redd.it/5qa8h6 12 | - https://github.com/orlp/pdqsort 13 | - https://github.com/stjepang/pdqsort 14 | - https://github.com/rust-lang/rust/pull/40601 15 | -------------------------------------------------------------------------------- /src/sorting/pdqsort/mod.rs: -------------------------------------------------------------------------------- 1 | /// Pattern-defeating quicksort. 2 | pub fn pdqsort(_arr: &mut [i32]) { 3 | unimplemented!() 4 | } 5 | -------------------------------------------------------------------------------- /src/sorting/quicksort/mod.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------- 2 | // Lomuto partition scheme 3 | // ------------------------------------- 4 | 5 | /// Quicksort with Lomuto parition scheme. 6 | pub fn quicksort(arr: &mut [i32]) { 7 | let hi = arr.len() as isize - 1; 8 | quicksort_helper(arr, 0, hi); 9 | } 10 | 11 | /// Recursion helper 12 | fn quicksort_helper(arr: &mut [i32], lo: isize, hi: isize) { 13 | if lo <= hi { 14 | let pivot = partition(arr, lo, hi); 15 | quicksort_helper(arr, lo, pivot - 1); 16 | quicksort_helper(arr, pivot + 1, hi); 17 | } 18 | } 19 | 20 | /// Tail-call opitimized quicksort with Lomuto parition scheme. 21 | pub fn quicksort_optimized(arr: &mut [i32]) { 22 | let hi = arr.len() as isize - 1; 23 | quicksort_helper_optimized(arr, 0, hi); 24 | } 25 | 26 | /// Tail-call optimized recursion helper. 27 | /// 28 | /// Can achieve O(log n) auxiliary space complexity if Rust compiler is 29 | /// implemented with tail-call optimization in the future). 30 | fn quicksort_helper_optimized(arr: &mut [i32], lo: isize, hi: isize) { 31 | if lo <= hi { 32 | let pivot = partition(arr, lo, hi); 33 | if pivot - lo < hi - pivot { 34 | // 1 35 | quicksort_helper_optimized(arr, lo, pivot - 1); 36 | quicksort_helper_optimized(arr, pivot + 1, hi); 37 | } else { 38 | quicksort_helper_optimized(arr, pivot + 1, hi); 39 | quicksort_helper_optimized(arr, lo, pivot - 1); 40 | } 41 | } 42 | } 43 | 44 | /// Manual tail-call opitimized quicksort with Lomuto parition scheme. 45 | pub fn quicksort_manual_tco(arr: &mut [i32]) { 46 | let hi = arr.len() as isize - 1; 47 | quicksort_helper_manual_tco(arr, 0, hi); 48 | } 49 | 50 | /// Manual tail-call opitimized recursion helper. 51 | /// 52 | /// Can achieve O(log n) auxiliary space complexity without any 53 | /// compiler-optimization of tail-call. 54 | fn quicksort_helper_manual_tco(arr: &mut [i32], mut lo: isize, mut hi: isize) { 55 | while lo < hi { 56 | let pivot = partition(arr, lo, hi); 57 | if pivot - lo < hi - pivot { 58 | quicksort_helper_manual_tco(arr, lo, pivot - 1); 59 | lo = pivot + 1; 60 | } else { 61 | quicksort_helper_manual_tco(arr, pivot + 1, hi); 62 | hi = pivot - 1; 63 | } 64 | } 65 | } 66 | 67 | /// Lomuto partition scheme 68 | /// 69 | /// Return index of the pivot. 70 | fn partition(arr: &mut [i32], lo: isize, hi: isize) -> isize { 71 | // -- Determine the pivot -- 72 | // In Lomuto parition scheme, 73 | // the latest element is always chosen as the pivot. 74 | let pivot = arr[hi as usize]; 75 | let mut i = lo; 76 | 77 | // -- Swap elements -- 78 | for j in lo..hi { 79 | if arr[j as usize] < pivot { 80 | arr.swap(i as usize, j as usize); 81 | i += 1; 82 | } 83 | } 84 | // Swap pivot to the middle of two piles. 85 | arr.swap(i as usize, hi as usize); 86 | i // Return the final index of the pivot 87 | } 88 | 89 | // ------------------------------------- 90 | // 3-way partition scheme 91 | // ------------------------------------- 92 | 93 | /// Quicksort with 3-way parition scheme. 94 | pub fn quicksort_3way(arr: &mut [i32]) { 95 | let hi = arr.len() as isize - 1; 96 | quicksort_helper_3way(arr, 0, hi); 97 | } 98 | 99 | /// Recursion helper 100 | fn quicksort_helper_3way(arr: &mut [i32], lo: isize, hi: isize) { 101 | if lo <= hi { 102 | let (smaller, larger) = partition_3way(arr, lo, hi); 103 | quicksort_helper_3way(arr, lo, smaller - 1); 104 | quicksort_helper_3way(arr, larger + 1, hi); 105 | } 106 | } 107 | 108 | /// 3-way paritition scheme 109 | /// 110 | /// Return smaller and larger index. (to avoid redundant work on identical elements) 111 | fn partition_3way(arr: &mut [i32], lo: isize, hi: isize) -> (isize, isize) { 112 | let pivot = arr[hi as usize]; 113 | let mut i = lo; // smaller 114 | let mut j = lo; // equal 115 | let mut k = hi; // large 116 | 117 | while j <= k { 118 | if arr[j as usize] < pivot { 119 | arr.swap(i as usize, j as usize); 120 | i += 1; 121 | j += 1; 122 | } else if arr[j as usize] > pivot { 123 | arr.swap(k as usize, j as usize); 124 | k -= 1; 125 | } else { 126 | // No swap when identicial. 127 | j += 1; 128 | } 129 | } 130 | 131 | // Return smaller and larger pointer to avoid iterate duplicate elements. 132 | (i, k) 133 | } 134 | 135 | // ------------------------------------- 136 | // Hoare partition scheme 137 | // ------------------------------------- 138 | 139 | /// Quicksort with Hoare parition scheme 140 | pub fn quicksort_hoare(arr: &mut [i32]) { 141 | if arr.is_empty() { 142 | return; 143 | } 144 | let hi = arr.len() - 1; 145 | quicksort_helper_hoare(arr, 0, hi); 146 | } 147 | 148 | /// Recursion helper 149 | fn quicksort_helper_hoare(arr: &mut [i32], lo: usize, hi: usize) { 150 | if lo < hi { 151 | let pivot = partition_hoare(arr, lo, hi); 152 | quicksort_helper_hoare(arr, lo, pivot); 153 | quicksort_helper_hoare(arr, pivot + 1, hi); 154 | } 155 | } 156 | 157 | /// Hoare partition scheme 158 | /// 159 | /// Return the middle index of the two partitions. 160 | /// 161 | /// Note that the return value is not necessarily be the index of the pivot, 162 | /// and the pivot is located somewhere of the first partition. 163 | fn partition_hoare(arr: &mut [i32], lo: usize, hi: usize) -> usize { 164 | let pivot = arr[lo]; 165 | let mut i = lo; 166 | let mut j = hi; 167 | 168 | loop { 169 | // Find element >= pivot from leftmost element. 170 | while arr[i] < pivot { 171 | i += 1; 172 | } 173 | // Find element <= pivot from rightmost element. 174 | while arr[j] > pivot { 175 | j -= 1; 176 | } 177 | if i >= j { 178 | return j; 179 | } 180 | // Two elements are misplaced, swap them. 181 | arr.swap(i, j); 182 | i += 1; 183 | j -= 1; 184 | } 185 | } 186 | 187 | #[cfg(test)] 188 | mod base { 189 | use super::*; 190 | base_cases!(quicksort); 191 | } 192 | 193 | #[cfg(test)] 194 | mod optimized { 195 | use super::*; 196 | base_cases!(quicksort_optimized); 197 | } 198 | 199 | #[cfg(test)] 200 | mod manual_tco { 201 | use super::*; 202 | base_cases!(quicksort_manual_tco); 203 | } 204 | 205 | #[cfg(test)] 206 | mod three_way { 207 | use super::*; 208 | base_cases!(quicksort_3way); 209 | } 210 | 211 | #[cfg(test)] 212 | mod hoare { 213 | use super::*; 214 | base_cases!(quicksort_hoare); 215 | } 216 | -------------------------------------------------------------------------------- /src/sorting/radix_sort/README.md: -------------------------------------------------------------------------------- 1 | # 基數排序 Radix sort 2 | 3 | 如果你對 [Counting sort](../counting_sort) 與 [Bucket sort](../bucket_sort) 有認識,應該知道這兩個排序都能突破比較排序法複雜度 $O(n \log n) $ 限制的特殊排序法。[Radix sort][wiki-radix-sort] 同樣是個特殊的[整數排序法][wiki-integer-sorting],效能同樣可達突破限制。差別在於,前兩者僅依據一個鍵值排序,而 Radix sort 則是依據多個鍵值排序。 4 | 5 | 舉例來說,欲排序一群範圍在 0 - 999 的整數,若以 Counting sort 排序,則需建立一個「1000 元素的陣列」來計算每個整數的出現次數;若使用以 10 為基數的 Radix sort,則僅需以個位數、十位數、百位數作為鍵值分別排序三次。通常 Radix sort 的排序副程式(Sorting subroutine)會選用 Counting sort 或 Bucket sort,而以 10 為基數的鍵值範圍僅 0 - 9,這種小範圍整數非常適合 Counting sort 作為排序副程式,節省了配置 `int arr[1000]` 的 count array 的時空間。 6 | 7 | Radix sort 基本特性如下: 8 | 9 | - **整數排序法**:以整數作為排序的鍵值。 10 | - **分配式排序法**:不透過兩兩比較,而是分析鍵值分佈來排序。特定情況下可達線性執行時間。 11 | - **穩定性**:採用 LSD 的 Radix sort 屬穩定排序法(Stable sort);透過優化,採用 MSD 也可以是穩定排序法。 12 | 13 | [wiki-integer-sorting]: https://en.wikipedia.org/wiki/Integer_sorting 14 | 15 | ## 步驟 16 | 17 | 常見的 Radix sort 依據整數的每個位數來排序,依照位數排序的先後順序,可分為兩種: 18 | 19 | - **Least significant digit (LSD)**:從最低有效鍵值開始排序(最小位數排到大)。 20 | - **Most significant digit (MSD)**:從最高有效鍵值開始排序(最大位數排到小)。 21 | 22 | 簡單的 LSD Radix sort 步驟如下: 23 | 24 | 1. **LSD of each key**:取得每個資料鍵值的最小位數(LSD)。 25 | 2. **Sorting subroutine**:依據該位數大小排序資料。 26 | 3. **Repeating**:取得下一個有效位數,並重複步驟二,至最大位數(MSD)為止。 27 | 28 | 29 | 而 MSD Radix sort 的步驟相似,但取得資料鍵值的方向相反。 30 | 31 | 1. **MSD of each key**:取得每個資料鍵值的最大位數(MSD)。 32 | 2. **Sorting subroutine**:依據該位數大小排序資料。 33 | 3. **Repeating**:取得下一個有效位數,並重複步驟二,至最小位數(LSD)為止。 34 | 35 | > 由於 MSD Radix sort 先排序最大位數,會出現 **8 > 126** 的結果,這種順序通常稱為 [Lexicographical order][wiki-lexicographical-order],有如字典一般,越前面的字母排序權重越重,也因此,基本版的 MSD Radix sort 並非穩定排序法。 36 | 37 | [wiki-lexicographical-order]: https://en.wikipedia.org/wiki/Lexicographical_order 38 | 39 | ## 說明 40 | 41 | 我們選用 LSD Radix sort 示範,並且為了增加可讀性,將基數設為 10。需注意在現實場景中,有時使用 bytes 作為基數可能更適合。 42 | 43 | 待排序的數列如下。 44 | 45 | ``` 46 | [170, 45, 75, 90, 802, 2, 24, 66] 47 | ``` 48 | 49 | > Radix sort 的排序副程式,通常選用 counting sort 或 bucket sort,因此,開始排序前,需建立供其使用的 buckets(或 count array)。這屬於其他排序法的範疇,有興趣可看 [Counting sort](../counting_sort) 或 [Bucket sort](../bucket_sort)。 50 | 51 | 首先,從最小位數開始排序。 52 | 注意,同樣鍵值的資料,相對位置不會改變(穩定排序)。 53 | 54 | ``` 55 | 0 5 5 0 2 2 4 6 56 | _ _ _ _ _ _ _ _ 57 | [170, 45, 75, 90, 802, 2, 24, 66] 58 | 59 | sort by rightmost digit --> 60 | 61 | 0 0 2 2 4 5 5 6 62 | _ _ _ _ _ _ _ _ 63 | [170, 90, 802, 2, 24, 45, 75, 66] 64 | ``` 65 | 66 | 再來,對下一個位數排序資料。位數不足的資料,予以補 0。 67 | 68 | ``` 69 | 7 9 0 0 2 4 7 6 70 | _ _ _ _ _ _ _ 71 | [170, 90, 802, 2, 24, 45, 75, 66] 72 | 73 | sort by next digit --> 74 | 75 | 0 0 2 4 6 7 7 9 76 | _ _ _ _ _ _ _ 77 | [802, 2, 24, 45, 66, 170, 75, 90] 78 | ``` 79 | 80 | 最終,對最後一個位數進行排序。大功告成! 81 | 82 | ``` 83 | 8 0 0 0 0 1 0 0 84 | _ _ 85 | [802, 2, 24, 45, 66, 170, 75, 90] 86 | 87 | sort by leftmost digit --> 88 | 89 | 0 0 0 0 0 0 1 8 90 | _ _ 91 | [2, 24, 45, 66, 75, 90, 170, 802] 92 | ``` 93 | 94 | ## 效能 95 | 96 | | | Complexity | 97 | | ------------ | ------------ | 98 | | Worst | $O(dn) $ | 99 | | Best | $O(dn) $ | 100 | | Average | $O(dn) $ | 101 | | Worst space | $O(d + n) $ auxiliary | 102 | 103 | > $n $:資料筆數。 104 | > $d $:number of digit,資料中最多有幾個位數(或鍵值)。 105 | > $k $:基數,就是一個位數最多有幾種可能的值。 106 | 107 | ### Time complexity 108 | 109 | 欲分析 Radix sort 的時間複雜度,我們可以逐一擊破,先從排序副程式開始分析。 110 | 111 | Radix sort 的 subroutine 通常採用 Counting sort 或 Bucket sort,因此每個 subroutine 的複雜度為 $O(n + k) $, $k $ 為 key 的範圍,以 10 為基數,就是 0 - 9 之間 $k = 10 $。 112 | 113 | 再來,我們分析整個主程式,Radix sort 每個位數各需排序一次,若最多位數的資料有 $d $ 位數,時間複雜度需乘上 $d $,為 $O(d (n + k)) $,那這個 $k $ 是否可以捨去呢? 114 | 115 | 分析 Counting sort 或 Bucket sort 時,範圍 $k $ 會隨輸入資料而變化,若 $k $ 過大,對複雜度的影響甚至會超過 $n $,因此分析複雜度時無法將 $k $ 捨去。而在 Radix sort, $k $ 通常為一個已知的常數,例如以 bytes 為基數 $k = 8 $, $k $ 可以捨去。最後可得 Radix sort 的時間複雜度為 $O(d \cdot n) $。 116 | 117 | ### Space complexity 118 | 119 | Radix sort 的空間複雜度同樣取決於排序副程式,Counting sort 與 Bucket sort 的空間複雜度皆為 $O(n \cdot k) $。Radix sort 的 $k $ 是常數,予以捨去。再乘上 $d $ 個位數,最差的空間複雜度為 $O(d \cdot n) $。 120 | 121 | ## 實作 122 | 123 | 這裡示範實作以 10 為基數,用來排序非負整數的 Radix sort。 124 | 125 | 首先,我們的排序副程式使用 Counting sort。 126 | 127 | ```rust 128 | // 0. Include counting sort. 129 | use ::sorting::counting_sort; 130 | ``` 131 | 132 | 再來,就是 Radix sort 本體了。為了凸顯 Radix sort 的概念,簡化了函式參數數量,除去泛型宣告,並將基數選擇寫死在函式裡。 133 | 134 | ```rust 135 | pub fn radix_sort(arr: &mut [i32]) { 136 | let radix = 10; // 1 137 | let mut digit = 1; // 2 138 | let max_value = arr // 3 139 | .iter() 140 | .max() 141 | .unwrap_or(&0) 142 | .clone(); 143 | while digit <= max_value { // 4 144 | counting_sort(arr, 0, 9, |t| (t / digit % radix) as usize); // 5 145 | digit *= radix; // 6 146 | } 147 | } 148 | ``` 149 | 150 | 1. 設定基數為 10。 151 | 2. 設定一個旗標,記錄當前在排序哪一位數,1 表示從最小位數(個位數)開始。 152 | 3. 先找到輸入資料的最大值,作為之後副程式迴圈結束的條件。尋找最大值的複雜度為 $O(n)$,因此不影響 Radix Sort 的複雜度。如果 `arr` 為空序列,則最大值設為 0,在第四步驟就會自動結束排序。 153 | 4. 判斷當前排序的位數是否大於最大值,例如當前排序百分位,`digit` 為 `100`,而最大值 `x` 為 26,則不需再排序百分位。 154 | 5. 使用 Counting sort 作為排序副程式,只需要有 0 - 9 十個桶子。而 `key` 參數則取出當前欲比較的位數。 155 | 6. 位數乘上基數,移至下一個位數繼續比較。 156 | 157 | > 小提醒:這是簡單又容易理解的實作,相對有許多額外的運算開銷(例如尋找最大值)。實務上,會在對資料有些了解才採用 Radix sort,因此實作並不會這麼 naive。 158 | 159 | ## 參考資料 160 | 161 | - [Wiki: Radix sort][wiki-radix-sort] 162 | - [Princeton University DSA Course: Radix sort](https://www.cs.princeton.edu/~rs/AlgsDS07/18RadixSort.pdf) 163 | - [ByVoid: 三種線性排序算法 計數排序、桶排序與基數排序](https://www.byvoid.com/zht/blog/sort-radix) 164 | 165 | [wiki-radix-sort]: https://en.wikipedia.org/wiki/Radix_sort 166 | -------------------------------------------------------------------------------- /src/sorting/radix_sort/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::sorting::counting_sort; 2 | 3 | /// Radix sort for sorting unsigned integers. 4 | /// 5 | /// * `arr` - Collection of value to be sorted in place. 6 | pub fn radix_sort(arr: &mut [i32]) { 7 | // 1. Choose 10 as our radix. 8 | let radix = 10; 9 | // 2. Started from least significant digit (rightmost). 10 | let mut digit = 1; 11 | // 3. Find the maximum value to determine break point of the loop. 12 | let max_value = arr // 3 13 | .iter() 14 | .max() 15 | .unwrap_or(&0) 16 | .clone(); 17 | // 4. Sorting subroutine (use counting sort). 18 | while digit <= max_value { 19 | counting_sort(arr, 0, 9, |t| (t / digit % radix) as usize); 20 | digit *= radix; 21 | } 22 | } 23 | 24 | #[cfg(test)] 25 | mod base { 26 | use super::*; 27 | base_cases!(radix_sort); 28 | } 29 | -------------------------------------------------------------------------------- /src/sorting/selection_sort/README.md: -------------------------------------------------------------------------------- 1 | # 選擇排序 Selection sort 2 | 3 | Selection sort 是最易實作的入門排序法之一,會將資料分為 sorted pile 與 unsorted pile,每次從 unsorted pile 尋找最大/最小值,加入 sorted pile 中。 4 | 5 | Selection sort 的特性如下: 6 | 7 | - 最簡單的排序法之一。 8 | - 對小資料序列排序效率較高。 9 | - **不穩定排序**:排序後,相同鍵值的元素相對位置可能改變。 10 | - **原地排序**:不需額外花費儲存空間來排序。 11 | 12 | ## 步驟 13 | 14 | 1. 將資料分為 sorted pile 與 unsorted pile。 15 | 2. 從 unsorted pile 尋找最小值。 16 | 3. 置換該最小值元素與 unsorted pile 第一個元素。 17 | 4. 重複步驟 2 - 3,直到排序完成。 18 | 19 | > 注意,這個 naïve 的 selection sort 實作為**不穩定排序**。 20 | 21 | ![](https://upload.wikimedia.org/wikipedia/commons/9/94/Selection-Sort-Animation.gif) 22 | 23 | _Joestape89 - CC BY-SA 3.0_ 24 | 25 | ## 說明 26 | 27 | 為什麼 naïve 的 selection sort 會是不穩定排序? 28 | 29 | 假定有一個序列要遞增排序,其中有重複的 `2` 元素,我們將其標上 `2a`、`2b` 以利辨識。 30 | 31 | ``` 32 | [2a, 3, 4, 2b, 1] 33 | ``` 34 | 35 | 開始疊代找出最小值並指環。 36 | 37 | ```bash 38 | * * 39 | [1, 3, 4, 2b, 2a] # 1. 置換 2a, 1 40 | 41 | * * 42 | [1, 2b, 4, 3, 2a] # 2. 置換 3, 2b 43 | 44 | * * 45 | [1, 2b, 2a, 3, 4] # 3. 置換 4, 2a 46 | ``` 47 | 48 | 有沒有發現,`2a` 與 `2b` 的相對順序顛倒了呢? 49 | 50 | 首先,回想一下穩定排序的定義:**相同鍵值的元素,排序後相對位置不改變。** 51 | 52 | 問題出在 naïve selection sort 是以置換的方式排序每次疊代的最小值。若我們將置換(swap)改為插入(insert),那麼 selection sort 就會是穩定排序,但相對地,需要位移剩餘未排序的元素,除非使用 linked list 或其他提供 $O(1) $ insertion 的資料結構,不然就會多出額外 $O(n^2) $ 的寫入成本。 53 | ## 效能 54 | 55 | | | Complexity | 56 | | ------------ | ------------- | 57 | | Worst | $O(n^2) $ | 58 | | Best | $O(n^2) $ | 59 | | Average | $O(n^2) $ | 60 | | Worst space | $O(1) $ auxiliary | 61 | 62 | 對於接近排序完成的序列,selector sort 並無法有自適應的方式加快排序疊代。第一個元素要做 $n - 1 $ 次比較,第二個 $n - 2 $ 次,總比較次數如下: 63 | 64 | $$ (n -1) + (n-2) + \cdots + 1 = \sum_{i=1}^{n-1} i = \frac{n(n - 1)}{2}$$ 65 | 66 | 因此無論序列是否排序完成,selection sort 仍需執行 $n^2 $ 次比較,時間複雜度為 $O(n^2) $。 67 | 68 | ## 實作 69 | 70 | 簡單實作如下: 71 | 72 | ```rust 73 | pub fn selection_sort(arr: &mut [i32]) { 74 | let len = arr.len(); 75 | for i in 0..len { // 1 76 | let mut temp = i; 77 | for j in (i + 1)..len { // 2 78 | if arr[temp] > arr[j] { 79 | temp = j; 80 | } 81 | } 82 | arr.swap(i, temp); // 3 83 | } 84 | } 85 | ``` 86 | 87 | 1. 外層迴圈負責儲存當前要排序的 index `i` 的位置。 88 | 2. 內層迴圈負責在 unsorted pile 範圍 [`i`, `len`) 找最小值。 89 | 3. 外層迴圈在找到最小值之後,置換兩元素。 90 | 91 | 眼尖的人會發現,內外兩層迴圈的 upper bound 都是 `len`,這樣是否內側迴圈會 out of bound?Rust 的 range operator(`core::ops::Range`)實作 [`Iterator`][impl-iterator] trait 時,有檢查 `range.start < range.end`,因此這個寫法並不會有出界問題,但會多跑一次無意義的疊代。 92 | 93 | [impl-iterator]: https://doc.rust-lang.org/core/ops/struct.Range.html#impl-Iterator 94 | 95 | ## 變形 96 | 97 | ### Heapsort 98 | 99 | [Heapsort][heapsort] 是一個高效的排序法,使用 selection sort 融合 [heap][wiki-heap] 這種半排序的資料結構,讓時間複雜度進化至 $O(n \log n) $。更多詳情可以參考[這篇介紹][heapsort]。 100 | 101 | [heapsort]: ../heapsort/ 102 | [wiki-heap]: https://en.wikipedia.org/wiki/Heap_(data_structure) 103 | 104 | ## 參考資料 105 | 106 | - [Wiki: Selection sort](https://en.wikipedia.org/wiki/Selection_sort) 107 | - [Why Selection sort can be stable or unstable](https://stackoverflow.com/questions/20761396/) 108 | - Sorting GIF by Joestape89 [CC BY-SA-3.0](http://creativecommons.org/licenses/by-sa/3.0/) via Wikimedia Commons. 109 | -------------------------------------------------------------------------------- /src/sorting/selection_sort/mod.rs: -------------------------------------------------------------------------------- 1 | /// Selection sort. 2 | pub fn selection_sort(arr: &mut [i32]) { 3 | let len = arr.len(); 4 | // Rust would skip iteration if lower bound >= upper bound. 5 | // Hence, no need to `len - 1`. 6 | for i in 0..len { 7 | let mut temp = i; 8 | for j in (i + 1)..len { 9 | if arr[temp] > arr[j] { 10 | temp = j; 11 | } 12 | } 13 | arr.swap(i, temp); 14 | } 15 | } 16 | 17 | #[cfg(test)] 18 | mod base { 19 | use super::*; 20 | base_cases!(selection_sort); 21 | } 22 | -------------------------------------------------------------------------------- /src/sorting/shellsort/README.md: -------------------------------------------------------------------------------- 1 | # 希爾排序 Shellsort 2 | 3 | 眾所周知,[Insertion sort](../insertion_sort) 用在幾乎完成排序的序列上非常高效,換句話說,當元素置換不需移動太遠時,效率很高。反之,如果有元素錯位非常遙遠,效能就會大打折扣。Shellsort 以一個 gap sequence 將資料依指定的間隔(gap)分組進行 insertion sort,使得較遠的元素能夠快速歸位,下一次的排序就會因前次排序結果愈來愈接近完成而加速。 4 | 5 | Shellsort 最後一個 gap 必定是 1,也就是排序會退化成 insertion sort,此時大部分元素皆排序完成,insertion sort 會非常高效。 6 | 7 | Shellsort 特性如下: 8 | 9 | - **自適應排序**:可根據當前資料排序情形加速排序,資料越接近排序完成,效率越高。 10 | - **不穩定排序**:排序後,相同鍵值的元素相對位置可能改變。 11 | - **原地排序**:不需額外花費儲存空間來排序。 12 | - 可視為一般化(Generalizaion)的 [insertion sort](../insertion_sort)。 13 | 14 | ## 步驟 15 | 16 | Shellsort 分為兩個步驟: 17 | 18 | 1. 決定一組 gap sequence。 19 | 2. 疊代 gap sequence 進行分組排序,每次執行有間隔的 insertion sort。也就是每個元素與其相鄰 gap 的元素比較與置換。 20 | 21 | > 最後一次排序(gap = 1)會退化為 insertion sort,完成整個排序。 22 | 23 | ### Gap Sequneces 24 | 25 | Shellsort 的效率取決於 gap sequence 的選擇,這邊舉幾個常見的 gap sequence: 26 | 27 | | | Sequence | 28 | | ------------ | ------------------------------- | 29 | | Marcin Ciura | 1, 4, 10, 23, 57, 132, 301, 701 | 30 | | $2^{k} - 1 $ | 1, 3, 7, 15, 31, 63,... | 31 | | $\lfloor {\frac {N}{2^k}} \rfloor $ | $\lfloor {\frac {N}{2}} \rfloor $, $\lfloor {\frac {N}{4}} \rfloor $, ..., 1| 32 | 33 | 感受一下 gap sequence 為 23, 10, 4, 1 的 shellsort 吧。 34 | 35 | ![](https://upload.wikimedia.org/wikipedia/commons/d/d8/Sorting_shellsort_anim.gif) 36 | 37 | ## 說明 38 | 39 | Shellsort 其實就是進行好幾次不同 gap 的 insertion sort,以下用 ASCII diagram 解釋。 40 | 41 | 假定這裡有一個序列需要遞增排序。 42 | 43 | ``` 44 | [5, 3, 8, 7, 4, 9, 6, 2] 45 | ``` 46 | 47 | 我們選擇最簡單的 $\lfloor {\frac {N}{2^k}} \rfloor $ gap sequence 來排序。我們以**星號**標示出每次 insertion sort 對應排序 48 | 49 | 首先算出第一個 gap 為 $8 / 2^1 = 4 $。開始 insertion sort。 50 | 51 | ``` 52 | * * 53 | [5, 3, 8, 7, 4, 9, 6, 2] 54 | 55 | -> (sort subsequence [5, 4]) 56 | 57 | * * 58 | [4, 3, 8, 7, 5, 9, 6, 2] 59 | 60 | -> (skip) 61 | * * 62 | [4, 3, 8, 7, 5, 9, 6, 2] 63 | 64 | -> (sort subsequence [8, 6]) 65 | * * 66 | [4, 3, 6, 7, 5, 9, 8, 2] 67 | 68 | -> (sort subsequence [7, 2]) 69 | 70 | [4, 3, 8, 2, 5, 9, 6, 7] 71 | ``` 72 | 73 | 再來算出第二個 gap 為 $8 / 2^2 = 2 $。開始 insertion sort。 74 | 75 | ``` 76 | * * 77 | [4, 3, 8, 2, 5, 9, 6, 7] 78 | 79 | -> (skip) 80 | * * 81 | [4, 3, 8, 2, 5, 9, 6, 7] 82 | 83 | -> (sort subsequence [3, 2]) 84 | * * * 85 | [4, 2, 8, 3, 5, 9, 6, 7] 86 | 87 | -> (sort subsequence [4, 8, 5]) 88 | * * * 89 | [4, 2, 5, 3, 8, 9, 6, 7] 90 | 91 | -> (skip) 92 | * * * * 93 | [4, 2, 5, 3, 8, 9, 6, 7] 94 | 95 | -> (sort subsequence [4, 5, 8, 6]) 96 | * * * * 97 | [4, 2, 5, 3, 6, 9, 8, 7] 98 | 99 | -> (sort subsequence [2, 3, 9, 7]) 100 | [4, 2, 5, 3, 6, 7, 8, 9] 101 | ``` 102 | 103 | 再來進行第三次排序。gap 為 $8 / 2^3 = 1 $,shellsort 退化至 insertion sort,但前一次結果已經很接近排序完成,insertion sort 可以幾乎在 one pass 完成排序。 104 | 105 | > Insertion sort 的 ASCII diagram 我們就不展示了,請參考 [Insertion sort](../insertion_sort)。 106 | 107 | ## 效能 108 | 109 | | | Complexity | 110 | | ------------ | ----------------------------------------------------- | 111 | | Worst | $O(n^2) $ ~ $O(n \log^2 n) $ (Depends on gap sequence) | 112 | | Best | $O(n \log n) $ | 113 | | Average | Depends on gap sequence | 114 | | Worst space | $O(1) $ auxiliary | 115 | 116 | Shellsort 的複雜度不容易計算,取決於 gap sequence 怎麼安排,太少 gap 會讓速度太接近 insertion sort,太多 gap 則會有過多額外開銷。目前已知的 gap sequence 中,最差時間複雜度可以達到 $O(n \log^2 n) $,有著不錯的表現。有興趣可以參考[這篇文章][best-sequence]。 117 | 118 | ## 實作 119 | 120 | 我們這裡以 [Marcin 的 Paper][marcin-sequence-paper] 中提到的經驗式為例,首先,先建立一個 gap sequence 的常數。 121 | 122 | ```rust 123 | /// Marcin Ciura's gap sequence. 124 | pub const MARCIN_GAPS: [usize; 8] = [701, 301, 132, 57, 23, 10, 4, 1]; 125 | ``` 126 | 127 | 再來就是主程式的部分,總共會有三個迴圈, 128 | 129 | - 最外層是疊代 gap sequence, 130 | - 中間層是疊代整個資料序列, 131 | - 內層就是每個元素的插入排序動作。 132 | 133 | ```rust 134 | /// Shellsort 135 | pub fn shellsort(arr: &mut [i32]) { 136 | let len = arr.len(); 137 | for gap in MARCIN_GAPS.iter() { // 1 138 | let mut i = *gap; // 4 139 | while i < len { // 2 140 | let mut j = i; 141 | while j >= *gap && arr[j - gap] > arr[j] { // 3 142 | arr.swap(j - *gap, j); 143 | j -= *gap; 144 | } 145 | i += 1; 146 | } 147 | } 148 | } 149 | ``` 150 | 151 | 1. 最外層的迴圈,利用 `iter()` trait 產生疊代器,疊代 gap sequence。 152 | 2. 中間層迴圈,控制 `i` 是否超出資料序列,以疊代整合資料序列。 153 | 3. 最內層迴圈,執行插入動作,將每個元素置換到正確位置。 154 | 4. 由於 `gap` 的型別是 `&usize`,需透過 `*gap` dereference 得到 `usize` 型別。 155 | 156 | ## 參考資料 157 | 158 | - [Wiki: Shellsort](https://en.wikipedia.org/wiki/Shellsort) 159 | - [Best Increments for the Average Case of Shellsort, M. Ciura, 2001][marcin-sequence-paper] 160 | - [Shellsort and Sorting Networks (Outstanding Dissertations in the Computer Sciences)][best-sequence] 161 | 162 | [best-sequence]: http://www.dtic.mil/get-tr-doc/pdf?AD=AD0740110 163 | [marcin-sequence-paper]: http://sun.aei.polsl.pl/~mciura/publikacje/shellsort.pdf 164 | 165 | -------------------------------------------------------------------------------- /src/sorting/shellsort/mod.rs: -------------------------------------------------------------------------------- 1 | /// Marcin Ciura's gap sequence. 2 | pub const MARCIN_GAPS: [usize; 8] = [701, 301, 132, 57, 23, 10, 4, 1]; 3 | 4 | /// Shellsort 5 | pub fn shellsort(arr: &mut [i32]) { 6 | let len = arr.len(); 7 | for gap in MARCIN_GAPS.iter() { 8 | let mut i = *gap; // Type of gap is `&usize`. Deference it! 9 | while i < len { 10 | let mut j = i; 11 | while j >= *gap && arr[j - gap] > arr[j] { 12 | arr.swap(j - *gap, j); 13 | j -= *gap; 14 | } 15 | i += 1; 16 | } 17 | } 18 | } 19 | 20 | #[cfg(test)] 21 | mod base { 22 | use super::*; 23 | base_cases!(shellsort); 24 | } 25 | -------------------------------------------------------------------------------- /src/sorting/test_cases.rs: -------------------------------------------------------------------------------- 1 | /// Common test cases for any sorting algorithms accepting i32 array as input. 2 | macro_rules! base_cases { 3 | ($algo:ident) => { 4 | fn assert(arr: &mut [i32], res: &[i32]) { 5 | $algo(arr); 6 | assert_eq!(arr, res); 7 | } 8 | 9 | #[test] 10 | fn empty() { 11 | let mut arr = []; 12 | let res = []; 13 | assert(&mut arr, &res); 14 | } 15 | 16 | #[test] 17 | fn one_element() { 18 | let mut arr = [1]; 19 | let res = [1]; 20 | assert(&mut arr, &res); 21 | } 22 | 23 | #[test] 24 | fn two_elements() { 25 | let mut arr = [1, 2]; 26 | let res = [1, 2]; 27 | assert(&mut arr, &res); 28 | 29 | let mut arr = [2, 1]; 30 | let res = [1, 2]; 31 | assert(&mut arr, &res); 32 | 33 | let mut arr = [1, 1]; 34 | let res = [1, 1]; 35 | assert(&mut arr, &res); 36 | } 37 | 38 | #[test] 39 | fn three_elements() { 40 | let mut arr = [1, 1, 2]; 41 | let res = [1, 1, 2]; 42 | assert(&mut arr, &res); 43 | 44 | let mut arr = [2, 1, 1]; 45 | let res = [1, 1, 2]; 46 | assert(&mut arr, &res); 47 | 48 | let mut arr = [1, 2, 1]; 49 | let res = [1, 1, 2]; 50 | assert(&mut arr, &res); 51 | 52 | let mut arr = [1, 2, 3]; 53 | let res = [1, 2, 3]; 54 | assert(&mut arr, &res); 55 | 56 | let mut arr = [1, 3, 2]; 57 | let res = [1, 2, 3]; 58 | assert(&mut arr, &res); 59 | 60 | let mut arr = [2, 1, 3]; 61 | let res = [1, 2, 3]; 62 | assert(&mut arr, &res); 63 | 64 | let mut arr = [2, 3, 1]; 65 | let res = [1, 2, 3]; 66 | assert(&mut arr, &res); 67 | 68 | let mut arr = [3, 1, 2]; 69 | let res = [1, 2, 3]; 70 | assert(&mut arr, &res); 71 | 72 | let mut arr = [3, 2, 1]; 73 | let res = [1, 2, 3]; 74 | assert(&mut arr, &res); 75 | } 76 | 77 | #[test] 78 | fn already_sorted() { 79 | let mut arr = [1, 2, 3, 4, 5]; 80 | let res = [1, 2, 3, 4, 5]; 81 | assert(&mut arr, &res); 82 | } 83 | 84 | #[test] 85 | fn reversed() { 86 | let mut arr = [5, 4, 3, 2, 1]; 87 | let res = [1, 2, 3, 4, 5]; 88 | assert(&mut arr, &res); 89 | } 90 | 91 | #[test] 92 | fn all_equal() { 93 | let mut arr = [1, 1, 1, 1, 1]; 94 | let res = [1, 1, 1, 1, 1]; 95 | assert(&mut arr, &res); 96 | } 97 | 98 | #[test] 99 | fn duplicate() { 100 | let mut arr = [1, 5, 3, 3, 4, 1, 3, 4]; 101 | let res = [1, 1, 3, 3, 3, 4, 4, 5]; 102 | assert(&mut arr, &res); 103 | 104 | let mut arr = [3, 1, 3, 3, 3, 2, 2, 1, 2, 1, 2, 3, 3, 2, 1]; 105 | let res = [1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3]; 106 | assert(&mut arr, &res); 107 | } 108 | 109 | #[test] 110 | fn even_length() { 111 | let mut arr = [8, 7, 1, 2, 4, 6, 5, 3]; 112 | let res = [1, 2, 3, 4, 5, 6, 7, 8]; 113 | assert(&mut arr, &res); 114 | } 115 | 116 | #[test] 117 | fn odd_length() { 118 | let mut arr = [7, 1, 2, 4, 6, 5, 3]; 119 | let res = [1, 2, 3, 4, 5, 6, 7]; 120 | assert(&mut arr, &res); 121 | } 122 | }; 123 | } 124 | 125 | /// Test cases for validate stability of sorting algorithm. 126 | /// The input value is a tuple type of (i32, i32). 127 | /// 128 | /// To test with these cases, an algorithm must accept generic input values. 129 | macro_rules! stability_cases { 130 | ($algo:ident) => { 131 | /// (key, value) 132 | fn assert_stability(arr: &mut [(i32, i32)], res: &[(i32, i32)]) { 133 | $algo(arr); 134 | assert_eq!(arr, res); 135 | } 136 | 137 | #[test] 138 | fn random() { 139 | let mut arr = [ 140 | (2, 1), 141 | (1, 1), 142 | (3, 1), 143 | (2, 2), 144 | (2, 3), 145 | (3, 2), 146 | (1, 2), 147 | (1, 3), 148 | (3, 3), 149 | (3, 4), 150 | (1, 4), 151 | (1, 5), 152 | (3, 5), 153 | (2, 4), 154 | (2, 5), 155 | ]; 156 | let res = [ 157 | (1, 1), 158 | (1, 2), 159 | (1, 3), 160 | (1, 4), 161 | (1, 5), 162 | (2, 1), 163 | (2, 2), 164 | (2, 3), 165 | (2, 4), 166 | (2, 5), 167 | (3, 1), 168 | (3, 2), 169 | (3, 3), 170 | (3, 4), 171 | (3, 5), 172 | ]; 173 | assert_stability(&mut arr, &res); 174 | } 175 | 176 | #[test] 177 | fn interleave() { 178 | let mut arr = [ 179 | (1, 1), 180 | (2, 1), 181 | (3, 1), 182 | (1, 2), 183 | (2, 2), 184 | (3, 2), 185 | (1, 3), 186 | (2, 3), 187 | (3, 3), 188 | (1, 4), 189 | (2, 4), 190 | (3, 4), 191 | (1, 5), 192 | (2, 5), 193 | (3, 5), 194 | ]; 195 | let res = [ 196 | (1, 1), 197 | (1, 2), 198 | (1, 3), 199 | (1, 4), 200 | (1, 5), 201 | (2, 1), 202 | (2, 2), 203 | (2, 3), 204 | (2, 4), 205 | (2, 5), 206 | (3, 1), 207 | (3, 2), 208 | (3, 3), 209 | (3, 4), 210 | (3, 5), 211 | ]; 212 | assert_stability(&mut arr, &res); 213 | } 214 | }; 215 | } 216 | -------------------------------------------------------------------------------- /src/sorting/timsort/README.md: -------------------------------------------------------------------------------- 1 | # 自適應合併排序 Timsort 2 | 3 | 🚧 🚧 🚧 TBD 🚧 🚧 🚧 4 | 5 | ## 效能 6 | 7 | | | Complexity | 8 | | ------------ | ------------------ | 9 | | Worst | $O(n \log n) $ | 10 | | Best | $O(n) $ | 11 | | Average | $O(n \log n) $ | 12 | | Worst space | $O(n) $ auxiliary | 13 | 14 | ## 實作 15 | 16 | ## 參考資料 17 | 18 | - [Wiki: Timsort](https://en.wikipedia.org/wiki/Timsort) 19 | - http://blog.csdn.net/yangzhongblog/article/details/8184707 20 | - https://github.com/rust-lang/rust/pull/38192 21 | - https://github.com/python/cpython/blob/master/Objects/listsort.txt 22 | - https://youtu.be/uVWGZyekGos 23 | -------------------------------------------------------------------------------- /src/sorting/timsort/mod.rs: -------------------------------------------------------------------------------- 1 | /// Timsort. 2 | pub fn timsort(_arr: &mut [i32]) { 3 | unimplemented!() 4 | } 5 | -------------------------------------------------------------------------------- /theme/custom.js: -------------------------------------------------------------------------------- 1 | MathJax.Hub.Config({ 2 | tex2jax: { 3 | inlineMath: [['$','$']], 4 | }, 5 | }); 6 | 7 | -------------------------------------------------------------------------------- /theme/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/rust-algorithm-club/1954301f21d44c18ea44864a776a3f6fd72eaefe/theme/favicon.png --------------------------------------------------------------------------------