Doubly linked lists
A doubly linked list stores both prev and next pointers, so you can walk and delete in both directions. This lesson covers the two pointers, O(1) insert and delete given a node, head and tail sentinels, two-way traversal and the memory overhead.
Learning goals
- Maintain prev and next consistently
- Insert and remove a node in O(1) given the node
- Use sentinels to avoid null checks
- Weigh the memory overhead
Prerequisites
Prev and next
Each node of a doubly linked list has two references: next to its successor and prev to its predecessor. Following next then prev returns to the same node. Every structural change must update both directions, and forgetting one leaves the list corrupt: forward traversal works but backward traversal does not.
The solid arrows in the diagram are next pointers and the dashed ones are prev pointers.
1class DNode {2int val;3DNode prev, next;4DNode(int val) { this.val = val; }5}
Doubly linked list with sentinels: insert and remove
Interactive · edit the inputdoubly linked list · 5 nodes
Sentinels H and T sit at both ends so every real node has a non-null prev and next: insertion and removal never need special cases for the ends.
1class DNode { int val; DNode prev, next; }2// head and tail are sentinels: head.next == tail when the list is empty, so no null checks are needed3static void insertAfter(DNode a, int v) { // O(1) once you hold node a4DNode n = new DNode(); n.val = v; DNode b = a.next;5n.prev = a; n.next = b; // point the new node at both neighbours first6a.next = n; b.prev = n; // then make both neighbours point at it7}8static void remove(DNode n) { // O(1): the node knows both neighbours9n.prev.next = n.next;10n.next.prev = n.prev;11}
Live state
size
3
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | size=3 | Sentinels H and T sit at both ends so every real node has a non-null prev and next: insertion and removal never need special cases for the ends. |
| 2 | L4 | steps=1 | Walk from the head: 1 step. Finding the position is O(index); the splice itself is O(1). |
| 3 | L4 | New node 15 between 10 and 20; nothing points to it yet. | |
| 4 | L5 | n.prev = a and n.next = b: the new node points at both neighbours first. | |
| 5 | L6 | a.next = n: the forward chain now includes the new node. | |
| 6 | L6 | b.prev = n: the backward chain too. Four pointer writes, no shifting. | |
| 7 | L7 | return=[10, 15, 20, 30] | Inserted 15 at index 1: [10, 15, 20, 30]. |
Insert/delete
Given a node a, inserting after it takes four pointer writes: set the new node's prev and next first, then make a.next and the old successor's prev point at it. Removing a node needs two: n.prev.next = n.next and n.next.prev = n.prev. No search for the previous node is needed, so both are O(1) once you hold the node, which is the main advantage over a singly linked list.
Step through insert and remove in the animation; each pointer write is one frame.
1static void insertAfter(DNode a, int v) {2DNode n = new DNode(v), b = a.next;3n.prev = a; n.next = b; // point the new node at both neighbours4a.next = n; b.prev = n; // then make both neighbours point back5}6static void remove(DNode n) {7n.prev.next = n.next;8n.next.prev = n.prev;9}
Doubly linked list with sentinels: insert and remove
Interactive · edit the inputdoubly linked list · 5 nodes
Sentinels H and T sit at both ends so every real node has a non-null prev and next: insertion and removal never need special cases for the ends.
1class DNode { int val; DNode prev, next; }2// head and tail are sentinels: head.next == tail when the list is empty, so no null checks are needed3static void insertAfter(DNode a, int v) { // O(1) once you hold node a4DNode n = new DNode(); n.val = v; DNode b = a.next;5n.prev = a; n.next = b; // point the new node at both neighbours first6a.next = n; b.prev = n; // then make both neighbours point at it7}8static void remove(DNode n) { // O(1): the node knows both neighbours9n.prev.next = n.next;10n.next.prev = n.prev;11}
Live state
size
3
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | size=3 | Sentinels H and T sit at both ends so every real node has a non-null prev and next: insertion and removal never need special cases for the ends. |
| 2 | L4 | steps=1 | Walk from the head: 1 step. Finding the position is O(index); the splice itself is O(1). |
| 3 | L4 | New node 15 between 10 and 20; nothing points to it yet. | |
| 4 | L5 | n.prev = a and n.next = b: the new node points at both neighbours first. | |
| 5 | L6 | a.next = n: the forward chain now includes the new node. | |
| 6 | L6 | b.prev = n: the backward chain too. Four pointer writes, no shifting. | |
| 7 | L7 | return=[10, 15, 20, 30] | Inserted 15 at index 1: [10, 15, 20, 30]. |
Head/tail sentinels
Sentinel (dummy) nodes at both ends hold no data and are never removed, so every real node always has a non-null prev and next. Insert and remove then need no special cases for the first or last element, and an empty list is just head.next == tail. The cost is two extra nodes.
H and T in the animation are the sentinels.
1class DList {2private final DNode head = new DNode(0), tail = new DNode(0);3private int size;4DList() { head.next = tail; tail.prev = head; }5void addFirst(int v) { insertAfter(head, v); size++; }6void addLast(int v) { insertAfter(tail.prev, v); size++; }7boolean isEmpty() { return head.next == tail; }8}
Doubly linked list with sentinels: insert and remove
Interactive · edit the inputdoubly linked list · 5 nodes
Sentinels H and T sit at both ends so every real node has a non-null prev and next: insertion and removal never need special cases for the ends.
1class DNode { int val; DNode prev, next; }2// head and tail are sentinels: head.next == tail when the list is empty, so no null checks are needed3static void insertAfter(DNode a, int v) { // O(1) once you hold node a4DNode n = new DNode(); n.val = v; DNode b = a.next;5n.prev = a; n.next = b; // point the new node at both neighbours first6a.next = n; b.prev = n; // then make both neighbours point at it7}8static void remove(DNode n) { // O(1): the node knows both neighbours9n.prev.next = n.next;10n.next.prev = n.prev;11}
Live state
size
3
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | size=3 | Sentinels H and T sit at both ends so every real node has a non-null prev and next: insertion and removal never need special cases for the ends. |
| 2 | L4 | steps=1 | Walk from the head: 1 step. Finding the position is O(index); the splice itself is O(1). |
| 3 | L4 | New node 15 between 10 and 20; nothing points to it yet. | |
| 4 | L5 | n.prev = a and n.next = b: the new node points at both neighbours first. | |
| 5 | L6 | a.next = n: the forward chain now includes the new node. | |
| 6 | L6 | b.prev = n: the backward chain too. Four pointer writes, no shifting. | |
| 7 | L7 | return=[10, 15, 20, 30] | Inserted 15 at index 1: [10, 15, 20, 30]. |
Two-way traversal
A doubly linked list can be walked forward from the head or backward from the tail, and a node can be found from either end: pick the nearer one when the index is known. java.util.LinkedList does this, so get(i) costs O(min(i, n - i)) instead of O(i). Backward traversal also makes tail deletion O(1).
Remember to stop at the sentinels, not at null.
1static void printBackward(DNode head, DNode tail) {2StringBuilder sb = new StringBuilder();3for (DNode cur = tail.prev; cur != head; cur = cur.prev) sb.append(cur.val).append(' ');4System.out.println(sb.toString().trim());5}6static DNode nodeAt(DNode head, DNode tail, int index, int size) {7if (index < size / 2) { DNode cur = head.next; for (int i = 0; i < index; i++) cur = cur.next; return cur; }8DNode cur = tail.prev; for (int i = size - 1; i > index; i--) cur = cur.prev; return cur;9}
Additional pointer overhead
The second pointer adds 4 bytes per node with compressed references. For an int value the node stays 24 bytes after 8-byte alignment (12 header + 4 value + 4 + 4), but for larger payloads the extra reference is real overhead, and every change costs more pointer writes. It is worth it when you must remove arbitrary nodes by reference, walk backwards or implement an LRU cache or a deque. If you only push and pop at the ends, an ArrayDeque is smaller and faster.
A singly linked list is enough for stacks, simple queues and forward-only scans.
| Singly linked | Doubly linked | |
|---|---|---|
| Pointers per node | 1 | 2 |
| Remove a node given the node | O(n): need the previous node | O(1) |
| Walk backwards | no | yes |
| Bytes per int node (64-bit, compressed refs) | 24 (20 rounded up) | 24 (exactly) |
| Pointer updates per insert | 2 | 4 |
Practice ladder
Now try it yourself
Easy
- Why does a doubly linked list use more memory per node?
- Which pointers change when removing a middle node?
Medium
- Insert 15 after 10 in 10 ↔ 20 ↔ 30 and list the four pointer writes
- Implement removeLast() on a list with sentinels in O(1)
Advanced
- Implement a deque with a doubly linked list and compare it with ArrayDeque
Key takeaways
- Update both prev and next on every change.
- Insert and remove are O(1) when you hold the node.
- Sentinels remove end special cases at the price of two extra nodes.