Doubly Linked Lists
Each node now has two pointers — next and previous. Learn the structure and why bidirectional traversal is useful.
What Is It?
A doubly linked list is like a singly linked list, but each node has an extra pointer going the other way.
In a singly linked list, each node only knows who comes next. In a doubly linked list, each node knows both who comes next AND who came before.
So each node holds three things:
- The value
- A
nextpointer — points to the next node - A
prevpointer — points to the previous node
Because you can go in both directions, the list also keeps track of both ends:
- head — the first node
- tail — the last node
Bidirectional links allow traversal in both directions. Head.prev and tail.next are null.
The head's prev is null (nothing comes before it). The tail's next is null (nothing comes after it).
Analogy
Imagine a train where each car is connected to both the car in front and the car behind. In a singly linked list, the couplings only go one direction — you can walk from the engine toward the caboose, but not back. In a doubly linked list, you can walk freely in both directions because every car is coupled to its neighbors on both sides.
The engine is the head, the caboose is the tail. If you are standing at any car, you can immediately step forward (next) or backward (prev) without having to go back to the engine and start over.
How It Works
The Node Structure
structure DNode:data // the value storedprev // pointer to previous node (or null)next // pointer to next node (or null)
The only difference from a singly linked list node is the extra prev pointer. That small addition makes a big difference.
The List Structure
structure DoublyLinkedList:head = nulltail = nullsize = 0
Having both head and tail means we can reach either end of the list instantly — O(1).
Building a Doubly Linked List by Hand
// Create nodesnodeA = new DNode(10)nodeB = new DNode(20)nodeC = new DNode(30)// Wire forward linksnodeA.next = nodeBnodeB.next = nodeC// Wire backward linksnodeB.prev = nodeAnodeC.prev = nodeB// Set list pointershead = nodeA // first nodetail = nodeC // last node
The resulting structure:
Every forward link has a matching backward link. This invariant must always hold.
Notice the symmetry: every forward link has a matching backward link. This must always be true — if it breaks, the list is broken.
Why the Prev Pointer Matters
1. Delete a node in O(1) when you have a reference to it.
In a singly linked list, to delete a node, you first need to find the node before it (which takes O(n) to walk there). In a doubly linked list, the node already knows its previous neighbor via prev. You can delete in O(1):
// Delete nodeB from: A <-> B <-> CnodeB.prev.next = nodeB.next // A.next = CnodeB.next.prev = nodeB.prev // C.prev = A// Result: A <-> C
This is the single biggest advantage of doubly linked lists.
2. Walk backward.
function traverseBackward(tail):current = tailwhile current is not null:process(current.data)current = current.prev
With a singly linked list, going backward is a major hassle. With a doubly linked list, it is just as easy as going forward.
3. O(1) insert and delete at both ends.
With both head and tail pointers, adding or removing from either end is instant. This makes doubly linked lists perfect for use cases like browser history or undo/redo.
The Pointer Rule
For any two neighboring nodes A and B in a doubly linked list, this must always be true:
A.next = B AND B.prev = A
Every insert and delete you write must maintain this. If you update one side and forget the other, the list is broken.
Traversal in Both Directions
function traverseForward(head):current = headwhile current is not null:process(current.data)current = current.nextfunction traverseBackward(tail):current = tailwhile current is not null:process(current.data)current = current.prev
Both are O(n), but going backward is something a singly linked list simply cannot do.
When to Use Doubly vs. Singly
The simple rule: use singly when you only move forward, doubly when you need to go both ways or want O(1) delete.
Examples
Example 1: Forward and Backward Print
List: null <- [1] <-> [2] <-> [3] -> nullForward: 1, 2, 3Backward: 3, 2, 1
Example 2: O(1) Node Deletion
Suppose you have a direct reference to the node containing 20:
Before: null <- [10] <-> [20] <-> [30] -> nullnode = reference to [20]node.prev.next = node.next // [10].next = [30]node.next.prev = node.prev // [30].prev = [10]After: null <- [10] <-> [30] -> null
No walking required — two pointer updates and it is done.
Example 3: Checking If a List Is a Palindrome
With a doubly linked list, you can compare from both ends at once:
function isPalindrome(list):left = list.headright = list.tailwhile left is not right and left.prev is not right:if left.data is not equal to right.data:return falseleft = left.nextright = right.prevreturn true
Common Mistakes
1. Updating only one direction. The most common bug. If you update A.next = C but forget C.prev = A, forward traversal will look fine but backward traversal will be wrong. Always update both sides.
2. Not updating head and tail. When inserting at the front, update head. When inserting at the back, update tail. When deleting the only node, set both to null.
3. Null pointer crashes at boundaries. When deleting the head node, node.prev is null — you cannot call node.prev.next. Always check for null before accessing neighbors.
4. Assuming O(1) delete always applies. O(1) delete only works when you already have a reference to the node. If you only know the value, you still need O(n) to find the node first.
Best Practices
Update both pointers every time. Every insert and delete touches four pointers minimum. Make it a habit to ask: "did I update both the forward AND backward links?"
Always update head and tail. Keep a mental checklist — if you touch the first node, check if head needs updating. If you touch the last node, check if tail needs updating.
Test insertion and deletion at all positions: front, middle, back, and on a single-element list. Boundary bugs are the most common source of errors.
Draw it before you code it. Sketch the before and after state with arrows in both directions. This makes the required pointer changes obvious and prevents mistakes.
Runnable Code
Here is a complete JavaScript implementation you can run. It covers creating a doubly linked list, appending, prepending, deleting, and traversing in both directions.
class DNode {constructor(data) {this.data = data;this.prev = null;this.next = null;}}class DoublyLinkedList {constructor() {this.head = null;this.tail = null;this.size = 0;}// Add to the endappend(data) {const node = new DNode(data);if (!this.head) {this.head = node;this.tail = node;} else {node.prev = this.tail;this.tail.next = node;this.tail = node;}this.size++;}// Add to the beginningprepend(data) {const node = new DNode(data);if (!this.head) {this.head = node;this.tail = node;} else {node.next = this.head;this.head.prev = node;this.head = node;}this.size++;}// Delete first occurrence of valuedelete(data) {let current = this.head;while (current) {if (current.data === data) {if (current.prev) current.prev.next = current.next;else this.head = current.next;if (current.next) current.next.prev = current.prev;else this.tail = current.prev;this.size--;return;}current = current.next;}}// Traverse head to tailtraverseForward() {let current = this.head;const values = [];while (current) {values.push(current.data);current = current.next;}console.log('Forward: ' + values.join(' <-> '));}// Traverse tail to headtraverseBackward() {let current = this.tail;const values = [];while (current) {values.push(current.data);current = current.prev;}console.log('Backward: ' + values.join(' <-> '));}}// --- Demo ---const dll = new DoublyLinkedList();dll.append(10);dll.append(20);dll.append(30);console.log('After append 10, 20, 30:');dll.traverseForward();dll.traverseBackward();dll.prepend(5);console.log('\nAfter prepend 5:');dll.traverseForward();dll.traverseBackward();dll.delete(20);console.log('\nAfter delete 20:');dll.traverseForward();dll.traverseBackward();console.log('Size:', dll.size);