From 068e0f20236291a2b0d163cf4e731c523fe7c85b Mon Sep 17 00:00:00 2001 From: sjamal <3092856+sjamal@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:20:25 -0400 Subject: [PATCH] feat: add Singly Linked List data structure (Phase 4) - Add src/data_structures/linked_list.py implementing a head/tail tracked singly linked list with O(1) append/prepend and iterative O(n) search, delete, and in-place reversal. - Add Linked List tests to tests/test_data_structures.py covering append/prepend (including empty-list prepend), search, deletion at every position (head/middle/tail/only-node), reversal, and empty-list reversal, with full coverage. - Expose Linked List via the service layer: build_and_query_linked_list wrapper in service/tools.py, MCP tool, and POST /data-structures/linked-list endpoint, each with tests. - Add ADR 0015 documenting the iterative pointer-rewiring design. - Update README and ROADMAP; this completes Phase 4. --- README.md | 2 + ROADMAP.md | 2 +- ...ewiring-for-singly-linked-list-reversal.md | 10 ++ service/http_app.py | 17 +++ service/mcp_server.py | 8 ++ service/tools.py | 18 +++ src/data_structures/linked_list.py | 106 ++++++++++++++++++ tests/test_data_structures.py | 93 ++++++++++++++- tests/test_service_http_app.py | 10 ++ tests/test_service_mcp_server.py | 9 ++ tests/test_service_tools.py | 14 +++ 11 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 docs/adr/0015-use-iterative-pointer-rewiring-for-singly-linked-list-reversal.md create mode 100644 src/data_structures/linked_list.py diff --git a/README.md b/README.md index 232b3b9..b63695a 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ The architectural choices, trade-offs, and design patterns for each algorithm ar * [ADR 0012: Iterative Boolean Marking for Sieve of Eratosthenes](docs/adr/0012-use-iterative-boolean-marking-for-sieve-of-eratosthenes.md) * [ADR 0013: Union by Rank with Path Compression for Union-Find](docs/adr/0013-use-union-by-rank-with-path-compression-for-union-find.md) * [ADR 0014: Bottom-Up Divide-and-Conquer Merge for Merge Sort](docs/adr/0014-use-bottom-up-divide-and-conquer-merge-for-merge-sort.md) +* [ADR 0015: Iterative Pointer Rewiring for Singly Linked List Reversal](docs/adr/0015-use-iterative-pointer-rewiring-for-singly-linked-list-reversal.md) --- @@ -115,3 +116,4 @@ To ensure uniformity, this repository follows strict standards derived from **PE 11. **Service Layer Input Validation:** The HTTP API validates request bodies via Pydantic schemas and translates algorithm-level `ValueError`s into HTTP 400 responses rather than leaking stack traces; both the MCP server and HTTP API are stateless per call, so no client-supplied data persists across requests. 12. **Fixed Element Universe:** Union-Find validates every `find()`/`union()` call against its initial element set and raises a `ValueError` for unknown elements, preventing silent creation of untracked entries. 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. \ No newline at end of file diff --git a/ROADMAP.md b/ROADMAP.md index 83fc5b0..0c99e3f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -27,7 +27,7 @@ This document serves as the long-term architectural roadmap for this learning re ### Phase 4: Foundational Structures & Sorting Alternatives * **Union-Find (Disjoint Set)**: Path-compressed, rank-unioned set tracking structure resolving connectivity queries in near-constant time. (Completed) * **Merge Sort**: Stable divide-and-conquer sorting engine contrasting Quicksort's in-place, non-stable partitioning approach. (Completed) -* **Singly Linked List**: Pointer-chained sequential collection supporting traversal, insertion, and reversal operations. +* **Singly Linked List**: Pointer-chained sequential collection supporting traversal, insertion, and reversal operations. (Completed) ### Phase 5: Dynamic Programming & Sequence Analysis * **0/1 Knapsack Problem**: Tabular matrix memoization framework designed to resolve finite profit boundaries. diff --git a/docs/adr/0015-use-iterative-pointer-rewiring-for-singly-linked-list-reversal.md b/docs/adr/0015-use-iterative-pointer-rewiring-for-singly-linked-list-reversal.md new file mode 100644 index 0000000..bc480cc --- /dev/null +++ b/docs/adr/0015-use-iterative-pointer-rewiring-for-singly-linked-list-reversal.md @@ -0,0 +1,10 @@ +# 15. Use Iterative Pointer Rewiring for Singly Linked List Reversal + +* **Status:** Approved +* **Context:** Phase 4 required a foundational pointer-chained sequential structure as a baseline before more advanced list-based structures, with support for traversal, insertion at both ends, deletion, and in-place reversal. +* **Decision:** We implemented a **Singly Linked List** tracking explicit `head` and `tail` pointers (enabling O(1) `append`/`prepend`), and reversed the list using **iterative pointer rewiring** — walking the chain once while redirecting each node's `next` pointer backward — rather than a recursive approach. +* **Consequences:** + * `append`/`prepend`/`reverse` all run in O(1)/O(1)/O(n) respectively with O(1) auxiliary space, since the iterative reversal avoids the O(n) call-stack depth a recursive implementation would incur. + * Tracking a `tail` pointer requires explicit bookkeeping during `delete` (reassigning `tail` when the removed node was the last one) and `reverse` (the old head becomes the new tail), trading a small amount of extra logic for O(1) append performance. + * *Trade-off:* As a singly-linked (not doubly-linked) structure, there is no O(1) backward traversal or O(1) arbitrary-node deletion without first locating the node via a full scan; this keeps the structure simple and memory-efficient for the append/prepend/reverse-focused use case it targets. + diff --git a/service/http_app.py b/service/http_app.py index 1875b54..da18200 100644 --- a/service/http_app.py +++ b/service/http_app.py @@ -47,6 +47,12 @@ class UnionFindRequest(BaseModel): query: Optional[List[str]] = None +class LinkedListRequest(BaseModel): + values: List[int] + search_for: Optional[int] = None + reverse: bool = False + + class WeightedGraphRequest(BaseModel): graph: Dict[str, List[List[object]]] source: str @@ -128,6 +134,17 @@ def build_and_query_union_find(request: UnionFindRequest) -> Dict: ) +@app.post("/data-structures/linked-list") +def build_and_query_linked_list(request: LinkedListRequest) -> Dict: + """Builds a Singly Linked List, optionally reversing it, and returns its layout.""" + return _call( + tools.build_and_query_linked_list, + request.values, + request.search_for, + request.reverse, + ) + + @app.post("/graphs/dijkstra") def graph_dijkstra(request: WeightedGraphRequest) -> Dict: """Computes single-source shortest paths using Dijkstra's algorithm.""" diff --git a/service/mcp_server.py b/service/mcp_server.py index 64e88c4..e81b5ab 100644 --- a/service/mcp_server.py +++ b/service/mcp_server.py @@ -61,6 +61,14 @@ def build_and_query_union_find( return tools.build_and_query_union_find(elements, unions, query) +@mcp.tool() +def build_and_query_linked_list( + values: List[int], search_for: Optional[int] = None, reverse: bool = False +) -> Dict: + """Builds a Singly Linked List from `values`, optionally reversing it and searching.""" + return tools.build_and_query_linked_list(values, search_for, reverse) + + @mcp.tool() def graph_dijkstra(graph: tools.WeightedGraph, source: str) -> Dict: """Computes single-source shortest paths using Dijkstra's algorithm.""" diff --git a/service/tools.py b/service/tools.py index e9db8ef..3d0b56f 100644 --- a/service/tools.py +++ b/service/tools.py @@ -16,6 +16,7 @@ from src.data_structures.avl_tree import AVLTree from src.data_structures.bst import BinarySearchTree from src.data_structures.dijkstra import dijkstra +from src.data_structures.linked_list import SinglyLinkedList from src.data_structures.union_find import UnionFind from src.graphs.a_star import a_star from src.graphs.bellman_ford import bellman_ford @@ -102,6 +103,23 @@ def build_and_query_union_find( return result +def build_and_query_linked_list( + values: List[int], search_for: Optional[int] = None, reverse: bool = False +) -> Dict: + """Builds a Singly Linked List from `values`, optionally reversing it and searching.""" + linked_list = SinglyLinkedList() + for value in values: + linked_list.append(value) + + if reverse: + linked_list.reverse() + + result: Dict = {"values": linked_list.to_list()} + if search_for is not None: + result["found"] = linked_list.search(search_for) + return result + + def graph_dijkstra(graph: WeightedGraph, source: str) -> Dict: """Computes single-source shortest paths using Dijkstra's algorithm.""" distances, predecessors = dijkstra(_to_adjacency_tuples(graph), source) diff --git a/src/data_structures/linked_list.py b/src/data_structures/linked_list.py new file mode 100644 index 0000000..5eddb2f --- /dev/null +++ b/src/data_structures/linked_list.py @@ -0,0 +1,106 @@ +"""Singly Linked List data structure supporting traversal, insertion, and reversal.""" + +from typing import List, Optional + + +class ListNode: + """Represents a single structural node within a Singly Linked List.""" + + def __init__(self, value: int) -> None: + self.value: int = value + self.next: Optional["ListNode"] = None + + +class SinglyLinkedList: + """Pointer-chained sequential collection supporting O(1) append/prepend and O(n) traversal. + + Complexity Analysis: + Time Complexity: O(1) for prepend/append (head/tail tracked), O(n) for + search, delete, and reversal. + Space Complexity: O(n) for n stored nodes. + """ + + def __init__(self) -> None: + self.head: Optional[ListNode] = None + self.tail: Optional[ListNode] = None + self._size: int = 0 + + def __len__(self) -> int: + return self._size + + def prepend(self, value: int) -> None: + """Inserts a new value at the front of the list in O(1) time.""" + new_node = ListNode(value) + new_node.next = self.head + self.head = new_node + + if self.tail is None: + self.tail = new_node + self._size += 1 + + def append(self, value: int) -> None: + """Inserts a new value at the end of the list in O(1) time.""" + new_node = ListNode(value) + + if self.tail is None: + self.head = self.tail = new_node + else: + self.tail.next = new_node + self.tail = new_node + self._size += 1 + + def search(self, value: int) -> bool: + """Returns True if `value` exists anywhere within the list.""" + current = self.head + while current is not None: + if current.value == value: + return True + current = current.next + return False + + def delete(self, value: int) -> bool: + """Removes the first occurrence of `value`; returns True if a node was removed.""" + previous: Optional[ListNode] = None + current = self.head + + while current is not None: + if current.value == value: + if previous is None: + self.head = current.next + else: + previous.next = current.next + + # Fix up the tail pointer if the removed node was the last one + if current is self.tail: + self.tail = previous + + self._size -= 1 + return True + + previous = current + current = current.next + + return False + + def reverse(self) -> None: + """Reverses the list in place in O(n) time using iterative pointer rewiring.""" + previous: Optional[ListNode] = None + current = self.head + self.tail = self.head + + while current is not None: + next_node = current.next + current.next = previous + previous = current + current = next_node + + self.head = previous + + def to_list(self) -> List[int]: + """Returns the list's values as a standard Python list, head to tail.""" + values: List[int] = [] + current = self.head + while current is not None: + values.append(current.value) + current = current.next + return values diff --git a/tests/test_data_structures.py b/tests/test_data_structures.py index 373e425..9b9f243 100644 --- a/tests/test_data_structures.py +++ b/tests/test_data_structures.py @@ -1,10 +1,11 @@ -"""Comprehensive evaluation suite tracking Dijkstra, BST, AVL Tree, and Union-Find operations.""" +"""Comprehensive evaluation suite tracking Dijkstra, BST, AVL Tree, Union-Find, and Linked List operations.""" import pytest from src.data_structures.dijkstra import dijkstra from src.data_structures.bst import BinarySearchTree from src.data_structures.avl_tree import AVLTree from src.data_structures.union_find import UnionFind +from src.data_structures.linked_list import SinglyLinkedList @pytest.fixture @@ -211,3 +212,93 @@ def test_union_find_attaches_lower_rank_tree_under_higher_rank_root(): uf.union("C", "A") assert uf.find("C") == "A" + + +def test_linked_list_append_and_prepend(): + """Verifies append/prepend build the expected head-to-tail ordering.""" + linked_list = SinglyLinkedList() + linked_list.append(2) + linked_list.append(3) + linked_list.prepend(1) + + assert linked_list.to_list() == [1, 2, 3] + assert len(linked_list) == 3 + assert linked_list.head.value == 1 + assert linked_list.tail.value == 3 + + +def test_linked_list_prepend_on_empty_list_sets_tail(): + """Ensures prepending into an empty list also initializes the tail pointer.""" + linked_list = SinglyLinkedList() + linked_list.prepend(1) + + assert linked_list.head.value == 1 + assert linked_list.tail.value == 1 + + +def test_linked_list_search(): + """Verifies search correctly reports present and absent values.""" + linked_list = SinglyLinkedList() + for value in [10, 20, 30]: + linked_list.append(value) + + assert linked_list.search(20) is True + assert linked_list.search(99) is False + + +def test_linked_list_delete_head_middle_and_tail(): + """Verifies deletion correctly rewires pointers regardless of node position.""" + linked_list = SinglyLinkedList() + for value in [1, 2, 3, 4]: + linked_list.append(value) + + # Delete from the middle + assert linked_list.delete(2) is True + assert linked_list.to_list() == [1, 3, 4] + + # Delete the head + assert linked_list.delete(1) is True + assert linked_list.to_list() == [3, 4] + assert linked_list.head.value == 3 + + # Delete the tail, confirming the tail pointer is fixed up + assert linked_list.delete(4) is True + assert linked_list.to_list() == [3] + assert linked_list.tail.value == 3 + + # Deleting a non-existent value is a safe no-op + assert linked_list.delete(999) is False + + +def test_linked_list_delete_only_node_empties_the_list(): + """Ensures deleting the sole remaining node resets head and tail to None.""" + linked_list = SinglyLinkedList() + linked_list.append(1) + + assert linked_list.delete(1) is True + assert linked_list.head is None + assert linked_list.tail is None + assert len(linked_list) == 0 + + +def test_linked_list_reverse(): + """Verifies in-place reversal flips both traversal order and the tail pointer.""" + linked_list = SinglyLinkedList() + for value in [1, 2, 3, 4]: + linked_list.append(value) + + linked_list.reverse() + + assert linked_list.to_list() == [4, 3, 2, 1] + assert linked_list.head.value == 4 + assert linked_list.tail.value == 1 + + +def test_linked_list_reverse_empty_list_is_a_no_op(): + """Ensures reversing an empty list does not raise and leaves it empty.""" + linked_list = SinglyLinkedList() + linked_list.reverse() + + assert linked_list.to_list() == [] + assert linked_list.head is None + assert linked_list.tail is None diff --git a/tests/test_service_http_app.py b/tests/test_service_http_app.py index 286c60d..87d383d 100644 --- a/tests/test_service_http_app.py +++ b/tests/test_service_http_app.py @@ -65,6 +65,16 @@ def test_http_build_and_query_union_find(): assert response.json() == {"groups": [["A", "B"], ["C"]], "connected": False} +def test_http_build_and_query_linked_list(): + """Verifies the Linked List endpoint builds, optionally reverses, and searches.""" + response = client.post( + "/data-structures/linked-list", + json={"values": [1, 2, 3], "search_for": 2, "reverse": True}, + ) + assert response.status_code == 200 + assert response.json() == {"values": [3, 2, 1], "found": True} + + def test_http_graph_dijkstra(): """Verifies the Dijkstra endpoint computes shortest path distances.""" response = client.post( diff --git a/tests/test_service_mcp_server.py b/tests/test_service_mcp_server.py index cd9b523..4938d63 100644 --- a/tests/test_service_mcp_server.py +++ b/tests/test_service_mcp_server.py @@ -21,6 +21,7 @@ def test_mcp_tool_registry_contains_all_algorithms(): "build_and_query_bst", "build_and_query_avl_tree", "build_and_query_union_find", + "build_and_query_linked_list", "graph_dijkstra", "graph_bellman_ford", "graph_a_star", @@ -72,6 +73,14 @@ def test_mcp_build_and_query_union_find(): assert result == {"groups": [["A", "B"], ["C"]], "connected": False} +def test_mcp_build_and_query_linked_list(): + """Verifies the Linked List tool builds, optionally reverses, and searches.""" + result = mcp_server.build_and_query_linked_list( + [1, 2, 3], search_for=2, reverse=True + ) + assert result == {"values": [3, 2, 1], "found": True} + + def test_mcp_graph_dijkstra(): """Verifies the Dijkstra tool computes shortest path distances.""" graph = {"A": [["B", 1]], "B": [["C", 2]], "C": []} diff --git a/tests/test_service_tools.py b/tests/test_service_tools.py index 3ab8c76..90920f6 100644 --- a/tests/test_service_tools.py +++ b/tests/test_service_tools.py @@ -59,6 +59,20 @@ def test_build_and_query_union_find_without_query(): assert result["groups"] == [["A", "B"]] +def test_build_and_query_linked_list(): + """Verifies Linked List wrapper builds, optionally reverses, and searches.""" + result = tools.build_and_query_linked_list([1, 2, 3], search_for=2, reverse=True) + assert result["values"] == [3, 2, 1] + assert result["found"] is True + + +def test_build_and_query_linked_list_without_search_or_reverse(): + """Ensures default behavior omits 'found' and preserves insertion order.""" + result = tools.build_and_query_linked_list([1, 2, 3]) + assert result["values"] == [1, 2, 3] + assert "found" not in result + + def test_graph_dijkstra(): """Verifies Dijkstra wrapper converts JSON edge lists and computes distances.""" graph = {"A": [["B", 1]], "B": [["C", 2]], "C": []}