Introduction#
A linked list is a linear data structure where elements are stored in nodes, and each node points to the next node in the sequence. Unlike arrays, linked lists do not store elements in contiguous memory locations — each node holds a value and a reference (pointer) to the next node.
Linked lists are foundational to understanding dynamic data structures and are commonly used in interview questions and system internals (e.g., LRU caches, adjacency lists in graphs).
Core Concepts#
A linked list consists of:
- Node: Holds a value and a reference to the next node
- Head: The first node in the list
- Tail: The last node (its
nextpointer isNone)
Types of Linked Lists#
- Singly Linked List: Each node points to the next node only
- Doubly Linked List: Each node points to both the next and previous nodes
- Circular Linked List: The tail node points back to the head
This post covers a singly linked list with a full implementation.
Implementation#
Node Class#
1
2
3
4
class Node:
def __init__(self, value: int) -> None:
self.value = value
self.next: "Node | None" = None
LinkedList Class#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
class LinkedList:
def __init__(self) -> None:
self.head: Node | None = None
self.size: int = 0
def append(self, value: int) -> None:
"""Insert a node at the end of the list."""
new_node = Node(value)
if self.head is None:
self.head = new_node
else:
current = self.head
while current.next is not None:
current = current.next
current.next = new_node
self.size += 1
def prepend(self, value: int) -> None:
"""Insert a node at the beginning of the list."""
new_node = Node(value)
new_node.next = self.head
self.head = new_node
self.size += 1
def insert_at(self, index: int, value: int) -> None:
"""Insert a node at the given index (0-based)."""
if index < 0 or index > self.size:
raise IndexError(f"Index {index} out of range for list of size {self.size}")
if index == 0:
self.prepend(value)
return
new_node = Node(value)
current = self.head
for _ in range(index - 1):
current = current.next
new_node.next = current.next
current.next = new_node
self.size += 1
def delete(self, value: int) -> bool:
"""Remove the first node with the given value. Returns True if deleted."""
if self.head is None:
return False
if self.head.value == value:
self.head = self.head.next
self.size -= 1
return True
current = self.head
while current.next is not None:
if current.next.value == value:
current.next = current.next.next
self.size -= 1
return True
current = current.next
return False
def delete_at(self, index: int) -> int:
"""Remove the node at the given index and return its value."""
if index < 0 or index >= self.size:
raise IndexError(f"Index {index} out of range for list of size {self.size}")
if index == 0:
value = self.head.value
self.head = self.head.next
self.size -= 1
return value
current = self.head
for _ in range(index - 1):
current = current.next
value = current.next.value
current.next = current.next.next
self.size -= 1
return value
def search(self, value: int) -> int:
"""Return the index of the first node with the given value, or -1."""
current = self.head
index = 0
while current is not None:
if current.value == value:
return index
current = current.next
index += 1
return -1
def reverse(self) -> None:
"""Reverse the linked list in place."""
previous = None
current = 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]:
"""Return all node values as a Python list."""
result = []
current = self.head
while current is not None:
result.append(current.value)
current = current.next
return result
def __len__(self) -> int:
return self.size
def __repr__(self) -> str:
nodes = self.to_list()
return " -> ".join(str(v) for v in nodes) + " -> None"
Usage Examples#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ll = LinkedList()
# Append elements
ll.append(10)
ll.append(20)
ll.append(30)
print(ll) # 10 -> 20 -> 30 -> None
# Prepend
ll.prepend(5)
print(ll) # 5 -> 10 -> 20 -> 30 -> None
# Insert at index
ll.insert_at(2, 15)
print(ll) # 5 -> 10 -> 15 -> 20 -> 30 -> None
# Delete by value
ll.delete(15)
print(ll) # 5 -> 10 -> 20 -> 30 -> None
# Delete at index
removed = ll.delete_at(0)
print(removed) # 5
print(ll) # 10 -> 20 -> 30 -> None
# Search
print(ll.search(20)) # 1
print(ll.search(99)) # -1
# Reverse
ll.reverse()
print(ll) # 30 -> 20 -> 10 -> None
# Length
print(len(ll)) # 3
Time and Space Complexity#
| Operation | Time Complexity | Notes |
|---|---|---|
append |
O(n) | Traverses to the tail |
prepend |
O(1) | Updates head only |
insert_at |
O(n) | Traverses to index |
delete |
O(n) | Scans for value |
delete_at |
O(n) | Traverses to index |
search |
O(n) | Linear scan |
reverse |
O(n) | Single pass, O(1) extra space |
Space complexity is O(n) for storing n nodes.
Best Practices#
- Prefer a doubly linked list when frequent backward traversal or O(1) tail deletion is needed
- Track
sizeexplicitly to avoid O(n) length queries - Keep a
tailpointer ifappendperformance matters (reduces O(n) to O(1)) - Use linked lists over arrays when frequent insertions/deletions at the head are required
Common Interview Patterns#
Linked lists appear frequently in technical interviews. Common problems include:
- Detecting a cycle (Floyd’s tortoise and hare algorithm)
- Finding the middle node (slow/fast pointer)
- Merging two sorted lists
- Reversing a sublist between positions
- Checking if a linked list is a palindrome
These all rely on a solid understanding of pointer manipulation, which the implementation above covers.
Conclusion#
A singly linked list is a versatile data structure with O(1) prepend and pointer-based insertion/deletion. It trades random access (O(n) vs O(1) for arrays) for flexible dynamic sizing. Understanding the implementation details — especially pointer reassignment during deletion and reversal — is essential for working with more complex structures like graphs, trees, and LRU caches.