| name | algo-linked-lists |
| description | Implement singly/doubly linked lists in Python (O(1) head/tail insert, delete, reverse) plus pointer problems like Floyd's cycle detection and merge-sorted-lists. Use for linked-list coding-interview questions. |
| tool_type | python |
| primary_tool | Python |
Linked Lists
When to Use
- Implementing a singly or doubly linked list from scratch (no built-in Python list allowed)
- Explaining why arrays beat linked lists for random access but lose on head insertion
- Solving classic pointer-manipulation interview problems: cycle detection, find middle, merge two sorted lists, remove n-th from end, palindrome check
- Building a data structure with O(1) insert/delete at known positions (e.g., an LRU cache's backing store, which needs a doubly linked list)
- Teaching/reviewing slow-fast (tortoise-and-hare) pointer technique
Version Compatibility
Pure Python standard library only — no third-party dependencies. Works on Python ≥3.8 (uses typing.Optional); on Python ≥3.10 you can replace Optional['Node'] with Node | None.
Prerequisites
- Comfort with Python classes and references/aliasing (a node's
next is a reference, not a copy)
- Big-O notation — see
algo-complexity-analysis
- No external packages needed
Array vs Linked List
| Feature | Array | Linked List |
|---|
| Memory | Contiguous | Scattered |
| Random access | O(1) | O(n) |
| Insert/delete at head | O(n) | O(1) |
| Insert/delete at tail | O(1) amortized | O(1) with tail ptr (singly), O(1) (doubly) |
| Cache performance | Excellent | Poor |
| Memory overhead | None | 1 pointer/node (singly), 2 (doubly) |
Singly Linked List: Core Implementation
Goal: a singly linked list with head/tail pointers so both head and tail insertion are O(1), plus search, delete, and reverse.
Approach: each Node holds data and next; the list tracks head, tail, and a cached _length so len() is O(1). Reversing rewires next pointers in a single pass and must reset tail to the old head.
from typing import Optional, Any, Iterator
class Node:
"""A node in a singly linked list."""
def __init__(self, data: Any, next_node: Optional["Node"] = None) -> None:
self.data = data
self.next = next_node
def __repr__(self) -> str:
return f"Node({self.data})"
class SinglyLinkedList:
"""Singly linked list with O(1) head/tail insertion via a cached tail pointer."""
def __init__(self) -> None:
self.head: Optional[Node] = None
self.tail: Optional[Node] = None
self._length: int = 0
def __len__(self) -> int:
return self._length
def __iter__(self) -> Iterator[Any]:
current = .head
current :
current.data
current = current.
() -> :
.head :
nodes = [ d ]
nodes.append()
.join(nodes)
() -> :
.head
() -> :
new_node = Node(data, .head)
.head = new_node
.tail :
.tail = new_node
._length +=
() -> :
new_node = Node(data)
.tail :
.head = .tail = new_node
:
.tail. = new_node
.tail = new_node
._length +=
() -> []:
.head :
data = .head.data
.head = .head.
.head :
.tail =
._length -=
data
() -> :
.head :
.head.data == data:
.delete_at_head()
current = .head
current. current..data != data:
current = current.
current. :
current. == .tail:
.tail = current
current. = current..
._length -=
() -> :
current, index = .head,
current :
current.data == data:
index
current = current.
index +=
-
() -> :
.tail = .head
prev, current = , .head
current :
next_node = current.
current. = prev
prev = current
current = next_node
.head = prev
__name__ == :
sll = SinglyLinkedList()
v [, , , ]:
sll.add_at_tail(v)
(sll) == [, , , ]
sll.search() == sll.search() == -
sll.delete_by_value() (sll) == [, , ]
sll.reverse()
(sll) == [, , ] sll.tail.data ==
()
Interview Problems: Slow/Fast Pointers
Goal: cycle detection, finding the middle node, merging two sorted lists, and removing the n-th node from the end — the problems that come up most in coding interviews.
Approach: most of these use two pointers moving at different speeds (Floyd's tortoise-and-hare) or a fixed gap between two pointers, giving O(n) time and O(1) extra space without ever converting to a Python list.
from typing import Optional
def has_cycle(head: Optional[Node]) -> bool:
"""Floyd's cycle detection: slow moves 1 step, fast moves 2. O(n)/O(1)."""
slow = fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return False
def find_middle(head: Optional[Node]) -> Optional[Node]:
"""Return the middle node (second middle for even length). O(n)/O(1)."""
slow = fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
return slow
def merge_sorted_lists(head1: Optional[Node], head2: Optional[Node]) -> [Node]:
dummy = Node()
current = dummy
head1 head2 :
head1.data <= head2.data:
current., head1 = head1, head1.
:
current., head2 = head2, head2.
current = current.
current. = head1 head1 head2
dummy.
() -> [Node]:
dummy = Node()
dummy. = head
first = second = dummy
_ (n + ):
first :
head
first = first.
first :
first = first.
second = second.
second. = second..
dummy.
__name__ == :
a, b, c, d = Node(), Node(), Node(), Node()
a., b., c. = b, c, d
has_cycle(a)
d. = b
has_cycle(a)
d. =
m = find_middle(Node(, Node(, Node(, Node(, Node())))))
m.data ==
l1 = Node(, Node(, Node()))
l2 = Node(, Node(, Node()))
merged = merge_sorted_lists(l1, l2)
out = []
merged:
out.append(merged.data)
merged = merged.
out == [, , , , , ]
nodes = [Node(i) i (, )]
i ():
nodes[i]. = nodes[i + ]
new_head = remove_nth_from_end(nodes[], )
out = []
new_head:
out.append(new_head.data)
new_head = new_head.
out == [, , , ]
()
Pitfalls
- Forgetting to update
tail when deleting the last node or when reversing — the classic off-by-reference bug
- Delete at tail is O(n) on a singly linked list (must find the second-to-last node); use a doubly linked list if tail deletion is frequent
slow == fast compares by value for custom __eq__; use is (identity) for cycle/middle detection so you're comparing node references, not data
- Losing the rest of the list when reversing — always save
next_node = current.next before rewiring current.next
- Off-by-one in
remove_nth_from_end: advance the lead pointer n + 1 steps (not n) so the trailing pointer lands just before the target node
See Also
algo-stacks-queues — a doubly linked list backs an O(1) deque
algo-dynamic-arrays — the array-based alternative and its amortized-O(1) append
algo-binary-search-trees — a "linked" structure with two child pointers instead of one next
algo-complexity-analysis — Big-O background for the tables above