Linked Lists

intermediate30 min

A different arrangement

An array keeps its elements packed together in one block. A linked list does the opposite: each element lives in its own small object, and each object holds a reference to the next one.

class Node {
    int value;
    Node next;         // reference to the following node, or null at the end

    Node(int value) {
        this.value = value;
        this.next = null;
    }
}

A list of three values is three Node objects, each pointing at the next:

[10] -> [20] -> [30] -> null

You keep a reference to the first node, called the head. Everything else is reached by following next from there.

This depends on understanding references — if Node next holding another object feels unclear, review Objects and References first.

Building one by hand

Node head = new Node(10);
head.next = new Node(20);
head.next.next = new Node(30);

// walk the list
Node current = head;
while (current != null) {
    System.out.println(current.value);
    current = current.next;
}

The walking pattern — a current reference that advances until it hits null — is how every operation on a linked list works. It is worth writing out a few times until it is automatic.

Use a separate variable to walk

Node current = head;
while (current != null) { ... current = current.next; }

Advance current, never head. If you write head = head.next you lose the start of the list permanently, and there is no way to get it back — nothing else refers to those earlier nodes.

Why insertion is cheap

To insert a value after a node you already have a reference to, you rearrange two references. Nothing moves.

void insertAfter(Node node, int value) {
    Node fresh = new Node(value);
    fresh.next = node.next;      // new node points at what came next
    node.next = fresh;           // previous node points at the new one
}

The order matters. Setting node.next = fresh first would lose the reference to the rest of the list.

Compare with an array, where inserting in the middle shifts every following element. Here it is two assignments regardless of how long the list is.

Removing is similarly cheap — point around the node you want gone:

void removeAfter(Node node) {
    if (node.next != null) {
        node.next = node.next.next;
    }
}

The removed node is no longer reachable, and Java’s garbage collector reclaims it.

Why indexing is expensive

There is no arithmetic that finds the fifth node. The nodes are scattered in memory, so the only way to reach index 5 is to start at the head and follow next five times.

int get(Node head, int index) {
    Node current = head;
    for (int i = 0; i < index; i++) {
        if (current == null) throw new IndexOutOfBoundsException();
        current = current.next;
    }
    return current.value;
}

This is the fundamental trade: arrays give instant indexing and expensive insertion, linked lists give cheap insertion and slow indexing.

Array versus linked list

OperationArray / ArrayListLinked list
Read by indexInstantProportional to index
Insert at the frontProportional to sizeInstant
Insert after a known nodeProportional to sizeInstant
Remove a known nodeProportional to sizeInstant
Find a valueProportional to sizeProportional to size
Memory per elementJust the valueValue plus a reference

Java’s LinkedList

You rarely write your own. Java provides one, and it implements both List and Deque.

import java.util.LinkedList;

LinkedList<String> tasks = new LinkedList<>();
tasks.add("intake");             // append
tasks.addFirst("initialise");    // prepend — instant
tasks.addLast("shoot");

System.out.println(tasks.getFirst());   // initialise
tasks.removeFirst();

Java’s version is doubly linked — each node also holds a previous reference. That makes it possible to walk backwards and to remove from the end instantly.

In practice, prefer ArrayList

This surprises people, so it is worth stating plainly: for most real programs ArrayList outperforms LinkedList even for operations where the table above favours linked lists.

The reason is memory layout. An array’s elements sit together, so the processor loads several at once into its cache. Linked-list nodes are scattered, so each hop may be a fresh trip to main memory. That penalty often outweighs the theoretical advantage.

LinkedList earns its place when you are constantly adding and removing at both ends — and for that, use it through the Deque interface. See Deques.

Why learn it then

Two reasons, both practical.

The node-and-reference idea is the foundation for trees and graphs, which are genuinely the right tool for many problems. A tree is a node with several next references instead of one. If linked lists make sense, Trees will too.

And it teaches you to reason about references — what happens when two variables point at the same object, and what it means for something to become unreachable.

Common mistakes

  • Losing the head by advancing it instead of a separate variable.
  • Wrong assignment order when inserting, which drops the rest of the list.
  • Forgetting the null check while walking, giving a NullPointerException at the end.
  • Assuming get(i) is cheap. A loop calling get(i) on a linked list re-walks from the head every time, turning a single pass into something far slower. Use a for-each loop or an iterator instead.
  • Reaching for LinkedList by default. ArrayList is usually faster.

Practice

Use your own Node class for 1-4, then compare with java.util.LinkedList.

  1. Build a linked list of the values 1 through 5 and print them in order.
  2. Write a method that counts the nodes in a list.
  3. Write a method that returns the largest value in a list.
  4. Write a method that reverses a linked list by rearranging references, without creating new nodes.
  5. Using java.util.LinkedList, add five task names and then print them in reverse order.
Hints
  1. Create the head, then chain with next, or loop and keep a tail reference so appending stays cheap.
  2. Walk with a counter.
  3. Walk, tracking a running maximum. Start it at the head’s value.
  4. Keep three references: previous, current, next. On each step, save current.next, point current.next at previous, then shift all three forward. The new head is the final previous. Draw it on paper first — this one is genuinely fiddly.
  5. descendingIterator(), or repeatedly removeLast().

Next