Back to articles

DSA for Android

Linked Lists Explained: A Visual, Beginner-to-Expert Guide (Java & Kotlin)

Linked Lists Explained: A Visual, Beginner-to-Expert Guide (Java & Kotlin)

 

A linked list is nothing more than boxes and arrows: a piece of data, and a note telling you where the next piece lives. That's it. Everything else in this guide is just consequences of that one idea. We'll build it up node by node, with a diagram for every move, from your first "what even is this" to writing cycle-detection code from memory.

linked list diagram - 00-hero-chain
FIG. 0 — a linked list is a chain of nodes, each one pointing to the next
SHEET 01 Beginner

Why not just use an array?

Before drawing a single node, it helps to know the problem a linked list is solving. Arrays are great — until you need to grow, shrink, or rearrange them a lot.

Array in memory

An array is one continuous block of memory. That makes reading element [i] instant — the computer just does math on the address. But inserting a new value in the middle means physically sliding every element after it over by one slot.

linked list diagram - 01-array-in-memory

Linked list in memory

A linked list's nodes can live anywhere in memory. Nothing has to move when you insert — you just rewrite two arrows. The trade-off: to reach node 5, you must walk through nodes 1 through 4 first. There is no "jump straight to index i."

linked list diagram - 02-linked-list-in-memory
Rule of thumb

Reach for arrays when you read a lot by position. Reach for linked lists when you insert and remove a lot, especially at the ends, and you're fine walking node-by-node to get anywhere else.

SHEET 02 Beginner

Anatomy of a single node

Every node is a tiny two-part container. Learn this shape and the rest of the guide is just this shape, repeated.

linked list diagram - 03-node-anatomy
FIG. 1 — a node holds a value, and the address of whoever comes next

In code, that's a small class or struct with two fields. Nothing more.

Node — 2 fields

Java

class Node {
    int data;
    Node next; // reference to the next node, or null

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

Kotlin

class Node(var data: Int, var next: Node? = null)
// next is nullable — null means "nothing comes after me"

That's the entire vocabulary. A linked list itself is just a class that remembers where the chain starts:

LinkedList — the entry point

Java

class LinkedList {
    Node head; // the only thing we truly need to remember
    int size = 0;
}

Kotlin

class LinkedList {
    var head: Node? = null // the only thing we truly need to remember
    var size = 0
}
SHEET 03 Beginner

The singly linked list, assembled

Chain a few nodes together and you have a singly linked list — "singly" because each node only points forward, never back. There's no way to walk backward once you've stepped off a node.

linked list diagram - 04-singly-list
FIG. 2 — head → 4 → 11 → 8 → NULL. Lose the head reference and the whole chain is unreachable.
The one rule that matters

head is the only door into the list. Every single operation — insert, delete, search — starts by walking from head, because that's the only address the list itself remembers.

SHEET 04 Intermediate

Insert & delete: rewiring two arrows

This is the part that actually earns a linked list its keep. Every insert or delete is just: point the new arrow, then let go of the old one. Nothing shifts.

Insert at the head

Cheapest operation in the whole structure — O(1), no walking required.

linked list diagram - 05-insert-head-before
before
linked list diagram - 06-insert-head-after
after — new node 9 becomes the head

insertAtHead — O(1)

Java

void insertAtHead(int value) {
    Node newNode = new Node(value);
    newNode.next = head; // point forward into the old chain
    head = newNode;       // then move the door
    size++;
}

Kotlin

fun insertAtHead(value: Int) {
    val newNode = Node(value, next = head) // point forward into the old chain
    head = newNode                         // then move the door
    size++
}

Insert at the tail

You have to walk to the last node first — O(n) — then attach after it.

linked list diagram - 07-insert-tail
FIG. 3 — reaching the tail costs the walk; attaching itself is instant

insertAtTail — O(n)

Java

void insertAtTail(int value) {
    Node newNode = new Node(value);
    if (head == null) {
        head = newNode;
        size++;
        return;
    }
    Node current = head;
    while (current.next != null) { // walk until the last node
        current = current.next;
    }
    current.next = newNode; // attach after it
    size++;
}

Kotlin

fun insertAtTail(value: Int) {
    val newNode = Node(value)
    if (head == null) {
        head = newNode
        size++
        return
    }
    var current = head
    while (current?.next != null) { // walk until the last node
        current = current.next
    }
    current?.next = newNode // attach after it
    size++
}

Delete a node

Deleting isn't destruction — it's just skipping. Point the previous node's arrow past the one you're removing, and it quietly falls out of reach.

linked list diagram - 08-delete-before
before — target: 11
linked list diagram - 09-delete-after
after — the arrow leapfrogs it; 11 is now unreachable

delete — O(n)

Java

void delete(int value) {
    if (head == null) return;
    if (head.data == value) { // special case: removing the head
        head = head.next;
        size--;
        return;
    }
    Node current = head;
    while (current.next != null && current.next.data != value) {
        current = current.next;
    }
    if (current.next != null) {
        current.next = current.next.next; // leapfrog the target
        size--;
    }
}

Kotlin

fun delete(value: Int) {
    if (head == null) return
    if (head?.data == value) { // special case: removing the head
        head = head?.next
        size--
        return
    }
    var current = head
    while (current?.next != null && current.next?.data != value) {
        current = current.next
    }
    current?.next?.let {
        current.next = it.next // leapfrog the target
        size--
    }
}
Watch for this

Removing the head is always a special case — there's no "previous node" whose arrow you can rewire, so you just move head itself. Beginners' bugs almost always live in this one branch.

SHEET 05 Beginner

Search & traverse

No shortcuts here — to find anything, you follow the arrows one at a time until you either find it or fall off the end.

contains / traverse — O(n)

Java

boolean contains(int value) {
    Node current = head;
    while (current != null) {
        if (current.data == value) return true;
        current = current.next; // step forward
    }
    return false;
}

void printAll() {
    Node current = head;
    while (current != null) {
        System.out.print(current.data + " -> ");
        current = current.next;
    }
    System.out.println("NULL");
}

Kotlin

fun contains(value: Int): Boolean {
    var current = head
    while (current != null) {
        if (current.data == value) return true
        current = current.next // step forward
    }
    return false
}

fun printAll() {
    var current = head
    val sb = StringBuilder()
    while (current != null) {
        sb.append(current.data).append(" -> ")
        current = current.next
    }
    println(sb.append("NULL"))
}

This is the pattern behind almost every linked-list function you'll ever write: current = head, loop while current != null, step with current = current.next. Memorize that shape and the rest is details.

SHEET 06 Intermediate

Doubly linked lists

Give every node a second arrow pointing backward, and you can walk the list in either direction — at the cost of one extra pointer to keep correct on every operation.

linked list diagram - 10-doubly-linked
FIG. 4 — two arrows per node: next and prev

DoublyNode — 3 fields

Java

class DoublyNode {
    int data;
    DoublyNode next;
    DoublyNode prev;

    DoublyNode(int data) {
        this.data = data;
    }
}

Kotlin

class DoublyNode(
    var data: Int,
    var next: DoublyNode? = null,
    var prev: DoublyNode? = null
)
Why bother

Deleting a node you already have a reference to becomes O(1) instead of O(n) — you don't need to walk from the head to find "the one before it," because prev already tells you. That's exactly why LinkedList in Java's standard library, and most "undo" or "browser history" implementations, are doubly linked under the hood.

SHEET 07 Intermediate

Circular linked lists

Take the last node's arrow and, instead of pointing to NULL, point it back to the head. Now there's no end — just a loop.

linked list diagram - 11-circular-linked
FIG. 5 — the last node's arrow (in orange) loops back to the first

Handy anywhere something repeats in a cycle with no natural "end": a music playlist on repeat, round-robin CPU scheduling, or a multiplayer game rotating through players' turns.

The classic trap

A plain while (current != null) traversal never terminates on a circular list — there is no null to hit. You either loop a known number of times, or stop when you arrive back at the node you started from.

SHEET 08 Intermediate

The cost sheet

Same operations, four structures. This table is the one thing worth memorizing before an interview.

Operation Array Singly linked Doubly linked Circular
Access by index O(1) O(n) O(n) O(n)
Search by value O(n) O(n) O(n) O(n)
Insert at head O(n) O(1) O(1) O(1)
Insert at tail O(1)* O(n) O(1)** O(1)**
Delete, node in hand O(n) O(n) O(1) O(1)
Extra memory / node none 1 pointer 2 pointers 1–2 pointers

* amortized, assumes room at the end. ** when a tail reference is kept.

SHEET 09 Beginner

Where this actually shows up

Not just an interview topic — you're using linked lists constantly without noticing.

Undo / redo

Each edit is a node; undo just walks backward one step, redo walks forward.

Browser history

Back and forward buttons are a doubly linked list of visited pages.

Music queue

"Next track" and "on repeat" map naturally onto singly and circular lists.

OS task scheduling

Round-robin schedulers rotate through processes with a circular list.

Hash map buckets

Chained hash tables resolve collisions by hanging a tiny linked list off each bucket.

LRU caches

A doubly linked list plus a hash map gives O(1) "move to front" — the classic LRU cache design.

SHEET 10 Expert

Practice problems

These three questions cover most of what actually gets asked. Try each one on paper before opening the answer.

Reverse a singly linked list, in place

Walk forward once, but at each node flip its arrow to point backward instead of forward. You need three pointers marching together: the node before, the current node, and a temporary hold on "next" before you overwrite it.

reverse — O(n) time, O(1) space

Java

Node reverse(Node head) {
    Node prev = null;
    Node current = head;
    while (current != null) {
        Node next = current.next; // save before we overwrite it
        current.next = prev;      // flip the arrow
        prev = current;
        current = next;
    }
    return prev; // prev is now the new head
}

Kotlin

fun reverse(head: Node?): Node? {
    var prev: Node? = null
    var current = head
    while (current != null) {
        val next = current.next // save before we overwrite it
        current.next = prev     // flip the arrow
        prev = current
        current = next
    }
    return prev // prev is now the new head
}
Detect a cycle without extra memory

Two runners on the same track, one moving twice as fast. If the list loops, the fast one eventually laps the slow one and they meet. If the list ends cleanly, the fast runner hits null first. This is Floyd's cycle-detection algorithm, nicknamed "tortoise and hare."

hasCycle — O(n) time, O(1) space

Java

boolean hasCycle(Node head) {
    Node slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) return true; // they lapped each other
    }
    return false;
}

Kotlin

fun hasCycle(head: Node?): Boolean {
    var slow = head
    var fast = head
    while (fast?.next != null) {
        slow = slow?.next
        fast = fast.next?.next
        if (slow === fast) return true // they lapped each other
    }
    return false
}
Find the middle node in one pass

Same tortoise-and-hare idea, minus the cycle. When the fast runner reaches the end, the slow one is standing exactly in the middle — because it only covered half the distance.

findMiddle — O(n) time, O(1) space

Java

Node findMiddle(Node head) {
    Node slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow; // standing at the midpoint
}

Kotlin

fun findMiddle(head: Node?): Node? {
    var slow = head
    var fast = head
    while (fast?.next != null) {
        slow = slow?.next
        fast = fast.next?.next
    }
    return slow // standing at the midpoint
}
SHEET 11 Expert

Expert notes: what the diagrams don't show

Everything above is correct, and also slightly idealized. Here's what changes once you're optimizing for real hardware.

Cache locality

Arrays win on real hardware more often than Big-O suggests, because array elements sit next to each other in memory and the CPU's cache loads them in bulk. Linked-list nodes are scattered, so every .next can be a fresh cache miss. In practice, a well-tuned array or a hybrid structure often outperforms a linked list even for insert-heavy workloads at small-to-medium sizes.

Skip lists

A skip list stacks several linked lists on top of each other, each one skipping more nodes than the last — like an express lane above the local lane. It gives you O(log n) search in a linked structure, which is how Redis's sorted sets and some databases implement ordered, insertable collections without a balanced tree.

Sentinel (dummy) nodes

A permanent, empty node placed before the real head removes the "is this the head?" special case from every insert and delete. Many production implementations use one purely to delete a branch of bugs.

When to actually avoid it

If you mostly read by index, binary search, or need cache-friendly iteration over millions of elements — use an array or a growable array (ArrayList/ArrayDeque). Reach for a linked list specifically when you're inserting or removing from the ends or middle far more often than you're reading by position.

5 views · 0 comments

Comments (0)

No comments yet. Be the first to share your thoughts.