Linked Lists
A linked list is a linear data structure where each element (node) stores a value and a pointer to the next node. Unlike arrays, linked lists do not require contiguous memory, making insertions and deletions efficient at arbitrary positions.
Node Structure
class Node:
def __init__(self, val):
self.val = val
self.next = None # singly linked
# self.prev = None # additionally for doubly linked
Singly linked list: each node has one pointer (next). Traversal: forward only.
Doubly linked list: each node has next and prev. Traversal: forward and backward. Efficient deletion of a given node (no need to find its predecessor).
Circular linked list: the last node’s next points to the first. Used in round-robin scheduling.
Operations and Complexity
| Operation | Singly | Doubly | Notes |
|---|---|---|---|
| Access by index | $O(n)$ | $O(n)$ | Must traverse |
| Search | $O(n)$ | $O(n)$ | Linear scan |
| Insert at head | $O(1)$ | $O(1)$ | |
| Insert at tail | $O(1)$ | $O(1)$ | With tail pointer |
| Insert at position $i$ | $O(n)$ | $O(n)$ | Find predecessor first |
| Delete head | $O(1)$ | $O(1)$ | |
| Delete given node | $O(n)$ | $O(1)$ | Need predecessor for singly |
| Delete by value | $O(n)$ | $O(n)$ | Must find node first |
vs. Arrays: linked lists are better for frequent insertions/deletions in the middle; arrays are better for random access and cache performance.
Common Operations
Reverse a Linked List
Iterative, $O(n)$ time, $O(1)$ space:
def reverse(head):
prev, curr = None, head
while curr:
nxt = curr.next
curr.next = prev
prev = curr
curr = nxt
return prev
Recursive:
def reverse(head):
if not head or not head.next:
return head
new_head = reverse(head.next)
head.next.next = head
head.next = None
return new_head
Detect a Cycle (Floyd’s Algorithm)
Use two pointers: slow moves 1 step, fast moves 2 steps. If they meet, there is a cycle.
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return False
Find cycle start: after detection, reset slow to head. Move both one step at a time; they meet at the cycle entry.
Find Middle
Fast and slow pointers: when fast reaches the end, slow is at the midpoint.
def middle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
Merge Two Sorted Lists
def merge(l1, l2):
dummy = Node(0)
cur = dummy
while l1 and l2:
if l1.val <= l2.val:
cur.next = l1; l1 = l1.next
else:
cur.next = l2; l2 = l2.next
cur = cur.next
cur.next = l1 or l2
return dummy.next
Remove Nth Node from End
Two-pointer: advance fast $n$ steps ahead; move both until fast reaches end.
def remove_nth(head, n):
dummy = Node(0); dummy.next = head
slow = fast = dummy
for _ in range(n + 1):
fast = fast.next
while fast:
slow = slow.next; fast = fast.next
slow.next = slow.next.next
return dummy.next
Dummy Head Node
A sentinel node before the real head simplifies edge cases (inserting/deleting at position 0). Always return dummy.next as the new head.
Skip List
A probabilistic data structure built on linked lists. Maintains multiple levels of forward pointers (like an index). Provides $O(\log n)$ expected time for search, insert, and delete.
Structure: level 0 is the full list; each higher level is a subset of the level below. A node is promoted to the next level with probability $p$ (typically $0.5$ or $0.25$).
Used in Redis sorted sets (ZSET) and LevelDB/RocksDB memtable.
XOR Linked List
A memory-efficient doubly linked list that stores XOR(prev, next) instead of two separate pointers. Requires only one pointer field per node. Rarely used in practice (breaks garbage collectors).
When to Use Linked Lists
Use when:
- Frequent insertions/deletions at arbitrary positions.
- Implementing queues (O(1) enqueue/dequeue with head/tail pointers).
- Implementing undo functionality (stack of changes).
- Merging sorted lists.
Avoid when:
- Random access is needed.
- Memory overhead of pointers is a concern.
- Cache performance is critical (poor spatial locality).