diff --git a/Sprint-2/implement_linked_list/CHANGES-MADE.md b/Sprint-2/implement_linked_list/CHANGES-MADE.md new file mode 100644 index 0000000..020399f --- /dev/null +++ b/Sprint-2/implement_linked_list/CHANGES-MADE.md @@ -0,0 +1,16 @@ +# Changes Made: Doubly Linked List Implementation + +## 1. Core Logic (`linked_list.py`) +- **Node Class**: Created a `Node` class to act as the building block of the list. Each node stores a `value` and has pointers for `next` and `previous`, allowing for bi-directional traversal. +- **LinkedList Class**: + - Implemented `push_head(value)`: Adds a new node to the beginning of the list in **O(1)** time. + - Implemented `pop_tail()`: Removes and returns the value from the end of the list in **O(1)** time. Includes logic for empty lists and single-node lists. + - Implemented `remove(node)`: Removes a specific node from the list and re-wires the surrounding nodes' pointers. Handles edge cases where the node is the current `head` or `tail`. + +## 2. Testing & Verification (`linked_list_test.py`) +- **Middle Removal**: Added `test_remove_middle` to verify that when a node in the center of the list is removed, the nodes ahead and behind it correctly link to each other. +- **Edge Case - Empty Pop**: Added `test_pop_empty_list` to ensure the program returns `None` rather than crashing when attempting to remove from an empty list. +- **Edge Case - Null Removal**: Added `test_remove_none` to verify the `remove` method handles `None` inputs gracefully. + +## 3. Technical Trade-offs +- **Space vs. Time**: Used a Doubly Linked List structure, trading slightly more memory (for the `previous` pointer) to achieve faster removal and tail-access times **`O(1)`**. \ No newline at end of file diff --git a/Sprint-2/implement_linked_list/linked_list.py b/Sprint-2/implement_linked_list/linked_list.py index e69de29..559f0b1 100644 --- a/Sprint-2/implement_linked_list/linked_list.py +++ b/Sprint-2/implement_linked_list/linked_list.py @@ -0,0 +1,69 @@ +from typing import Any, Optional + +class Node: + """ + Represents a single person in line. + Stores a value and links to the people ahead and behind. + """ + __slots__ = ['value', 'next', 'previous'] + + def __init__(self, value: Any): + self.value = value + self.next: Optional['Node'] = None + self.previous: Optional['Node'] = None + + +class LinkedList: + """ + Manages the line by keeping track of the Head and the Tail. + """ + def __init__(self): + self.head: Optional[Node] = None + self.tail: Optional[Node] = None + + def push_head(self, value: Any) -> Node: + """Adds a new node to the front of the list.""" + new_node = Node(value) + + if self.head is None: + self.head = new_node + self.tail = new_node + + else: + new_node.next = self.head + self.head.previous = new_node + self.head = new_node + + return new_node + + def pop_tail(self) -> Optional[Any]: + """Removes the last node and returns its value.""" + + if self.tail is None: + return None + + value_to_return = self.tail.value + + self.remove(self.tail) + + return value_to_return + + def remove(self, node: Optional[Node]) -> None: + """Removes a specific node from anywhere in the list.""" + if node is None: + return + + if node == self.head: + self.head = node.next + + if node == self.tail: + self.tail = node.previous + + if node.previous is not None: + node.previous.next = node.next + + if node.next is not None: + node.next.previous = node.previous + + node.next = None + node.previous = None \ No newline at end of file diff --git a/Sprint-2/implement_linked_list/linked_list_test.py b/Sprint-2/implement_linked_list/linked_list_test.py index d59d9c5..fb9a02c 100644 --- a/Sprint-2/implement_linked_list/linked_list_test.py +++ b/Sprint-2/implement_linked_list/linked_list_test.py @@ -34,6 +34,34 @@ def test_remove_tail(self): self.assertIsNone(b.next) self.assertIsNone(b.previous) + def test_remove_middle(self): + l = LinkedList() + l.push_head("tail_node") + middle = l.push_head("middle_node") + l.push_head("head_node") + + l.remove(middle) + + self.assertEqual(l.head.value, "head_node") + self.assertEqual(l.tail.value, "tail_node") + self.assertEqual(l.head.next.value, "tail_node") + self.assertEqual(l.tail.previous.value, "head_node") + + def test_pop_empty_list(self): + l = LinkedList() + # If the list is empty, popping the tail should return None + # instead of crashing the program with an error. + result = l.pop_tail() + + self.assertIsNone(result) + + def test_remove_none(self): + l = LinkedList() + l.push_head("a") + # This should just do nothing and not crash + l.remove(None) + + self.assertEqual(l.pop_tail(), "a") if __name__ == "__main__": unittest.main()