Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down Expand Up @@ -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.
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.
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
9 changes: 9 additions & 0 deletions docs/adr/0018-use-binary-max-heap-for-heap-sort.md
Original file line number Diff line number Diff line change
@@ -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.

6 changes: 6 additions & 0 deletions service/http_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
6 changes: 6 additions & 0 deletions service/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`."""
Expand Down
6 changes: 6 additions & 0 deletions service/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
46 changes: 46 additions & 0 deletions src/sorting/heap_sort.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 7 additions & 0 deletions tests/test_service_http_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 6 additions & 0 deletions tests/test_service_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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]
Expand Down
5 changes: 5 additions & 0 deletions tests/test_service_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
28 changes: 27 additions & 1 deletion tests/test_sorting.py
Original file line number Diff line number Diff line change
@@ -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():
Expand Down Expand Up @@ -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]
Loading