Linked List Field Notes
Singly linked lists, doubly linked lists, and queues. Every diagram shows exactly which pointer moved. Read the notes, then close them and rebuild the code from memory.
0Why linked lists exist
An array is a row of numbered lockers, all next to each other. A linked list is a treasure hunt: each clue is stored anywhere in the building, and each clue tells you where the next clue is. To reach clue 3, you must first read clue 1 and clue 2.
Both store items in order. The difference is where the items live in memory and how you find the next one.
| Question | Array | Linked list |
|---|---|---|
| Get item at position i | O(1) | O(n) |
| Insert or delete in the middle (you already stand at that node) | O(n) shifting | O(1) |
| Memory per item | value only | value + pointer(s) |
| Grow without copying | No (dynamic array copies) | Yes |
1Singly linked list
The node: two things in a box
A ListNode is an object with exactly two fields. val holds the data. next holds a reference (also called a pointer): the memory address of another node. In Python, "no node" is written None. NeetCode's videos say null; it is the same idea.
class ListNode:
def __init__(self, val):
self.val = val
self.next = None # a new node points nowhere
Building a chain by hand
Create three nodes, then set each next to the following node. Only the last node keeps next = None.
ListNode1 = ListNode("red")
ListNode2 = ListNode("blue")
ListNode3 = ListNode("green")
ListNode1.next = ListNode2 # red → blue
ListNode2.next = ListNode3 # blue → green
ListNode3.next = None # green is the end (already None, shown for clarity)
head is a pointer to the first node; tail points to the last one. If the list has one node, head and tail point to the same node. If the list is empty, both are None.Traversal: walk the chain
Traverse means "visit every node from first to last". You cannot jump. You start at the head and follow next until you hit None.
cur = head # cur = "the node I am standing on"
while cur: # stops when cur becomes None
print(cur.val)
cur = cur.next # step forward
| Loop turn | cur is | while cur? | prints | then cur = cur.next → |
|---|---|---|---|---|
| 1 | red node | True | red | blue node |
| 2 | blue node | True | blue | green node |
| 3 | green node | True | green | None |
| 4 | None | False | - | exit loop |
cur.next is a node object, not a value. To read the next value write cur.next.val. Writing print(cur.next) prints something like <ListNode object at 0x1f3...>.
Append at the tail: 2 lines
Because we keep a tail pointer, adding to the end does not need a walk. Two pointer changes, O(1).
ListNode4 = ListNode("yellow")
tail.next = ListNode4 # step 1: old last node now points to the new node
tail = ListNode4 # step 2: move the tail label to the new node
tail label. Do them in this order: if you move tail first, you lose your handle on green and cannot set its next.Delete a node: skip it
To delete a node you never "erase" it. You make the node before it point past it. Suppose we delete blue (the node after head):
head.next = head.next.next
# head.next is blue
# head.next.next is green
# so: red now points to green; blue is skipped
tail to red.Insert and delete are O(1) only if you already hold a reference to the node just before the spot. If you must walk from the head to find that spot, the walk costs O(n). NeetCode marks this with a star in the table. Interviewers ask about it.
Circular list: the infinite loop trap
If the last node's next points back to an earlier node, the list has no end. while cur: never sees None and runs forever.
Full class with head and tail
class SinglyLinkedList:
def __init__(self):
self.head = None
self.tail = None
def append(self, val):
node = ListNode(val)
if self.tail: # list has nodes
self.tail.next = node
self.tail = node
else: # list is empty: head and tail are the same node
self.head = self.tail = node
def delete_after(self, node):
# removes node.next (the node AFTER the one you hold)
if node is None or node.next is None:
return
removed = node.next
node.next = removed.next
if removed is self.tail: # deleted the last node? move tail back
self.tail = node
def to_list(self):
out, cur = [], self.head
while cur:
out.append(cur.val)
cur = cur.next
return out
2Doubly linked list
A train. Each car is hooked to the car in front and the car behind. You can walk from the engine to the back, or from the back to the engine. The engine has nothing in front (prev = None); the last car has nothing behind (next = None).
Same as a singly linked list, plus one extra pointer per node: prev, which points to the previous node. Cost: a little more memory and one more pointer to update on every change. Benefit: you can walk backwards, and you can delete a node when you only hold that node (no need for the node before it).
prev = None tells you "I am the head". next = None tells you "I am the tail".class DNode:
def __init__(self, val):
self.val = val
self.prev = None
self.next = None
Insert at end: 3 lines, in order
ListNode4 = DNode(4)
tail.next = ListNode4 # ① old tail → new node
ListNode4.prev = tail # ② new node ← old tail
tail = tail.next # ③ move the tail label
prev and walking backwards will skip the new node.Delete at end
Here prev pays off. In a singly linked list you cannot delete the tail in O(1) because you do not know the node before it. In a doubly linked list, tail.prev gives it to you instantly.
ListNode2 = tail.prev # ① grab the node before the tail
ListNode2.next = None # ② cut the forward arrow
tail = ListNode2 # ③ move the tail label back
# optional: old_tail.prev = None (helps garbage collection, not required)
Front operations (the animations in the lesson)
Same idea, mirrored. Insert at front: new node's next is the old head, old head's prev is the new node, then move head. Delete at front: grab head.next, set its prev to None, move head.
class DoublyLinkedList:
def __init__(self):
self.head = None
self.tail = None
def insert_end(self, val):
node = DNode(val)
if self.tail:
self.tail.next = node
node.prev = self.tail
self.tail = node
else:
self.head = self.tail = node
def insert_front(self, val):
node = DNode(val)
if self.head:
node.next = self.head
self.head.prev = node
self.head = node
else:
self.head = self.tail = node
def delete_end(self):
if not self.tail:
return None
val = self.tail.val
if self.tail is self.head: # only one node → list becomes empty
self.head = self.tail = None
else:
before = self.tail.prev
before.next = None
self.tail = before
return val
def delete_front(self):
if not self.head:
return None
val = self.head.val
if self.head is self.tail:
self.head = self.tail = None
else:
after = self.head.next
after.prev = None
self.head = after
return val
Every method has three cases: empty list, one node, many nodes. Most bugs live in the first two. When you write a linked list method, test those two first.
3Queue
A line at the bank. New people join at the back. The teller serves the person at the front. First in, first out. That rule has a name: FIFO.
A queue is not a new structure; it is a linked list with two rules. Add only at the back (enqueue). Remove only from the front (dequeue). NeetCode names the two ends left (front) and right (back). Compare with a stack, which is LIFO: last in, first out.
left is the head (exit door), right is the tail (entry door). Both operations touch only the ends, so both are O(1).The full queue, with the empty-list cases handled
class Queue:
def __init__(self):
self.left = None # front: dequeue here
self.right = None # back: enqueue here
def enqueue(self, val):
newNode = ListNode(val)
if self.right: # queue is non-empty
self.right.next = newNode
self.right = self.right.next
else: # queue is empty
self.left = self.right = newNode
def dequeue(self):
if not self.left: # queue is empty: nothing to return
return None
val = self.left.val
self.left = self.left.next
if not self.left: # we just removed the last node
self.right = None # right must not point at a dead node
return val
if not self.left: self.right = None. After removing the last node, right still points to it. The next enqueue would then attach to a node that is not in the queue, and left stays None. The queue silently breaks.
In real Python code
Do not build a queue from scratch in an interview unless asked. Use collections.deque. A deque ("deck", double-ended queue) supports O(1) add and remove at both ends.
from collections import deque
q = deque()
q.append(1) # enqueue at the back
q.append(2)
q.popleft() # dequeue from the front → 1
q.appendleft(0) # also allowed: add at front
q.pop() # also allowed: remove from back
# NEVER do this for a queue:
lst = [1, 2, 3]
lst.pop(0) # O(n): every element shifts left
Where you will use a queue next: breadth-first search (BFS) on trees and graphs. BFS is "visit everything 1 step away, then everything 2 steps away". A queue keeps that order for you. Trees are the next big topic in the course.
4Three moves that solve most linked list problems
The lessons teach the structure. Interviews test these three moves on top of it. Learn them as muscle memory.
Move 1: Reverse with prev / cur / nxt
Walk the list once. At each node, save where you are going, flip the arrow to point backwards, then move both markers forward.
def reverse_list(head):
prev, cur = None, head
while cur:
nxt = cur.next # 1. save next (or you lose the rest of the list)
cur.next = prev # 2. flip the arrow
prev = cur # 3. move prev forward
cur = nxt # 4. move cur forward
return prev # prev is the new head
nxt first. The moment you flip cur.next, node 3 and everything after it are unreachable from cur. nxt is your only way there.Move 2: Dummy node
Deleting the head is a special case because nothing points to it. Fix: put a fake node in front so every real node has a node before it. Return dummy.next at the end.
def remove_value(head, target):
dummy = ListNode(0)
dummy.next = head
cur = dummy # cur always stands BEFORE the node we inspect
while cur.next:
if cur.next.val == target:
cur.next = cur.next.next # skip it (same trick as section 1)
else:
cur = cur.next
return dummy.next # the real head, even if the old head was removed
Move 3: Slow and fast pointers
Two walkers. Slow moves 1 step, fast moves 2. When fast hits the end, slow is at the middle. If the list has a cycle, fast laps slow and they meet.
def middle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
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
while fast and fast.next. fast.next.next crashes if fast.next is None. This guard is the most common bug in this pattern.5Big-O summary
Say these out loud in interviews. The star always means: "if I already hold a reference to the node at that spot".
| Operation | Singly LL | Doubly LL | Queue (LL) | Python list |
|---|---|---|---|---|
| Access by index | O(n) | O(n) | - | O(1) |
| Search by value | O(n) | O(n) | - | O(n) |
| Insert at end (have tail) | O(1) | O(1) | O(1) enqueue | O(1)* |
| Delete at end | O(n) | O(1) | - | O(1) |
| Insert / delete at front | O(1) | O(1) | O(1) dequeue | O(n) |
| Insert / delete in middle | O(1)* | O(1)* | - | O(n) |
| Walk backwards | no | yes | - | yes |
Why singly delete-at-end is O(n): you need the node before the tail, and a singly list gives you no way to go back. You must walk from the head. Python list's O(1)* at end is "amortized": occasionally it copies to a bigger array.
6Mistakes to watch for
You overwrite cur.next before saving it. Everything after cur is gone. Always nxt = cur.next first.
cur.next is an object. Compare or print cur.next.val. You made this exact mistake earlier; it is normal.
AttributeError: 'NoneType' object has no attribute 'next'. Guard every step: while cur and cur.next, while fast and fast.next.
After append, tail must point at the new node. After removing the last node, tail (or the queue's right) must be updated or set to None.
First line of every method: what if the list is empty? Second: what if it has one node? Write those branches before the general case.
In a doubly linked list, draw the arrow to the new node before moving head or tail. Move the label last.
O(n) every time. Use collections.deque and popleft().
val / next / prev belong to the node. head / tail / left / right belong to the list or queue. Do not put self.val inside Queue.__init__.
Interview one-liners
- "A linked list trades O(1) random access for O(1) insert and delete at a known position."
- "Nodes are not contiguous in memory, so there is no index math; I have to traverse."
- "Insert and delete are O(1) only if I already hold the node before the position; otherwise the walk is O(n)."
- "A doubly linked list costs one extra pointer per node and buys O(1) delete-at-tail and backward traversal."
- "A queue is FIFO; with a linked list both enqueue and dequeue are O(1). In Python I use
collections.deque." - "For head-edge cases I use a dummy node. For middle or cycle questions I use slow and fast pointers."
7Practice, in this order
Notes do not make you good at this. Solving does. Do these on LeetCode with no notes open, then check against the templates above.
| # | Problem | Move it trains | Level |
|---|---|---|---|
| 206 | Reverse Linked List | prev / cur / nxt | Easy |
| 21 | Merge Two Sorted Lists | dummy node + walk two lists | Easy |
| 203 | Remove Linked List Elements | dummy node + skip | Easy |
| 876 | Middle of the Linked List | slow / fast | Easy |
| 141 | Linked List Cycle | slow / fast (Floyd) | Easy |
| 707 | Design Linked List | full doubly list, all edge cases | Medium |
| 1472 | Design Browser History | doubly list, prev and next | Medium |
| 19 | Remove Nth Node From End | dummy + two pointers with a gap | Medium |
| 2 | Add Two Numbers | dummy + build a new list | Medium |
| 1700 | Number of Students Unable to Eat Lunch | queue simulation with deque | Easy |
| 225 | Implement Stack using Queues | queue rotation | Easy |
| 143 | Reorder List | middle + reverse + merge (all three moves) | Medium |
Your next step today: LeetCode 206. Write it from memory, run it, then read the reverse diagram again and compare.
8Self-check
Answer out loud before opening. If you cannot answer in one breath, reread that section.
Q1What are the two fields of a singly linked list node, and what does each hold?
val holds the data. next holds a reference (address) to the next node, or None if this is the last node.
Q2Why is accessing the 100th node O(n) in a linked list but O(1) in an array?
Array elements are contiguous, so the address of index i is start + i × size: one multiplication. Linked list nodes are scattered; the only way to reach node 100 is to follow 99 next pointers.
Q3Write the two lines that append node to a singly linked list that has a tail. Why does the order matter?
tail.next = node then tail = node. If you move tail first, you no longer have a reference to the old last node, so you cannot connect it to the new one.
Q4What does head.next = head.next.next do?
It deletes the second node. head now points at what used to be the third node. The second node becomes unreachable and is garbage collected.
Q5Explain the star in "Insert O(1)*".
The insert itself is one or two pointer changes, O(1). But that assumes you already hold the node just before the insertion point. If you have to walk to it from the head, the walk is O(n).
Q6Deleting the tail: why O(n) in a singly list but O(1) in a doubly list?
To delete the tail you must set the previous node's next to None. A singly list cannot go backwards, so you walk from the head to find that node. A doubly list gives it to you instantly as tail.prev.
Q7Insert node X at the end of a doubly linked list. Write the three lines in the correct order.
tail.next = X, X.prev = tail, tail = X. (The first two can swap; tail = X must be last.)
Q8In dequeue, why do we set self.right = None when self.left becomes None?
We just removed the last node. right still points at it. If we leave it, the next enqueue sees a non-empty queue and attaches after a node that is no longer in the queue, while left stays None. The queue is corrupted.
Q9What happens when you run while cur: on a circular linked list?
Infinite loop. No node has next = None, so cur never becomes None. Detect cycles with slow and fast pointers.
Q10In the reverse-list loop, which line must come first and why?
nxt = cur.next. The next line, cur.next = prev, overwrites the only pointer to the rest of the list. Without saving it, everything after cur is lost.
Q11Which Python tool should you use for a queue, and which method removes from the front?
collections.deque, and popleft(). list.pop(0) is O(n) because every element shifts.
Q12Trace it: list is 1 → 2 → 3 → 4. Run middle(head). What are slow and fast after each loop turn, and what is returned?
Start: slow=1, fast=1. Turn 1: slow=2, fast=3. Turn 2: slow=3, fast=None (4.next). Loop stops because fast is None. Returns node 3, the second middle of an even-length list.