Linked list basics
In one line
A linked list is a chain of nodes where each node holds a value and a pointer to the next node, so you can insert or remove at a known position in O(1) but must walk O(n) to reach the k-th item.
What it is
A linked list stores items in separate nodes scattered in memory. Each node holds a value and a reference (pointer) to the next node; the list itself is just a reference to the first node, the head. The last node points to null (None in Python).
In a singly linked list each node knows only its successor. In a doubly linked list each node also points to its predecessor, which lets you remove a node in O(1) when you hold a reference to it, and walk backwards. A circular list links the last node back to the first.
Compared with an array, a linked list gives up O(1) indexing and cache-friendly contiguous memory, and in return gets O(1) insertion and deletion once you are at the right node, with no shifting of other elements and no resizing.
Why interviewers ask
Linked list problems test careful pointer manipulation: changing links in the right order, handling the empty list, one node and the head and tail, and never losing the rest of the list. Interviewers also ask when you would choose a linked list over an array, which checks that you understand memory layout and real complexity, not just the textbook table.
How it works
- 1Traverse: start with cur = head and repeat cur = cur.next until cur is None. Reaching position k takes k steps, so access is O(n).
- 2Insert after a node: create the new node, point it at node.next first, then set node.next to the new node. Doing it in the other order loses the rest of the list.
- 3Insert at the head: new.next = head, then head = new. O(1).
- 4Delete the node after prev: prev.next = prev.next.next. To delete a node in a singly linked list you need its predecessor, which usually means walking from the head.
- 5In a doubly linked list, delete node x in O(1): x.prev.next = x.next and x.next.prev = x.prev. Keeping a tail pointer also gives O(1) append.
- 6Always check edge cases: head is None, the list has one node, and the change happens at the head or the tail.
Worked example · Insert 5 after 3, then delete 7, in 3 -> 7 -> 9
- Start: head points to node 3; 3.next = 7, 7.next = 9, 9.next = None.
- Insert 5 after node 3: new node 5 gets 5.next = 3.next, which is node 7. Then 3.next = 5. List: 3 -> 5 -> 7 -> 9. Only two pointers changed; node 9 was never touched.
- Delete 7: walk from the head until cur.next.val == 7. cur is node 5. Set 5.next = 7.next, which is node 9. List: 3 -> 5 -> 9.
- Node 7 is now unreachable and is freed by the garbage collector (in C you would free it yourself).
- Deleting the head value 3 is a special case: there is no previous node, so the new head is simply head.next. The dummy-node technique removes that special case.
Picture it
A treasure hunt: each clue tells you where the next clue is hidden. Adding a new clue in the middle only means rewriting one clue to point to the new spot, but to find the tenth clue you have to follow the first nine.
Reference code
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def build(values):
"""Build a list from a Python list and return its head."""
dummy = tail = ListNode()
for v in values:
tail.next = ListNode(v)
tail = tail.next
return dummy.next
def insert_after(node, val):
node.next = ListNode(val, node.next) # new node points at the old successor first
def delete_first(head, val):
"""Remove the first node holding val; return the (possibly new) head."""
if head and head.val == val:
return head.next
cur = head
while cur and cur.next:
if cur.next.val == val:
cur.next = cur.next.next # unlink: skip over the node
break
cur = cur.next
return head
build uses a dummy node so the first append is not a special case. insert_after sets the new node's next before relinking, so nothing is lost. delete_first handles the head separately, then looks one node ahead so it can rewire the predecessor. Insert is O(1) given the node; delete by value is O(n) because of the search.
Key patterns
Traversal with a runner pointer
When: Count, search or print a list
How: cur = head; while cur: ...; cur = cur.next. Use while cur.next when you need to stop at the last node.
Keep the previous node
When: Delete or insert based on the next node's value
How: Walk with cur and look at cur.next, so you can rewire cur.next when you find the match.
Two pointers at different speeds
When: Middle node, cycle detection, k-th from the end
How: Move one pointer faster or ahead of the other; see fast and slow pointers.
Hash map + linked list
When: O(1) lookup plus O(1) ordered insert and delete
How: Map keys to nodes in a doubly linked list, as in LRU caches and ordered dictionaries.
Real-world use
Linux kernel lists
The Linux kernel uses an intrusive circular doubly linked list (struct list_head) embedded inside structures such as tasks and inodes, so objects can be added to or removed from many lists in O(1) without extra allocations.
Java LinkedList and LinkedHashMap
java.util.LinkedList is a doubly linked list that implements both List and Deque, and LinkedHashMap threads a doubly linked list through its entries to remember insertion or access order.
Python deque and Redis lists
Python's collections.deque is a doubly linked list of fixed-size blocks, giving O(1) appends and pops at both ends; Redis lists are a quicklist, a linked list of compact listpack nodes, for fast LPUSH and RPOP.
Memory allocators
Allocators such as malloc implementations keep free blocks in free lists, linked lists threaded through the free memory itself, so finding and returning a block is a few pointer updates.
In a project
A music app's play queue lets users add songs next, remove songs and reorder while playing. The queue is stored as an array of 5,000 tracks, and every 'play next' or removal shifts thousands of entries, causing visible lag on older phones.
- Model the queue as a doubly linked list of track nodes, plus a hash map from track id to node so any node can be found in O(1).
- Play next: insert the new node right after the current node by updating four pointers.
- Remove a track: look up its node in the map and unlink it with node.prev.next = node.next and node.next.prev = node.prev.
- Keep head and tail pointers for O(1) append and for 'play from start', and unit test the empty queue and removal of the current, first and last track.
Impact: Queue edits go from O(n) shifts to O(1) pointer updates, so actions respond instantly regardless of queue length, and there is no array resizing during playback.
Trade-offs
Use it when
- Many insertions and deletions in the middle at positions you already hold a reference to
- You need O(1) push and pop at both ends (deques, queues)
- Objects must be moved between lists without copying (intrusive lists, LRU ordering)
Avoid it when
- You need random access by index: arrays give O(1), lists O(n)
- Performance depends on fast iteration: scattered nodes cause cache misses, so arrays are often several times faster in practice
- Memory is tight: every node carries one or two pointers of overhead
Cost: Access or search O(n); insert or delete at a known node O(1) (singly needs the previous node, doubly does not); insert at head O(1); append O(1) with a tail pointer. Extra space is one or two pointers per element.
Common mistakes
- Overwriting node.next before saving it, which loses the rest of the list.
- Calling insertion O(1) without counting the O(n) walk to find the position.
- Forgetting the empty list, the single node and updates at the head or tail.
- Dereferencing cur.next.next without checking that cur.next exists.
- Saying linked lists are generally faster than arrays: for iteration and most workloads arrays win because of cache locality.
Interview questions
Quick revision
- Node = value + next pointer (+ prev pointer in a doubly linked list).
- Access k-th: O(n). Insert or delete at a held node: O(1).
- Singly delete needs the previous node; doubly can delete itself in O(1).
- Set new.next before changing the old pointer.
- Edge cases: empty, one node, head, tail.
- Tail pointer gives O(1) append.
- Arrays usually beat lists for iteration because of cache locality.
Check yourself
1. What is the time to access the 1000th element of a singly linked list from the head?
2. To insert node new after node a, which order is safe?
3. Why can a doubly linked list delete a given node in O(1) when a singly linked list cannot?
4. Which is usually true in practice?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.