From 86f7b3ee67fc438f2abcf168f1858543ce031490 Mon Sep 17 00:00:00 2001 From: sjamal <3092856+sjamal@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:37:55 -0400 Subject: [PATCH] feat: add Heap Sort algorithm (Phase 6) - Add src/sorting/heap_sort.py implementing in-place binary max-heap construction and repeated root-extraction sorting, guaranteeing O(n log n) worst-case time with O(1) auxiliary space. - Add Heap Sort tests to tests/test_sorting.py covering typical, boundary, pre/reverse-sorted, duplicate, and non-mutation scenarios, with full coverage. - Expose Heap Sort via the service layer: sort_heap_sort wrapper in service/tools.py, MCP tool, and POST /sorting/heap-sort endpoint, each with corresponding tests. - Add ADR 0018 documenting the binary max-heap design vs. Quicksort and Merge Sort trade-offs. - Update README and ROADMAP; first Phase 6 item. --- README.md | 4 +- ROADMAP.md | 2 +- .../0018-use-binary-max-heap-for-heap-sort.md | 9 ++++ service/http_app.py | 6 +++ service/mcp_server.py | 6 +++ service/tools.py | 6 +++ src/sorting/heap_sort.py | 46 +++++++++++++++++++ tests/test_service_http_app.py | 7 +++ tests/test_service_mcp_server.py | 6 +++ tests/test_service_tools.py | 5 ++ tests/test_sorting.py | 28 ++++++++++- 11 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 docs/adr/0018-use-binary-max-heap-for-heap-sort.md create mode 100644 src/sorting/heap_sort.py diff --git a/README.md b/README.md index 5d2daab..2e98d4d 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ The architectural choices, trade-offs, and design patterns for each algorithm ar * [ADR 0015: Iterative Pointer Rewiring for Singly Linked List Reversal](docs/adr/0015-use-iterative-pointer-rewiring-for-singly-linked-list-reversal.md) * [ADR 0016: Bottom-Up Tabulation with Backtracking for 0/1 Knapsack](docs/adr/0016-use-bottom-up-tabulation-with-backtracking-for-01-knapsack.md) * [ADR 0017: Bottom-Up Tabulation with Diagonal Backtracking for LCS](docs/adr/0017-use-bottom-up-tabulation-with-diagonal-backtracking-for-lcs.md) +* [ADR 0018: Binary Max-Heap for Heap Sort](docs/adr/0018-use-binary-max-heap-for-heap-sort.md) --- @@ -120,4 +121,5 @@ To ensure uniformity, this repository follows strict standards derived from **PE 13. **Worst-Case DoS Mitigation:** Merge Sort guarantees $O(n \log n)$ even on adversarial input, making it the safer default over Quicksort when sorting untrusted, attacker-influenced data where worst-case scaling matters. 14. **Bounded Traversal Footprint:** The Singly Linked List's `search`/`delete`/`reverse` operations are strictly O(n) iterative walks with no recursion, preventing stack-depth exhaustion on very large untrusted input lists. 15. **Iterative DP, No Recursion Limits:** The 0/1 Knapsack solver uses bottom-up tabulation rather than top-down recursion, avoiding Python's `RecursionError` on large item counts. -16. **Quadratic Complexity Awareness:** LCS runs in $O(n \times m)$ time and space; callers should bound input string lengths when comparing untrusted, attacker-controlled text to avoid excessive memory allocation on very large inputs. \ No newline at end of file +16. **Quadratic Complexity Awareness:** LCS runs in $O(n \times m)$ time and space; callers should bound input string lengths when comparing untrusted, attacker-controlled text to avoid excessive memory allocation on very large inputs. +17. **In-Place Worst-Case Guarantee:** Heap Sort provides the same $O(n \log n)$ worst-case guarantee as Merge Sort but with $O(1)$ auxiliary space, useful when both adversarial-input resilience and memory constraints matter simultaneously. \ No newline at end of file diff --git a/ROADMAP.md b/ROADMAP.md index b00b813..46d8bf9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -35,7 +35,7 @@ This document serves as the long-term architectural roadmap for this learning re ### Phase 6: Foundational Primitives & Auxiliary Structures * **Trie (Prefix Tree)**: Character-branching tree structure resolving prefix-based word lookups and autocomplete-style queries. -* **Heap Sort**: In-place comparison sort built atop a binary max-heap, contrasting Quicksort/Merge Sort's partitioning and merging strategies. +* **Heap Sort**: In-place comparison sort built atop a binary max-heap, contrasting Quicksort/Merge Sort's partitioning and merging strategies. (Completed) * **Euclidean Algorithm (GCD)**: Iterative remainder-based reduction resolving the greatest common divisor between two integers. * **Valid Parentheses (Stack-Based Matching)**: Stack-tracked bracket balancing validating correctly nested and closed symbol pairs. diff --git a/docs/adr/0018-use-binary-max-heap-for-heap-sort.md b/docs/adr/0018-use-binary-max-heap-for-heap-sort.md new file mode 100644 index 0000000..4ca1a07 --- /dev/null +++ b/docs/adr/0018-use-binary-max-heap-for-heap-sort.md @@ -0,0 +1,9 @@ +# 18. Use a Binary Max-Heap for Heap Sort + +* **Status:** Approved +* **Context:** Phase 6 required an in-place comparison sort guaranteeing $O(n \log n)$ worst-case time without Merge Sort's $O(n)$ auxiliary space, rounding out the sorting toolkit alongside Quicksort (fast average case, in-place) and Merge Sort (stable, guaranteed worst case, extra space). +* **Decision:** We implemented **Heap Sort**: first heapify the array into a binary max-heap (in place, via repeated `_sift_down` calls from the last parent node upward), then repeatedly swap the root (maximum element) to the end of the unsorted region and sift down to restore heap order. +* **Consequences:** + * Guarantees $O(n \log n)$ time in the best, average, and worst case, like Merge Sort, but with only $O(1)$ auxiliary space since the heap is built directly within the array being sorted. + * *Trade-off:* Heap Sort is not stable (equal elements may be reordered during heap restructuring) and has weaker real-world cache locality than Quicksort due to its non-sequential index-jumping access pattern, so it typically runs slower in practice despite matching Merge Sort's worst-case guarantee. + diff --git a/service/http_app.py b/service/http_app.py index 533c9f2..e8cdb21 100644 --- a/service/http_app.py +++ b/service/http_app.py @@ -116,6 +116,12 @@ def sort_merge_sort(request: SortRequest) -> List[int]: return _call(tools.sort_merge_sort, request.values) +@app.post("/sorting/heap-sort") +def sort_heap_sort(request: SortRequest) -> List[int]: + """Sorts a list of integers in ascending order using Heap Sort.""" + return _call(tools.sort_heap_sort, request.values) + + @app.post("/string-matching/kmp") def search_kmp(request: KmpSearchRequest) -> List[int]: """Finds every 0-indexed starting position of a pattern within a text.""" diff --git a/service/mcp_server.py b/service/mcp_server.py index aa05bb0..5359051 100644 --- a/service/mcp_server.py +++ b/service/mcp_server.py @@ -33,6 +33,12 @@ def sort_merge_sort(values: List[int]) -> List[int]: return tools.sort_merge_sort(values) +@mcp.tool() +def sort_heap_sort(values: List[int]) -> List[int]: + """Sorts a list of integers in ascending order using Heap Sort.""" + return tools.sort_heap_sort(values) + + @mcp.tool() def search_kmp(text: str, pattern: str) -> List[int]: """Finds every 0-indexed starting position of `pattern` within `text`.""" diff --git a/service/tools.py b/service/tools.py index b0e5fba..4e7efa5 100644 --- a/service/tools.py +++ b/service/tools.py @@ -26,6 +26,7 @@ from src.machine_learning.kmeans import KMeans from src.machine_learning.pca import PCA from src.numeric.sieve import sieve_of_eratosthenes +from src.sorting.heap_sort import heap_sort from src.sorting.merge_sort import merge_sort from src.sorting.quicksort import quicksort from src.string_matching.kmp import kmp_search @@ -53,6 +54,11 @@ def sort_merge_sort(values: List[int]) -> List[int]: return merge_sort(values) +def sort_heap_sort(values: List[int]) -> List[int]: + """Sorts a list of integers in ascending order via Heap Sort.""" + return heap_sort(values) + + def search_kmp(text: str, pattern: str) -> List[int]: """Finds every 0-indexed starting position of `pattern` within `text`.""" return kmp_search(text, pattern) diff --git a/src/sorting/heap_sort.py b/src/sorting/heap_sort.py new file mode 100644 index 0000000..58aa9f5 --- /dev/null +++ b/src/sorting/heap_sort.py @@ -0,0 +1,46 @@ +"""Heap Sort module implementing in-place, comparison-based array sorting via a binary max-heap.""" + +from typing import List + + +def heap_sort(array: List[int]) -> List[int]: + """Sorts an array of integers in ascending order using Heap Sort. + + This function is completely pure and thread-safe: it builds an isolated + shallow copy of the collection to eliminate unexpected external mutable mutations. + + Complexity Analysis: + Time Complexity: O(n log n) in the best, average, and worst cases. + Space Complexity: O(1) auxiliary space (sorting occurs in place on the copy). + """ + # Defensive programming: isolate state from side effects + arr_copy = list(array) + n = len(arr_copy) + + # Build a max-heap: start from the last parent node and sift downward + for root in range(n // 2 - 1, -1, -1): + _sift_down(arr_copy, n, root) + + # Repeatedly swap the max element to the end, then restore heap order + for end in range(n - 1, 0, -1): + arr_copy[0], arr_copy[end] = arr_copy[end], arr_copy[0] + _sift_down(arr_copy, end, 0) + + return arr_copy + + +def _sift_down(array: List[int], heap_size: int, root: int) -> None: + """Restores the max-heap property for the subtree rooted at `root`.""" + largest = root + left = 2 * root + 1 + right = 2 * root + 2 + + if left < heap_size and array[left] > array[largest]: + largest = left + if right < heap_size and array[right] > array[largest]: + largest = right + + if largest != root: + array[root], array[largest] = array[largest], array[root] + # Continue sifting downward since the swapped subtree may now violate heap order + _sift_down(array, heap_size, largest) diff --git a/tests/test_service_http_app.py b/tests/test_service_http_app.py index eee285f..a9f240c 100644 --- a/tests/test_service_http_app.py +++ b/tests/test_service_http_app.py @@ -21,6 +21,13 @@ def test_http_sort_merge_sort(): assert response.json() == [1, 2, 3, 4, 5] +def test_http_sort_heap_sort(): + """Verifies the Heap Sort endpoint returns an ascending-order list.""" + response = client.post("/sorting/heap-sort", json={"values": [5, 2, 4, 1, 3]}) + assert response.status_code == 200 + assert response.json() == [1, 2, 3, 4, 5] + + def test_http_search_kmp(): """Verifies the KMP search endpoint returns matching start indices.""" response = client.post( diff --git a/tests/test_service_mcp_server.py b/tests/test_service_mcp_server.py index 72c2788..7cc6b5c 100644 --- a/tests/test_service_mcp_server.py +++ b/tests/test_service_mcp_server.py @@ -17,6 +17,7 @@ def test_mcp_tool_registry_contains_all_algorithms(): assert registered_names == { "sort_quicksort", "sort_merge_sort", + "sort_heap_sort", "search_kmp", "build_and_query_bst", "build_and_query_avl_tree", @@ -46,6 +47,11 @@ def test_mcp_sort_merge_sort(): assert mcp_server.sort_merge_sort([5, 2, 4, 1, 3]) == [1, 2, 3, 4, 5] +def test_mcp_sort_heap_sort(): + """Verifies the Heap Sort tool returns an ascending-order list.""" + assert mcp_server.sort_heap_sort([5, 2, 4, 1, 3]) == [1, 2, 3, 4, 5] + + def test_mcp_search_kmp(): """Verifies the KMP search tool returns matching start indices.""" assert mcp_server.search_kmp("ababcababc", "abc") == [2, 7] diff --git a/tests/test_service_tools.py b/tests/test_service_tools.py index 4c18017..e7f334b 100644 --- a/tests/test_service_tools.py +++ b/tests/test_service_tools.py @@ -15,6 +15,11 @@ def test_sort_merge_sort(): assert tools.sort_merge_sort([5, 2, 4, 1, 3]) == [1, 2, 3, 4, 5] +def test_sort_heap_sort(): + """Verifies Heap Sort wrapper returns an ascending-order list.""" + assert tools.sort_heap_sort([5, 2, 4, 1, 3]) == [1, 2, 3, 4, 5] + + def test_search_kmp(): """Verifies KMP wrapper returns matching start indices.""" assert tools.search_kmp("ababcababc", "abc") == [2, 7] diff --git a/tests/test_sorting.py b/tests/test_sorting.py index 93d510b..e5b41c5 100644 --- a/tests/test_sorting.py +++ b/tests/test_sorting.py @@ -1,7 +1,8 @@ -"""Comprehensive evaluation suite tracking Quicksort and Merge Sort performance scenarios.""" +"""Comprehensive evaluation suite tracking Quicksort, Merge Sort, and Heap Sort performance scenarios.""" from src.sorting.quicksort import quicksort from src.sorting.merge_sort import merge_sort +from src.sorting.heap_sort import heap_sort def test_quicksort_permutations(): @@ -52,3 +53,28 @@ def test_merge_sort_is_stable(): entries = [(2, "a"), (1, "b"), (2, "c"), (1, "d")] assert merge_sort(entries) == [(1, "b"), (1, "d"), (2, "a"), (2, "c")] + + +def test_heap_sort_permutations(): + """Validates structural sorting accuracy across diverse edge cases.""" + # Standard array checks + assert heap_sort([3, 1, 4, 1, 5, 9, 2, 6]) == [1, 1, 2, 3, 4, 5, 6, 9] + + # Boundary conditions: empty and single-element lists + assert heap_sort([]) == [] + assert heap_sort([42]) == [42] + + # Pre-sorted and reverse-sorted sequences + assert heap_sort([1, 2, 3, 4, 5]) == [1, 2, 3, 4, 5] + assert heap_sort([5, 4, 3, 2, 1]) == [1, 2, 3, 4, 5] + + # Large duplicate cluster evaluation + assert heap_sort([2, 2, 2, 2]) == [2, 2, 2, 2] + + +def test_heap_sort_does_not_mutate_input(): + """Ensures the original input list is left untouched, confirming pure-function behavior.""" + original = [3, 1, 2] + heap_sort(original) + + assert original == [3, 1, 2]