Linked Lists
Linked-list questions test whether pointer changes preserve the nodes you still need. Before editing a link, identify the processed part, the unprocessed part, and how they will reconnect.
1. Cost Depends on Which Node You Already Know
| Operation | Singly linked list | Doubly linked list |
|---|---|---|
Find index i or search by value | ||
| Insert after a known node | ||
| Delete a known node | Need its predecessor; finding it is | when the node reference is known |
| Append with a maintained tail | ||
| Remove the tail | to find predecessor | with a tail pointer |
Head, tail and size must remain consistent after the empty-to-singleton and singleton-to-empty transitions. Compare node identity when detecting cycles or shared tails; equal values do not imply the same node. Clarify whether the solution may reuse input nodes or must allocate copies.
2. Use a Dummy Head and Preserve the Unread Suffix
A dummy node gives the real head a predecessor, so inserting, deleting or reconnecting at the front follows the same rule as elsewhere. Return dummy.next, not the dummy. In a merge, a tail pointer marks the end of the result and always attaches the smaller current node; when one input ends, attach the remaining suffix. Time is and auxiliary space when reusing nodes.
For reversal, prev is the already-reversed prefix and current is the first node of the untouched suffix:
def reverse_list(head):
prev, current = None, head
while current is not None:
following = current.next
current.next = prev
prev, current = current, following
return prev
Saving following before rewiring prevents losing the suffix. Each step transfers one node from the untouched part to the reversed part; prev is the new head at termination. Time is and auxiliary space . A recursive reverse also costs time but needs stack space and must clear the old forward link to avoid a cycle.
For a sublist or block reversal, split the work: locate bounds, detach the section, reverse it, reconnect. Save the node before the section, its old head (the future tail), and the node after it. Reconnect before.next to the reversed head and the old head to after. Specify whether a final block shorter than k is reversed; Block Reversal defines its own contract.
3. Fast/Slow Pointers and Fixed Gaps
Middle: start both at the head and advance slow once, fast twice while fast and fast.next exist. For even length this selects the second middle. To end at the first middle before splitting, use a loop requiring fast.next and fast.next.next. Write down which half should receive the extra node.
Cycle: with speeds one and two, fast gains one position per iteration modulo the cycle length, so it must meet slow if a cycle exists. Stop when fast reaches null otherwise. After a meeting, reset one pointer to the head and move both one step at a time; their next meeting is the entry. The distance from head to entry matches the remaining distance from the meeting point modulo the cycle length. Time is , space .
Nth from the end: keep a lead pointer n nodes ahead of a follower. When the lead reaches the end, the follower identifies the desired position. For deletion, use a dummy and keep the follower at the predecessor; validate whether n can exceed the list length before advancing.
4. Compose Small Operations
Linked List Zip reorders 1 -> 2 -> 3 -> 4 -> 5 into 1 -> 5 -> 2 -> 4 -> 3: find the midpoint, cut the link between halves, reverse the second half, then alternate nodes from the two halves. Save both next pointers before each splice. Cutting first prevents the old midpoint link from forming a cycle during weaving.
The three phases are all linear, so their sum is , with auxiliary space. The same decomposition supports palindrome checking: compare the first half with a reversed second half, then restore the second half if callers expect the input unchanged.
5. Key Takeaways
- Save
current.nextbefore changing it; otherwise the unprocessed suffix may become unreachable. - Use a dummy when the head can change, and reconnect both ends of a reversed section.
- Define the even-length midpoint and short-final-block behavior explicitly.
- A tail pointer makes append constant-time, but does not make singly linked tail deletion constant-time.
Review Two Pointers for read/write and parallel scans, and Recursion for recursive contracts. Move to Trees when a node has multiple child links instead of one next link.
Practice Problems
Core Practice
Reverse Linked List
Given the head of a singly linked list, reverse the list, and return the reversed list.
Merge Two Sorted Lists
You are given the heads of two sorted linked lists list1 and list2.
Linked List Cycle
Given head, determine if the linked list has a cycle in it.
Linked List Zip
Reorder a list from L0 -> L1 -> ... -> Ln to L0 -> Ln -> L1 -> Ln-1 -> ....
Optional Variations (5)
Choose a variation when you want to explore a specific pattern. These are not required before moving to the next topic.
Design Singly Linked List
Design your implementation of a singly linked list. Implement get, addAtHead, addAtTail, addAtIndex, and deleteAtIndex.
Linked List Copy
Given the head of a singly linked list, return a new list with the same values.
Doubly Linked List to Array
Given a non-null node from a doubly linked list (which might not be the head), return an array of values from head to tail.
Linked List Midpoint
Given the head of a linked list, return the middle node. If there are two middle nodes, return the second one.
Linked List Block Reversal
Given the head of a linked list and an integer k, reverse nodes in groups of k. If the final group has fewer than k nodes, leave it as is.