3.2.3Linear Data Structures

Doubly linked list — bidirectional traversal

2,019 words9 min readdifficulty · medium4 backlinks

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):

for every node x:x.next.prev=xandx.prev.next=x\text{for every node } x:\quad x.next.prev = x \quad\text{and}\quad x.prev.next = x

(whenever those neighbours exist). If you ever break this, traversal lies to you.

Figure — Doubly linked list — bidirectional traversal

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 forward

Why cur is not None? Because the last node's next is null. Reaching None is 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 backward

Why start at tail and not head? Because going backward means following prev. We need a node at the back to begin. This is the whole payoff of storing tail: backward traversal is O(n)O(n) from the start, never O(n2)O(n^2).


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            # 5

Why this exact order? Steps 1–2 wire new to its neighbours first, while the old links still exist. If you did step 5 (node.next = new) too early, you'd lose the pointer to 30 and couldn't reach it to fix its prev. 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.prev

Why is this still fine? Total O(n)+O(n)=O(n)O(n)+O(n)=O(n). Storing tail only saves the first walk. The prev pointers 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?
A 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?
Both are null (None).
Time complexity to traverse all nodes backward in a DLL?
O(n) — you still visit every node once.
Time complexity to step back exactly one node in a DLL vs. a singly linked list?
O(1) in a DLL; O(n) in a singly list (must restart from head).
State the doubly linked list invariant.
For every node x: x.next.prev x and x.prev.next x (when neighbours exist).
Why store a tail pointer?
So backward traversal can start at the end immediately, avoiding an O(n) forward walk first.
In insert_after, why attach the new node's links before rewriting node.next?
Because rewriting node.next first would lose the pointer to the old successor, making it unreachable to fix its prev.
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?
O(n) — one extra pointer per node.
What is the most common structural bug when editing a DLL?
Updating only one of the two pointers (next or prev), breaking the invariant.

Connections

Concept Map

adds prev pointer

each node stores

first node

last node

must satisfy

follow next

follow prev

stops at None

stops at None

enables

solves

second pointer

Singly linked list

Doubly linked list

Node: data prev next

head prev is null

tail next is null

Invariant x.next.prev equals x

Forward traversal

Backward traversal

O of n time

Step back is O of 1

Undo, back-forward, LRU cache

O of n extra space

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 invariantx.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.

Go deeper — visual, from zero

Test yourself — Linear Data Structures

Connections