Topics › Linked List › Learn Doubly LinkedList
Introduction to Doubly LL
Java source ↗ Edit this article ↗
Jump to code:
What this covers
A singly linked list lets you move forward and nowhere else. Many operations suffer from that: deleting a node you already hold a reference to requires finding its predecessor, which means a scan from the head; walking backwards is impossible without reversing or using a stack. A doubly linked list (DLL) fixes both by giving every node a second reference, prev, that points at the node before it. The price is one extra reference per node and twice as many links to keep consistent on every edit.
This article introduces the two-link node, builds a DLL from an array, traverses it in both directions, defines the invariant that makes a DLL “well formed”, and shows the two operations that justify the extra link: inserting after a node and deleting a node, both in O(1) with no scan. It closes with a comparison against the singly linked version.
The node
static class Node {
int val;
Node prev, next;
Node(int val) { this.val = val; }
Node(int val, Node prev, Node next) { this.val = val; this.prev = prev; this.next = next; }
}
A node holds a value and two references. prev is null for the first node and next is null for the last. A one-node list has both set to null, and the empty list is, as always, a null head. The three-argument constructor builds a node that is already wired to both neighbours, which keeps insertion code short.
The defining property of a DLL is a mirror invariant: whenever a.next == b, it must also hold that b.prev == a. Every edit changes links in pairs to preserve it; forgetting one half of a pair is the characteristic DLL bug, and it is invisible when you only traverse forward.
Building a list from an array
The construction is the singly linked one with one extra assignment per node. A mover stays on the current last node; each new node is attached after it, and the new node’s prev is pointed back at the mover.
static Node arrayToDLL(int[] arr) {
if (arr.length == 0) return null;
Node head = new Node(arr[0]);
Node mover = head; // always the current last node
for (int i = 1; i < arr.length; i++) {
Node temp = new Node(arr[i]);
mover.next = temp; // forward link
temp.prev = mover; // backward link, the one a singly list does not have
mover = temp;
}
return head;
}
O(n) time, O(n) space for the n nodes. The head’s prev is never assigned, so it stays null, as the invariant requires. The last node’s next likewise stays null.
Traversal in both directions
Forward traversal is identical to the singly linked case. Two passes are needed to fill a Java array, one to count and one to copy.
static int[] toArrayForward(Node head) {
int n = 0;
for (Node t = head; t != null; t = t.next) n++;
int[] out = new int[n];
int i = 0;
for (Node t = head; t != null; t = t.next) out[i++] = t.val;
return out;
}
Backward traversal is the new ability. Given only the head, we first walk to the tail (remembering the last non-null node), then follow prev back to the start. If a tail reference were maintained alongside the head, the first walk would disappear.
static int[] toArrayBackward(Node head) {
int n = 0;
Node tail = null;
for (Node t = head; t != null; t = t.next) { n++; tail = t; }
int[] out = new int[n];
int i = 0;
for (Node t = tail; t != null; t = t.prev) out[i++] = t.val;
return out;
}
The backward array is the forward array reversed, and the test file checks exactly that on every input. This is a cheap and powerful consistency test: if any prev link is wrong, the backward walk either skips nodes, visits them in the wrong order, or stops early, and the comparison fails.
A rendering helper makes examples readable. 1 <-> 2 <-> 3 is drawn with null on both ends, which reminds you that both the head’s prev and the tail’s next are null.
static String render(Node head) {
if (head == null) return "null";
StringBuilder sb = new StringBuilder("null <- ");
for (Node t = head; t != null; t = t.next) {
sb.append(t.val);
sb.append(t.next != null ? " <-> " : " -> null");
}
return sb.toString();
}
Checking the invariant
Because the mirror invariant is so easy to break, it is worth having a function that verifies it. A list is well formed when the head has no predecessor and every forward link is matched by the corresponding backward link.
static boolean isWellFormed(Node head) {
if (head == null) return true;
if (head.prev != null) return false;
for (Node t = head; t != null; t = t.next) {
if (t.next != null && t.next.prev != t) return false;
}
return true;
}
The test file deliberately builds a broken two-node list by setting next without prev, confirms isWellFormed reports it, repairs the link and confirms it passes. Keep a helper like this in your own tests whenever you write DLL code; it catches the half-updated links that forward-only checks miss.
O(1) local edits
The reason to pay for prev is that edits around a node you already hold become constant time. To insert after node, create the new node with prev = node and next = node.next, tell the old successor (if any) to look back at the new node, and finally point node.next at it. Four links change instead of one, but there is still no scan.
static void insertAfter(Node node, int val) {
Node fresh = new Node(val, node, node.next); // prev = node, next = node's old successor
if (node.next != null) node.next.prev = fresh; // the old successor now looks back at fresh
node.next = fresh;
}
The order matters: node.next.prev must be updated before node.next is overwritten, otherwise the old successor is no longer reachable from node. Building fresh first with the constructor sidesteps most ordering mistakes, because fresh captures node.next before anything is changed.
Deletion is where a singly linked list truly struggles. Given only a reference to target, a singly list needs the predecessor, and the only way to find it is a walk from the head. With prev the predecessor is one field read away. The two neighbours are made to bypass target in both directions; if target has no predecessor it was the head, so the successor becomes the new head.
static Node deleteNode(Node head, Node target) {
if (target == null) return head;
if (target.prev != null) target.prev.next = target.next; // bypass target going forward
else head = target.next; // target was the head
if (target.next != null) target.next.prev = target.prev; // bypass target going backward
target.prev = null;
target.next = null;
return head;
}
The method returns the possibly new head, so the caller writes head = deleteNode(head, target);. Clearing target’s own links is good hygiene: a deleted node that still points into the list is a trap for anyone who later follows it. Both edits are O(1) time and O(1) space, and the tests run them at the head, in the middle and at the tail, checking forward order, backward order and isWellFormed after each.
Singly versus doubly
| Singly linked | Doubly linked | |
|---|---|---|
| Links per node | 1 | 2 |
| Walk backwards | no | yes, via prev |
| Delete a node given only its reference | O(n), must find the predecessor |
O(1) |
| Insert before a node given only its reference | O(n) |
O(1) |
| Delete the tail | O(n) |
O(n) to reach it, O(1) with a tail reference |
| Links to update per edit | 1 | 2 to 4 |
| Memory per node | value plus one reference | value plus two references |
Doubly linked lists are what java.util.LinkedList uses, and they are the backbone of LRU caches, where “move this node to the front” and “evict the last node” must both be O(1). When you only ever traverse forward and edit at the head, the singly linked version is simpler and smaller.
Edge cases and pitfalls
- Empty list.
head == null. The builder returns it, both traversals return an empty array,renderprintsnull, andisWellFormedis vacuously true. - Single node. Both
prevandnextarenull. Deleting it must returnnull; inserting after it must leave itsprevasnull. - Half-updated links. Setting
a.next = bwithoutb.prev = apasses every forward test and fails the first backward walk. Always update in pairs and check withisWellFormed. - Order of assignments in insertion. Read the old neighbour before overwriting the link that reaches it, or capture it in the constructor as
insertAfterdoes. - Head replacement. Deleting the head changes which node the list starts at; the new head’s
prevmust be set tonull, and the caller must store the returned head. - Equal values. Nodes are compared by identity.
5 <-> 5 <-> 5is three nodes, and deleting one of them by reference removes exactly that one. - Detaching deleted nodes. Clear both links of a removed node so stale references cannot walk back into the live list.
Related problems
- Insert node before head in Doubly Linked List is the first DLL operation on this foundation.
- Delete head of Doubly Linked List is its inverse and reuses
deleteNode. - Reverse a Doubly Linked List swaps
prevandnextin every node. - Introduction to Singly LinkedList is the one-link version this article extends.
- Delete all occurrences of a key in DLL applies
deleteNoderepeatedly during a traversal. - Find Pairs with Given Sum in Doubly Linked List uses the backward link for a two-pointer sweep.
- Remove duplicates from sorted DLL is another traversal with in-place deletions.