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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

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

17 changes: 17 additions & 0 deletions service/http_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down
8 changes: 8 additions & 0 deletions service/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
18 changes: 18 additions & 0 deletions service/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
106 changes: 106 additions & 0 deletions src/data_structures/linked_list.py
Original file line number Diff line number Diff line change
@@ -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
93 changes: 92 additions & 1 deletion tests/test_data_structures.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
10 changes: 10 additions & 0 deletions tests/test_service_http_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
9 changes: 9 additions & 0 deletions tests/test_service_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": []}
Expand Down
14 changes: 14 additions & 0 deletions tests/test_service_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": []}
Expand Down
Loading