Linked Lists
Singly Linked List
A linked list is a sequence of elements where each element ("node") holds its value plus a reference ("pointer") to the next node. Unlike a Python list/array (M
Jr Codex DSA Notes
Level: Intermediate Prerequisites: Module 5, Chapter 5 Time to complete: ~25 minutes
Table of Contents
- What Is a Linked List?
- The Node Class
- Building and Traversing a List
- Insertion
- Deletion
- Linked Lists vs. Arrays — Complexity Compared
- Summary & Next Steps
1. What Is a Linked List?
A linked list is a sequence of elements where each element ("node") holds its value plus a reference ("pointer") to the next node. Unlike a Python list/array (Module 2), the elements are not stored contiguously in memory — they're scattered, connected only by these pointers.
[ 3 | next ] → [ 7 | next ] → [ 1 | next ] → [ 9 | None ]
head node tail node
This scattered layout is the whole point: inserting or deleting a node only requires rewiring a couple of pointers, not shifting every element after it — the opposite of what happens with an array (Module 2, Chapter 2).
2. The Node Class
class Node:
def __init__(self, value):
self.value = value
self.next = None # points to the next Node, or None if it's the last oneA linked list itself is usually just a reference to its first node (the "head"):
class LinkedList:
def __init__(self):
self.head = None3. Building and Traversing a List
# Building [3, 7, 1, 9] by hand
head = Node(3)
head.next = Node(7)
head.next.next = Node(1)
head.next.next.next = Node(9)
# Traversal: follow .next until it's None
def print_list(head):
current = head
while current is not None:
print(current.value, end=" -> ")
current = current.next
print("None")
# O(n) time — must visit every node; O(1) auxiliary spaceManually chaining .next calls doesn't scale — in practice you build lists with a helper:
def build_list(values):
head = None
tail = None
for value in values:
node = Node(value)
if head is None:
head = node
tail = node
else:
tail.next = node
tail = node
return head
# O(n) time, O(n) space (n new Node objects)4. Insertion
def insert_at_head(head, value):
new_node = Node(value)
new_node.next = head
return new_node # new_node is the new head
# O(1) — no traversal needed
def insert_at_tail(head, value):
new_node = Node(value)
if head is None:
return new_node
current = head
while current.next is not None: # must walk to the end
current = current.next
current.next = new_node
return head
# O(n) — without a tail pointer, you must traverse the whole list first
def insert_after(node, value):
new_node = Node(value)
new_node.next = node.next
node.next = new_node
# O(1) — given a reference to `node`, insertion is a pointer rewireNote the asymmetry: inserting at the head is O(1), but inserting at the tail is O(n) unless you separately track a tail pointer (as LinkedList above does) — a common design choice specifically to make tail insertion O(1) too.
5. Deletion
def delete_value(head, value):
if head is None:
return None
if head.value == value: # deleting the head
return head.next
current = head
while current.next is not None:
if current.next.value == value:
current.next = current.next.next # skip over the node — it's now unreferenced
return head
current = current.next
return head
# O(n) — must find the node (and the one before it) firstDeleting the head is O(1). Deleting anywhere else requires first finding the node before the one to delete (since a singly linked list has no way to go backward) — that search is O(n).
6. Linked Lists vs. Arrays — Complexity Compared
| Operation | Array / Python list | Singly Linked List |
|---|---|---|
| Access by index | O(1) | O(n) — must walk from the head |
| Search by value | O(n) | O(n) |
| Insert at front | O(n) — shifts everything right | O(1) |
| Insert at back | O(1) amortized | O(n) (or O(1) with a tracked tail) |
| Insert in middle | O(n) — shift | O(1) once you're at the position, O(n) to get there |
| Delete at front | O(n) — shifts everything left | O(1) |
| Memory layout | Contiguous | Scattered, plus one pointer per node |
Neither structure is "better" in general — it's the tradeoff from Chapter 4 of Module 1 (time-space tradeoff) playing out again: arrays win at random access, linked lists win at front insertion/deletion. Choosing between them means asking which operation your problem does most.
7. Summary & Next Steps
Key Takeaways
- A linked list is a chain of
Nodeobjects, each pointing to the next — no contiguous memory required. - Insertion/deletion at the head is
O(1), a clear win over an array'sO(n)shift. - Random access by index is
O(n)for a linked list, a clear loss compared to an array'sO(1). - The right structure depends on which operations dominate your use case.
Concept Check
- Why is inserting at the head of a linked list
O(1), while inserting at the head of a Python list isO(n)? - Why can't you binary search a singly linked list in
O(log n), even though it's sorted? - What would you need to add to
LinkedListto make tail insertionO(1)?
Next Chapter
→ Chapter 2: Doubly Linked List
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index