Doubly linked list — bidirectional traversal
What is a Doubly Linked List?
head tail
│ │
▼ ▼
┌────┬───┬────┐ ┌────┬───┬────┐ ┌────┬───┬────┐
│null│ A │ ──┼──▶│ ◀──┤ B │ ──┼─▶│ ◀──┤ C │null│
└────┴───┴────┘◀──┴────┴───┴────┘◀─┴────┴───┴────┘
prev data next prev data next prev data next
The crucial invariant (a rule that must always hold):
(whenever those neighbours exist). If you ever break this, traversal lies to you.

Bidirectional Traversal — deriving it from scratch
WHAT we want: visit every node either left-to-right or right-to-left.
HOW, forward (head → tail):
def traverse_forward(head):
cur = head
while cur is not None: # stop when we fall off the end
visit(cur.data)
cur = cur.next # step one door forwardWhy
cur is not None? Because the last node'snextisnull. ReachingNoneis the signal "no more nodes."
HOW, backward (tail → head):
def traverse_backward(tail):
cur = tail
while cur is not None:
visit(cur.data)
cur = cur.prev # step one door backwardWhy start at
tailand nothead? Because going backward means followingprev. We need a node at the back to begin. This is the whole payoff of storingtail: backward traversal is from the start, never .
Worked Example 1 — Print forward then backward
List: 10 ⇄ 20 ⇄ 30. head → node(10), tail → node(30).
Forward:
| step | cur |
action | next cur |
|---|---|---|---|
| 1 | 10 | print 10 | cur=cur.next → 20 |
| 2 | 20 | print 20 | → 30 |
| 3 | 30 | print 30 | → None |
| 4 | None | loop ends | — |
Output: 10 20 30. Why step 4? 30.next is None, so the while condition fails — that's the clean stop, no off-by-one.
Backward (start at tail=30):
Output: 30 20 10, by following prev each time. Why it works: the invariant guarantees 30.prev = 20, 20.prev = 10, 10.prev = None.
Worked Example 2 — Insert in the middle (and keep the invariant)
Insert 25 between 20 and 30.
def insert_after(node, new):
new.prev = node # 1
new.next = node.next # 2
if node.next is not None: # 3
node.next.prev = new # 4
else:
tail = new # 4' (node was the tail)
node.next = new # 5Why this exact order? Steps 1–2 wire
newto its neighbours first, while the old links still exist. If you did step 5 (node.next = new) too early, you'd lose the pointer to30and couldn't reach it to fix itsprev. Always attach the new node before detaching the old links.
After insertion, check the invariant: 25.next.prev = 30.prev = 25 ✓ and 25.prev.next = 20.next = 25 ✓. Bidirectional traversal now correctly produces 10 20 25 30 forward and the reverse backward.
Worked Example 3 — Reverse traversal without a tail pointer
What if you only have head? You can still go backward — but you must first walk forward to the end:
cur = head
while cur.next is not None: # walk to last node: O(n)
cur = cur.next
while cur is not None: # now walk back: O(n)
visit(cur.data)
cur = cur.prevWhy is this still fine? Total . Storing
tailonly saves the first walk. Theprevpointers do the real work either way.
Recall Feynman: explain to a 12-year-old
Imagine a line of kids holding hands. In a normal (singly) line, each kid only holds the hand of the kid in front. If you want to go to the back, you have to walk all the way again. In a doubly linked line, each kid holds hands both ways — front and back. So you can walk forward to the end, then just turn around and follow the back-hands all the way to the start. If a new kid joins in the middle, the two kids around them must each grab the new kid's hand — both sides — or the chain breaks and someone gets lost.
Active Recall
What extra pointer does a doubly linked list add over a singly linked list?
prev pointer in each node, pointing to the previous node.What is the prev of the head node and the next of the tail node?
null (None).Time complexity to traverse all nodes backward in a DLL?
Time complexity to step back exactly one node in a DLL vs. a singly linked list?
State the doubly linked list invariant.
Why store a tail pointer?
In insert_after, why attach the new node's links before rewriting node.next?
Which loop condition visits ALL nodes, including the last?
while cur is not None (not while cur.next is not None).Extra space cost of a DLL over a singly list?
What is the most common structural bug when editing a DLL?
Connections
- Singly Linked List — DLL is this plus a
prevpointer. - Linked List vs Array — DLL trades extra memory for O(1) neighbour access.
- LRU Cache — uses a DLL + hashmap for O(1) eviction from the tail.
- Browser History — back/forward maps directly to
prev/next. - Circular Doubly Linked List — tail.next = head, head.prev = tail.
- Big-O Notation — justifies the O(n)/O(1) costs above.
Concept Map
Hinglish (regional understanding)
Intuition Hinglish mein samjho
Dekho, singly linked list ek one-way road jaisi hoti hai — har node sirf apne next ko jaanta hai, isliye tum sirf aage ja sakte ho. Peeche jaana ho toh wapas head se chalna padega, jo O(n) ka kaam hai. Doubly linked list mein har node ke paas ek extra pointer hota hai — prev — jo pichle node ko point karta hai. Matlab har node ab do-taraf wali door ban gaya: aage bhi jao, peeche bhi. Isi wajah se "ek step peeche jaana" sirf O(1) ho jaata hai.
Bidirectional traversal ka funda simple hai. Forward jaana ho toh head se start karo aur cur = cur.next karte raho jab tak None na aaye. Backward jaana ho toh tail se start karo aur cur = cur.prev karte raho. Dono direction mein poori list ghoomne ka cost O(n) hi rehta hai — kyunki har node ek baar visit hota hai. tail pointer rakhne ka faayda ye hai ki backward shuru karne ke liye seedha end mil jaata hai, warna pehle aage tak chalna padta.
Sabse important cheez yaad rakho: DLL ka invariant — x.next.prev == x aur x.prev.next == x. Jab bhi tum koi node insert ya delete karte ho, do pointers update karne padte hain, sirf ek nahi. Aur ek golden rule: "Attach before you detach" — naya node pehle dono taraf connect karo, tabhi purane links todo, warna koi node kho jaayega aur chain toot jaayegi.
Yeh structure real life mein bahut kaam aata hai — browser ka back/forward button, undo-redo, aur LRU cache — sabhi DLL pe bante hain, kyunki "ek step peeche/aage" ka O(1) operation inko fast banata hai.