Java LinkedList Deep Dive — Doubly-Linked Nodes
LinkedList is a doubly-linked list that implements both List and Deque. It excels at O(1) insertions/removals at the head and tail — but its poor cache locality means ArrayList is almost always faster in practice.
Internal Structure
LinkedList<String> list = LinkedList of ["A", "B", "C"]
┌──────────┐ ┌──────────┐ ┌──────────┐
│ prev: null│←──│ prev │←──│ prev │
│ item: "A" │ │ item: "B" │ │ item: "C" │
│ next │──→│ next │──→│ next: null│
└──────────┘ └──────────┘ └──────────┘
↑ ↑
first last
size = 3
Each element is stored in a Node object:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
Big-O Complexity
| Operation | Time | Notes |
|---|---|---|
addFirst(e) / addLast(e) |
O(1) | Direct pointer update |
removeFirst() / removeLast() |
O(1) | Direct pointer update |
getFirst() / getLast() |
O(1) | Direct access to head/tail |
get(index) |
O(n) | Must traverse from nearest end |
add(index, element) |
O(n) | O(n) to find + O(1) to insert |
remove(index) |
O(n) | O(n) to find + O(1) to unlink |
contains(Object) |
O(n) | Linear search |
size() |
O(1) | Stored field |
The O(n) traversal for
get(index)is the killer — it makesLinkedListunsuitable for random access.
LinkedList as a Deque
LinkedList implements Deque (double-ended queue), making it usable as both a stack and a queue:
As a Queue (FIFO)
Deque<String> queue = new LinkedList<>();
queue.offer("First"); // add to tail
queue.offer("Second");
queue.offer("Third");
queue.poll(); // "First" — remove from head
queue.peek(); // "Second" — look at head without removing
As a Stack (LIFO)
Deque<String> stack = new LinkedList<>();
stack.push("Bottom"); // add to head
stack.push("Middle");
stack.push("Top");
stack.pop(); // "Top" — remove from head
stack.peek(); // "Middle"
Prefer
ArrayDequeoverLinkedListfor both stack and queue use cases — it’s faster due to array-backed cache locality.
When LinkedList Is Appropriate
| Use Case | Why |
|---|---|
| Implementing a Queue/Deque | O(1) head + tail operations |
| Frequent insert/remove at both ends | No shifting like ArrayList |
| Iterator-based insert/remove | O(1) when you already have the position |
When LinkedList Is NOT Appropriate
| Situation | Why Not |
|---|---|
| Random access by index | O(n) traversal |
| Large lists with iteration | Poor cache locality |
| General-purpose list | ArrayList is faster in practice |
| Memory-constrained | ~40 bytes per node vs ~8 bytes per slot |
Memory Overhead
ArrayList element: 1 reference (8 bytes on 64-bit)
LinkedList element: Node object (16 bytes header + 8 prev + 8 next + 8 item = 40 bytes)
For 1 million elements:
- ArrayList: ~8 MB + some wasted capacity
- LinkedList: ~40 MB (5× more memory)
Plus, each Node is a separate heap object → more garbage collection pressure.
Cache Locality — The Hidden Performance Killer
ArrayList: [A][B][C][D][E][F] ← contiguous in memory → CPU cache friendly
One cache line loads multiple elements
LinkedList: [A]→...→[B]→...→[C] ← scattered in memory → cache misses
Each access may be a cache miss
Modern CPUs are optimised for sequential memory access. ArrayList’s contiguous array lets the CPU prefetch next elements. LinkedList nodes are scattered across the heap, causing frequent cache misses.
This is why ArrayList often outperforms LinkedList even for operations where LinkedList has better theoretical complexity.
Practical Comparison
// Benchmark: Iterating 1 million elements
ArrayList: ~2 ms (sequential memory access)
LinkedList: ~8 ms (pointer chasing, cache misses)
// Benchmark: Adding 1 million elements to end
ArrayList: ~15 ms (amortized O(1), occasional resize)
LinkedList: ~25 ms (O(1) per add, but object creation overhead)
// Benchmark: Adding at position 0 (1000 elements)
ArrayList: ~0.5 ms (shifts all elements — O(n) per insert)
LinkedList: ~0.1 ms (pointer update — O(1))
LinkedListwins only for repeated inserts/removes at the head of large lists.
Using ListIterator for Efficient Modification
If you already have an Iterator positioned at the right spot, insert/remove is O(1):
LinkedList<String> list = new LinkedList<>(List.of("A", "C", "D"));
ListIterator<String> it = list.listIterator();
it.next(); // advance past "A"
it.add("B"); // insert "B" after "A" → O(1)
// list = [A, B, C, D]
it.next(); // advance past "C"
it.remove(); // remove "C" → O(1)
// list = [A, B, D]
The Verdict
| Factor | ArrayList | LinkedList | Winner |
|---|---|---|---|
| Random access | O(1) | O(n) | ArrayList |
| Append to end | O(1) amortized | O(1) | Tie |
| Insert at head | O(n) | O(1) | LinkedList |
| Memory usage | Low | High (~5×) | ArrayList |
| Cache performance | Excellent | Poor | ArrayList |
| As a Deque | ❌ | ✅ (but ArrayDeque is better) | ArrayDeque |
Bottom line: Use ArrayList for List operations. Use ArrayDeque for Deque operations. LinkedList is rarely the optimal choice.
Key Takeaways
LinkedListis a doubly-linked list — O(1) at head/tail, O(n) random access.- It implements both
ListandDequeinterfaces. - Memory overhead is ~5× that of
ArrayListper element. - Cache locality makes
ArrayListfaster in practice for most operations. - Use
LinkedListonly for Deque operations whereArrayDequeisn’t suitable, or for iterator-based insertions in the middle of a traversal. - The correct answer in most cases: use
ArrayList.
What’s Next?
Next: Stack and Queue — LIFO/FIFO structures with Deque, ArrayDeque, and real-world use cases.