Exercises — Doubly linked list — bidirectional traversal
Throughout, we use the same node shape from the parent note: every node holds data, a next pointer (to the node in front) and a prev pointer (to the node behind). The head is the first node (head.prev = None); the tail is the last node (tail.next = None). The rule that must never break — the invariant — is:
(whenever those neighbours exist).
Level 1 — Recognition
Exercise 1.1
A doubly linked list holds 5 ⇄ 8 ⇄ 13. Name the four boundary pointer values: head.prev, head.next.data, tail.next, tail.prev.data.
Recall Solution 1.1
Picture the chain: head → node(5) → node(8) → node(13) ← tail.
head.prev=None(nothing sits before the first node — that is what "first" means).head.next.data=8(head is node 5; itsnextis node 8).tail.next=None(nothing sits after the last node).tail.prev.data=8(tail is node 13; itsprevis node 8).
Exercise 1.2
For the list 5 ⇄ 8 ⇄ 13, is the following true or false? head.next.prev == head. Explain using the invariant.
Recall Solution 1.2
True. Set head (node 5). The invariant says . Here head.next is node 8, and node 8's prev is node 5, which is exactly head. So head.next.prev == head holds. This is the invariant doing its job: stepping forward one door and then backward one door lands you exactly where you started.
Level 2 — Application
Exercise 2.1
List: 2 ⇄ 4 ⇄ 6 ⇄ 8. Run traverse_forward(head) and traverse_backward(tail). Write both outputs.
Recall Solution 2.1
Forward starts at head (node 2) and follows next until None:
2 → 4 → 6 → 8 → None (stop). Output: 2 4 6 8.
Backward starts at tail (node 8) and follows prev until None:
8 → 6 → 4 → 2 → None (stop). Output: 8 6 4 2.
Notice the backward output is the forward output reversed — because prev walks the exact same chain the other way.
Exercise 2.2
For the same list 2 ⇄ 4 ⇄ 6 ⇄ 8, how many times does the while cur is not None loop body execute in traverse_forward? How many times is the condition evaluated?
Recall Solution 2.2
The body runs once per node = 4 times (for nodes 2, 4, 6, 8).
The condition is checked once before each body run plus one final time that fails: 5 evaluations. The 5th check is cur is not None when cur = None (after stepping off node 8), which is False and ends the loop. That extra final check is the "clean stop" — it is not an off-by-one bug, it is the design.
Exercise 2.3
Insert 5 after node 4 in 2 ⇄ 4 ⇄ 6 ⇄ 8 using insert_after(node4, node5). Trace the five steps and give the final forward output.
Recall Solution 2.3
Apply insert_after(node, new) with node = 4, new = 5:
new.prev = node→5.prev = 4.new.next = node.next→5.next = 6.node.next is not None? Yes (4.nextwas6), so:node.next.prev = new→6.prev = 5.node.next = new→4.next = 5.
Now the chain reads 2 ⇄ 4 ⇄ 5 ⇄ 6 ⇄ 8. Forward output: 2 4 5 6 8.
Invariant check on the new node: 5.next.prev = 6.prev = 5 ✓ and 5.prev.next = 4.next = 5 ✓.
Level 3 — Analysis
Exercise 3.1
A student inserts 5 after 4 but writes the steps in this order: node.next = new first, then tries node.next.prev = new. The list was 2 ⇄ 4 ⇄ 6 ⇄ 8. What goes wrong, and which node becomes unreachable?
Recall Solution 3.1
Look at the figure below. The moment we run node.next = new, the pointer from node 4 that used to reach node 6 is overwritten — it now points to node 5. But we never saved node 6's address anywhere, and node 5's next was never set to 6.

So when the code then runs node.next.prev = new, node.next is now node 5, not node 6. We set 5.prev = 5 (nonsense, a node pointing back to itself), and node 6 is now orphaned: nothing's next reaches it going forward. Forward traversal would produce 2 4 5 and then loop or stop early. Node 6 becomes unreachable from the head. This is exactly why the parent rule is "attach the new node before detaching the old links."
Exercise 3.2
Given only a broken list where 6.prev was accidentally set to None (everything else correct) in 2 ⇄ 4 ⇄ 6 ⇄ 8, does forward traversal still print all four nodes? Does backward traversal from tail?
Recall Solution 3.2
Forward uses only next pointers, and none of those were touched, so traverse_forward still prints 2 4 6 8 correctly.
Backward from tail (node 8) follows prev: 8 → 6 → then 6.prev is None, so the loop stops. Output: 8 6 — it silently drops nodes 4 and 2.
Lesson: a one-sided break is invisible to traversal in the other direction, which is why one-sided edits are so dangerous — the bug hides until someone walks the affected direction.
Exercise 3.3
Someone claims: "Because a DLL has prev pointers, I can jump backward nodes in ." Refute this precisely for .
Recall Solution 3.3
A prev pointer gives exactly one step back per follow, each in . To go back 3 nodes you must follow prev three times: cur = cur.prev; cur = cur.prev; cur = cur.prev. That is constant-time steps = for a fixed constant 3, yes — but the claim quietly implies arbitrary jumps are free. Going back nodes costs ; going back to the head from the tail costs . There is no random access (that is an array's power, see Linked List vs Array). The confusion mixes "one cheap step" with "cheap arbitrary jump."
Level 4 — Synthesis
Exercise 4.1
Write delete_node(node) that removes node from a DLL and preserves the invariant, correctly handling: (a) middle node, (b) head, (c) tail. Return the possibly-updated head and tail.
Recall Solution 4.1
The idea: reconnect the two neighbours around the node so the chain skips it. Handle the ends specially because a missing neighbour is None.
def delete_node(node, head, tail):
# link the left neighbour forward past `node`
if node.prev is not None:
node.prev.next = node.next
else:
head = node.next # node was the head
# link the right neighbour backward past `node`
if node.next is not None:
node.next.prev = node.prev
else:
tail = node.prev # node was the tail
node.prev = node.next = None # optional: fully detach
return head, tailWhy two ifs? Each boundary has a neighbour that might be None. When node.prev is None, there is no left neighbour to fix — instead the head itself moves to node.next. Symmetrically for the tail. Both branches touch two links total across the gap so the invariant holds for the reconnected pair.
- (a) Middle node
6in2 ⇄ 4 ⇄ 6 ⇄ 8:4.next = 8,8.prev = 4. Result2 ⇄ 4 ⇄ 8. - (b) Head
2:node.previsNone, sohead = 4; then4.prev = None. Result4 ⇄ 6 ⇄ 8. - (c) Tail
8:node.nextisNone, sotail = 6; then6.next = None. Result2 ⇄ 4 ⇄ 6.
Exercise 4.2
Write is_palindrome(head, tail) that checks whether the data values read the same forward and backward, using both pointers marching from the two ends toward the middle. State its time and space cost.
Recall Solution 4.2
Walk left forward and right backward simultaneously; compare data; stop when they meet or cross.
def is_palindrome(head, tail):
left, right = head, tail
while left is not right and left.prev is not right:
if left.data != right.data:
return False
left = left.next # step inward from the front
right = right.prev # step inward from the back
return TrueWhy two stop conditions? left is right catches an odd length (pointers land on the same middle node — nothing left to compare). left.prev is right catches an even length (pointers cross without meeting; the last valid pair was already compared). Missing either lets the loop run past the middle and re-compare pairs (harmless for correctness but wasteful) or, worse for a sloppy version, run off the end.
1 ⇄ 2 ⇄ 1→ compares (1,1), thenleft=2right=2 are equal,left is right→ stop → True.1 ⇄ 2 ⇄ 3 ⇄ 1→ compares (1,1) OK, then (2,3) → False.- Cost: each node is visited at most once from one side ⇒ time, extra space (just two pointers). This space is the DLL advantage over a singly list, which needs an stack or a full reversal to compare from the back.
Level 5 — Mastery
Exercise 5.1
You must repeatedly (i) find a node by its data and (ii) move it to the front (like an LRU Cache "touch"). Compare the cost of the move operation for a DLL versus a singly linked list, and explain why the DLL's prev pointer is what makes it fast.
Recall Solution 5.1
"Move to front" = delete the node from its current spot, then insert it at the head.
- Deletion needs to splice the node's two neighbours together:
node.prev.next = node.nextandnode.next.prev = node.prev. In a DLL you already holdnode.prevandnode.next, so this splice is . - In a singly list you have
nextbut noprev. To rewire the predecessor'snext, you must first find the predecessor by walking from the head — . So each move is there. - Insertion at head is in both.
Verdict: move-to-front is in a DLL, in a singly list. The prev pointer is the predecessor you would otherwise have to search for — that is precisely why real LRU caches and Browser History use doubly linked lists. See Big-O Notation for why an -per-touch structure matters when touches happen millions of times.
Exercise 5.2
Suppose you run traverse_forward on a list where, by a bug, one middle node's next accidentally points back to an earlier node, creating a cycle: ... 4 → 6 → 8 → 4 → ... (node 8's next is node 4 instead of None). What happens to while cur is not None, and what is the fixed cost of one full "traversal" now?
Recall Solution 5.1
The condition cur is not None is never satisfied to stop, because following next now loops 4 → 6 → 8 → 4 → 6 → 8 → … forever — no node's next is ever None. The loop is infinite; cost is unbounded (∞), not . This is the danger of a broken invariant: 8.next = 4 but 4.prev is still node 2, so 8.next.prev = 4.prev = 2 ≠ 8 — the invariant is violated and would have flagged the bug. A cycle-safe traversal would need a visited-set or Floyd's two-pointer method, but the real fix is to never break the invariant in the first place.
Exercise 5.3
A DLL has nodes. You perform: one forward traversal, one backward traversal, then single-step "go back one" operations. Give the total time cost in Big-O and justify each term.
Recall Solution 5.3
- Forward traversal: visits all nodes once ⇒ .
- Backward traversal: visits all nodes once ⇒ .
- single-step back operations: each is (follow
prevonce) ⇒ total.
Total: . We do not write or : the traversals are linear (constant work per node), and the steps are constant each, so they simply add. If instead each "go back" had to restart from the head (a singly list), each would be and the last term would balloon to — the whole point of prev.
Active Recall
Connections
- Parent topic — the concepts these exercises drill.
- Singly Linked List — the predecessor-search these exercises contrast against.
- LRU Cache — Exercise 5.1 is the heart of it.
- Browser History — back/forward is bidirectional traversal.
- Linked List vs Array — why there is no random access (L3.3).
- Big-O Notation — the add-vs-multiply reasoning of L5.
- Circular Doubly Linked List — the wrap-around variant (L1 trap).