Topic 9.3
LinkedList
In one line
LinkedList is a doubly linked list: a chain of node objects, each pointing to the one before and after. Adding or removing at either end is O(1), but reaching index i means walking node by node, so get(i) is O(n), and in practice ArrayList or ArrayDeque is almost always the better choice.
Think of it like this
A treasure hunt. Each clue tells you where the next clue is, and (in this version) also where the previous one was. Starting a new hunt from the first or last clue is instant, and slipping a new clue between two others only means changing two notes. But to find the 50th clue there's no shortcut: you follow the chain one clue at a time. That's a linked list.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Node
- A small object holding one element plus links to its neighbours.
- Doubly linked
- Each node points both to the next node and to the previous one, so you can walk forwards and backwards.
- Head and tail
- The first and last nodes of the chain (
firstandlastin the JDK code). - Traversal
- Walking through the chain from node to node.
- ListIterator
- An iterator for lists that can move both ways and add, set or remove at its current position.
- Cache miss
- When the CPU needs data that isn't in its fast nearby memory and must wait for slower main memory. Scattered nodes cause many.
- Deque
- A "double-ended queue": a sequence you can add to and remove from at both ends.
Step by step
01The shape in memory
After adding "a", "b" and "c", the LinkedList object holds first (the node for a), last (the node for c) and size = 3. Each node is its own heap object; they can be anywhere in memory.
Compare with Topic 9.2: an ArrayList keeps one array whose slots sit side by side. The linked list trades that contiguity for cheap relinking.
02Adding at the end: link a new node
addLast(e) (and plain add(e)) creates new Node<>(last, e, null), sets the old last node's next to it, and moves last. If the list was empty, first points at it too. No copying, no growing: O(1) every time, not just on average.
addFirst is the mirror image. Removing an end unlinks the node and clears its fields so it doesn't keep its neighbours reachable.
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) { ... }
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null) first = newNode; // the list was empty
else l.next = newNode;
size++;
modCount++;
}03get(i): walking from the nearer end
get(i) calls node(i): if i < (size >> 1) it starts at first and follows next i times; otherwise it starts at last and follows prev size - 1 - i times. For a 1,000-element list, get(500) makes about 500 hops.
Each hop is a memory load that depends on the previous one, so the CPU can't fetch ahead. That's why walking a linked list is much slower per element than scanning an array, even when both are O(n).
Node<E> node(int index) {
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++) x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--) x = x.prev;
return x;
}
}04The O(n²) loop hiding in plain sight
for (int i = 0; i < list.size(); i++) total += list.get(i); looks linear, but on a LinkedList each get(i) walks from an end. The hops add up to about n²/4. For 100,000 elements that's 2.5 billion hops.
Loop with for-each (which uses the iterator and moves one node per step) and the same work is O(n). Code that must work on any List should iterate, not index, unless it checks list instanceof RandomAccess.
05Insert in the middle with a ListIterator
A ListIterator remembers where it is. it.add(e) links a new node right there by changing four references, and it.remove() unlinks the last returned node. Each is O(1).
This is the one pattern where LinkedList really beats ArrayList: one pass that inserts or removes many elements as it goes. An ArrayList doing the same with it.add shifts the tail on every insert.
ListIterator<String> it = list.listIterator();
while (it.hasNext()) {
if (it.next().equals("b")) {
it.add("x"); // O(1): link a node after "b"
}
}06As a queue and a stack
Because it implements Deque, a LinkedList offers offer/poll/peek (queue), push/pop (stack, working at the front) and all the First/Last methods. It's the only standard deque that allows null elements, which is a mixed blessing: poll() returns null both for "empty" and for a stored null.
ArrayDeque does all of this faster, with less memory and no garbage per element, so prefer it unless you need nulls or the List methods too.
Try it yourself
- 1
Predict the hop counts
In the hops example, add
list.get(4)andlist.get(6)for the 10-element list. Predict the hops for each (4 and 3), then run. - 2
Swap LinkedList for ArrayList
In the
ListIteratorexample, changenew LinkedList<>tonew ArrayList<>. Predict whether the output changes. (It doesn't: the behaviour is the same, only the cost of eachit.adddiffers, because the array must shift its tail.) - 3
Measure your own data
Extend the indexed-vs-iterator example with n = 1,000,000 (use
long). Predict roughly how many hops the indexed loop needs (about 2.5 × 10¹¹) before you run it.
Code & diagrams
Expected output
list: [a, b, c], first a, last c
poll: job1, left [job2]
pop: plate2, left [plate1]
pollFirst on empty: null
removeFirst on empty: NoSuchElementExceptionIndex 5 of 10 isn't below size/2, so the walk starts at the tail.
Expected output
get(0) walked 0 hops
a
get(3) walked 3 hops
d
get(5) walked 4 hops
f
get(9) walked 0 hops
jTen times more elements means a hundred times more hops for the indexed loop: that's O(n²).
Expected output
n=1000: indexed loop 249500 hops, iterator 999 hops
n=10000: indexed loop 24995000 hops, iterator 9999 hops
n=100000: indexed loop 2499950000 hops, iterator 99999 hopsExpected output
[wake, brush, rinse, eat breakfast, brush, rinse, sleep]
first letters backwards: srberbwBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Remove from an empty LinkedList
Write LinkedList<String> jobs = new LinkedList<>(); String next = jobs.removeFirst();.
Break #2
Call addFirst through a List variable on Java 17
Declare List<String> jobs = new LinkedList<>(); and call jobs.addFirst("urgent");, compiling with --release 17.
Myth vs fact
Myth
LinkedList inserts in the middle in O(1).
Fact
Only once you're at the node. Reaching index i is an O(n) walk, so list.add(i, e) is O(n), the same Big-O as ArrayList and usually slower in practice.
Myth
LinkedList is the right class for a queue.
Fact
It works, but ArrayDeque is faster and uses far less memory. Use LinkedList as a queue only if you need null elements.
Myth
LinkedList uses less memory because it doesn't over-allocate.
Fact
Each element costs a whole node object (about 24 bytes) versus a 4-byte array slot plus up to a third spare capacity in an ArrayList. The linked list is several times bigger.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
With compressed object pointers (the default for heaps under 32 GB), a
LinkedList.Nodehas a 12-byte header and three 4-byte references: 24 bytes. A million-elementLinkedListspends about 24 MB on nodes alone; anArrayListspends about 4 MB on its array. - ▸
LinkedListdoesn't implementRandomAccess, soCollections.binarySearchfalls back to an iterator-based search that still does O(log n) comparisons but O(n) node hops. Sorting is done by copying into an array (toArray), sorting that, and writing back through aListIterator. - ▸
Every
addallocates a node, which is garbage later. In allocation-heavy services, swapping aLinkedListqueue forArrayDequecan measurably cut GC pressure. - ▸
LinkedListis not thread-safe. The JDK's concurrent linked structures (ConcurrentLinkedQueue,ConcurrentLinkedDeque,LinkedBlockingQueue) are separate, lock-free or lock-based designs (Topic 13.8).
Remember this
- 1
LinkedListstores each element in a separate node object with three fields:item(the element),nextandprev(references to the neighbouring nodes). The list itself keepsfirst,lastandsize. Because each node knows both neighbours, it's a doubly linked list and can be walked in either direction. - 2
Ends are cheap.
addFirst,addLast,removeFirst,removeLast,getFirstandgetLastonly touch thefirstorlastnode and change a couple of references: O(1), with no array to grow and no elements to shift. - 3
The middle is slow.
get(i),set(i, e),add(i, e)andremove(i)must first walk to node i. The JDK starts from whichever end is closer (i < size / 2walks fromfirst), so it's at most n/2 hops, but that's still O(n). A loop likefor (int i = 0; i < list.size(); i++) list.get(i)is therefore O(n²). - 4
The O(1) middle insert people quote is only true when you're already standing at the node: through a
ListIterator,it.add(e)andit.remove()relink in O(1). Finding the position is still the O(n) walk. - 5
LinkedListimplements bothListandDeque, so it can be a list, a queue or a stack, and it acceptsnullelements. Memory is its weak point: each node is an object of about 24 bytes (with compressed references) on top of the element, versus 4 bytes per slot in anArrayList. Nodes are scattered across the heap, so walking them causes CPU cache misses. - 6
The modern advice is blunt: for a list, use
ArrayList; for a queue, stack or deque, useArrayDeque(Topic 9.7).LinkedListwins only in narrow cases, such as many inserts and removals at positions you reach with an iterator you already hold. For linked-list algorithms themselves (reversing, cycle detection, merging), see the DSA course's Linked Lists module (/dsa/linked-lists), where you build the nodes yourself.
Explain it without notes
Describe how LinkedList stores elements and what get(i) does.
When is LinkedList genuinely better than ArrayList?
Why is for (int i = 0; i < list.size(); i++) list.get(i) dangerous on a LinkedList?
Compare LinkedList and ArrayDeque as a queue.
Practice
Use a LinkedList as a stack to check whether the brackets in "{[()()]}" and "([)]" are balanced. Print true/false for each.
With a ListIterator, remove every element equal to "ad" from [news, ad, sport, ad, weather] in one pass and print the list.
Write a method that reverses a LinkedList<Integer> in place by repeatedly moving the last element to a growing position from the front, using removeLast and add(index, e). Print the result for [1, 2, 3, 4]. Then explain in a comment why Collections.reverse is better.
Trade-offs
- ↔
LinkedListgives true O(1) at both ends without ever copying, so no single operation is slow.ArrayDequeis O(1) amortised with an occasional grow, but faster on average and far smaller. - ↔
Iterator-based splicing in the middle is O(1) per change in a
LinkedListand O(n) in anArrayList. Unless the list is large and the edits many, the array's cache-friendliness still wins; measure before switching. - ↔
Allowing
nullelements is convenient but makespoll()andpeek()ambiguous.ArrayDeque's refusal ofnullis a deliberate design choice.
Done when you can
Done when you can draw the nodes of a doubly linked list and the
first/lastfields.Done when you can give the Big-O of
get(i),addFirst,removeLastandadd(i, e)on aLinkedList.Done when you can spot the O(n²) indexed loop and fix it with an iterator.
Done when you can use a
ListIteratorto add, set and remove while walking.Done when you can explain why
ArrayListandArrayDequeare usually better choices.