diff --git a/README.md b/README.md index 2e98d4d..843b515 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ The architectural choices, trade-offs, and design patterns for each algorithm ar * [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) +* [ADR 0019: Iterative Midpoint Bisection for Binary Search](docs/adr/0019-use-iterative-midpoint-bisection-for-binary-search.md) --- @@ -122,4 +123,5 @@ To ensure uniformity, this repository follows strict standards derived from **PE 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. -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 +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. +18. **Precondition Responsibility:** Binary Search assumes sorted input and does not validate it; callers must guarantee sortedness themselves, since verifying it would negate the algorithm's logarithmic performance advantage. \ No newline at end of file diff --git a/ROADMAP.md b/ROADMAP.md index 46d8bf9..198f995 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -38,6 +38,16 @@ This document serves as the long-term architectural roadmap for this learning re * **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. +* **Binary Search**: Divide-and-conquer $O(\log n)$ lookup resolving a target's position within a sorted array. (Completed) + +### Phase 7: Graph Traversal & Minimum Spanning Trees +* **Breadth-First Search (BFS)**: Queue-driven level-order graph traversal resolving shortest unweighted paths and reachability. +* **Depth-First Search (DFS)**: Stack/recursion-driven graph traversal resolving connectivity, cycle detection, and ordering. +* **Kruskal's Algorithm**: Greedy edge-sorted minimum spanning tree construction built atop the existing Union-Find structure. + +### Phase 8: Numerical Methods & Ranking Algorithms +* **PageRank (Power Iteration)**: Iterative eigenvector approximation ranking nodes by weighted incoming link importance. +* **Fast Inverse Square Root**: Bit-level floating-point approximation technique accelerating $1/\sqrt{x}$ via a single Newton-Raphson refinement step. --- diff --git a/docs/adr/0019-use-iterative-midpoint-bisection-for-binary-search.md b/docs/adr/0019-use-iterative-midpoint-bisection-for-binary-search.md new file mode 100644 index 0000000..6161db0 --- /dev/null +++ b/docs/adr/0019-use-iterative-midpoint-bisection-for-binary-search.md @@ -0,0 +1,10 @@ +# 19. Use Iterative Midpoint Bisection for Binary Search + +* **Status:** Approved +* **Context:** Phase 6 required a foundational $O(\log n)$ lookup mechanism as a baseline searching primitive, applicable whenever data is already sorted (as opposed to linear O(n) scanning). +* **Decision:** We implemented **Binary Search** iteratively, using `low + (high - low) // 2` to compute the midpoint (rather than `(low + high) // 2`) and narrowing the search window each iteration based on comparison against the target. +* **Consequences:** + * Achieves $O(\log n)$ time and $O(1)$ space, since the iterative approach avoids any recursive call-stack growth. + * The `low + (high - low) // 2` midpoint formula is a defensive habit carried over from languages with fixed-width integers (where `low + high` can overflow); it is unnecessary in Python's arbitrary-precision integers but costs nothing and keeps the implementation portable as a reference pattern. + * *Trade-off:* The function assumes its input is already sorted and provides no validation of that precondition — enforcing sortedness would cost an extra $O(n)$ pass, defeating the purpose of using binary search in the first place. Callers are responsible for ensuring sorted input. + diff --git a/service/http_app.py b/service/http_app.py index e8cdb21..c061d44 100644 --- a/service/http_app.py +++ b/service/http_app.py @@ -31,6 +31,11 @@ class SortRequest(BaseModel): values: List[int] +class BinarySearchRequest(BaseModel): + sorted_values: List[int] + target: int + + class KmpSearchRequest(BaseModel): text: str pattern: str @@ -122,6 +127,12 @@ def sort_heap_sort(request: SortRequest) -> List[int]: return _call(tools.sort_heap_sort, request.values) +@app.post("/searching/binary-search") +def search_binary_search(request: BinarySearchRequest) -> Dict: + """Returns the index of a target within a sorted array, or -1 if absent.""" + return _call(tools.search_binary_search, request.sorted_values, request.target) + + @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 5359051..1c5988e 100644 --- a/service/mcp_server.py +++ b/service/mcp_server.py @@ -39,6 +39,12 @@ def sort_heap_sort(values: List[int]) -> List[int]: return tools.sort_heap_sort(values) +@mcp.tool() +def search_binary_search(sorted_values: List[int], target: int) -> Dict: + """Returns the index of `target` within a sorted array, or -1 if absent.""" + return tools.search_binary_search(sorted_values, target) + + @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 4e7efa5..74102c8 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.searching.binary_search import binary_search from src.sorting.heap_sort import heap_sort from src.sorting.merge_sort import merge_sort from src.sorting.quicksort import quicksort @@ -59,6 +60,11 @@ def sort_heap_sort(values: List[int]) -> List[int]: return heap_sort(values) +def search_binary_search(sorted_values: List[int], target: int) -> Dict: + """Returns the index of `target` within a sorted array, or -1 if absent.""" + return {"index": binary_search(sorted_values, target)} + + 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/searching/__init__.py b/src/searching/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/searching/binary_search.py b/src/searching/binary_search.py new file mode 100644 index 0000000..6217a6f --- /dev/null +++ b/src/searching/binary_search.py @@ -0,0 +1,29 @@ +"""Binary Search module implementing iterative divide-and-conquer lookups on sorted arrays.""" + +from typing import List + + +def binary_search(sorted_array: List[int], target: int) -> int: + """Returns the index of `target` within a sorted array, or -1 if absent. + + The input array must already be sorted in ascending order; behavior on an + unsorted array is undefined. + + Complexity Analysis: + Time Complexity: O(log n) where n = len(sorted_array). + Space Complexity: O(1) — iterative, no recursive call stack growth. + """ + low, high = 0, len(sorted_array) - 1 + + while low <= high: + # Midpoint computed this way avoids integer overflow in lower-level languages + mid = low + (high - low) // 2 + + if sorted_array[mid] == target: + return mid + if sorted_array[mid] < target: + low = mid + 1 + else: + high = mid - 1 + + return -1 diff --git a/tests/test_searching.py b/tests/test_searching.py new file mode 100644 index 0000000..0bc9588 --- /dev/null +++ b/tests/test_searching.py @@ -0,0 +1,32 @@ +"""Comprehensive evaluation suite tracking Binary Search lookup operations.""" + +from src.searching.binary_search import binary_search + + +def test_binary_search_finds_present_values(): + """Verifies correct index resolution across first, middle, and last positions.""" + sorted_array = [1, 3, 5, 7, 9, 11, 13] + + assert binary_search(sorted_array, 1) == 0 + assert binary_search(sorted_array, 7) == 3 + assert binary_search(sorted_array, 13) == 6 + + +def test_binary_search_absent_value_returns_negative_one(): + """Ensures a target not present in the array resolves to -1.""" + sorted_array = [2, 4, 6, 8, 10] + + assert binary_search(sorted_array, 5) == -1 + assert binary_search(sorted_array, -1) == -1 + assert binary_search(sorted_array, 100) == -1 + + +def test_binary_search_empty_array(): + """Ensures searching an empty array returns -1 without error.""" + assert binary_search([], 5) == -1 + + +def test_binary_search_single_element_array(): + """Ensures single-element arrays resolve correctly for both match and mismatch.""" + assert binary_search([42], 42) == 0 + assert binary_search([42], 7) == -1 diff --git a/tests/test_service_http_app.py b/tests/test_service_http_app.py index a9f240c..249ea02 100644 --- a/tests/test_service_http_app.py +++ b/tests/test_service_http_app.py @@ -28,6 +28,23 @@ def test_http_sort_heap_sort(): assert response.json() == [1, 2, 3, 4, 5] +def test_http_search_binary_search(): + """Verifies the Binary Search endpoint returns the correct index, or -1 if absent.""" + response = client.post( + "/searching/binary-search", + json={"sorted_values": [1, 3, 5, 7, 9], "target": 7}, + ) + assert response.status_code == 200 + assert response.json() == {"index": 3} + + response = client.post( + "/searching/binary-search", + json={"sorted_values": [1, 3, 5, 7, 9], "target": 4}, + ) + assert response.status_code == 200 + assert response.json() == {"index": -1} + + 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 7cc6b5c..6f9f268 100644 --- a/tests/test_service_mcp_server.py +++ b/tests/test_service_mcp_server.py @@ -18,6 +18,7 @@ def test_mcp_tool_registry_contains_all_algorithms(): "sort_quicksort", "sort_merge_sort", "sort_heap_sort", + "search_binary_search", "search_kmp", "build_and_query_bst", "build_and_query_avl_tree", @@ -52,6 +53,12 @@ def test_mcp_sort_heap_sort(): assert mcp_server.sort_heap_sort([5, 2, 4, 1, 3]) == [1, 2, 3, 4, 5] +def test_mcp_search_binary_search(): + """Verifies the Binary Search tool returns the correct index, or -1 if absent.""" + assert mcp_server.search_binary_search([1, 3, 5, 7, 9], target=7) == {"index": 3} + assert mcp_server.search_binary_search([1, 3, 5, 7, 9], target=4) == {"index": -1} + + 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 e7f334b..20e5c40 100644 --- a/tests/test_service_tools.py +++ b/tests/test_service_tools.py @@ -20,6 +20,12 @@ def test_sort_heap_sort(): assert tools.sort_heap_sort([5, 2, 4, 1, 3]) == [1, 2, 3, 4, 5] +def test_search_binary_search(): + """Verifies Binary Search wrapper returns the correct index, or -1 if absent.""" + assert tools.search_binary_search([1, 3, 5, 7, 9], target=7) == {"index": 3} + assert tools.search_binary_search([1, 3, 5, 7, 9], target=4) == {"index": -1} + + def test_search_kmp(): """Verifies KMP wrapper returns matching start indices.""" assert tools.search_kmp("ababcababc", "abc") == [2, 7]