Skip to content

Linked Lists

Definition

A linear data structure where elements are stored in nodes, each containing data and reference(s) to other node(s). Unlike arrays, elements are not stored in contiguous memory locations.

Singly Linked List

Singly Linked List

Doubly Linked List

Doubly Linked List

Circular Linked List

Circular Linked List


Big O Complexity

Linked List Time Complexity


When to Use

USE WHEN: - Frequent insertions/deletions at beginning - Size is unknown and changes frequently - Don't need random access - Memory allocation in small chunks is preferred - Implementing stacks, queues, or deques - Need to insert/delete in middle (with node reference)

DON'T USE WHEN: - Need fast random access by index - Memory efficiency is critical (pointer overhead) - Need cache-friendly traversal - Mostly doing lookups - Data size is fixed and known

Common Use Cases: - Implementing LRU Cache (with HashMap) - Undo functionality in applications - Music playlist (prev/next) - Browser history (back/forward) - Polynomial representation - Memory management (free lists)


Pros and Cons

PROS: - Dynamic size - grows/shrinks easily - O(1) insertion/deletion at head - No wasted memory from pre-allocation - Easy to implement stacks and queues - No shifting elements needed for insert/delete - Can easily split or merge lists

CONS: - O(n) random access - no indexing - Extra memory for pointers - Not cache-friendly (scattered memory) - No backward traversal (singly linked) - More complex to implement than arrays - Can't do binary search efficiently

SINGLY vs DOUBLY: - Singly: Less memory, simpler, can't traverse backward - Doubly: More memory, O(1) delete with node ref, bidirectional traversal


Implementation

Node Structure

// Singly Linked List Node
class ListNode {
    int val;
    ListNode next;

    ListNode(int val) {
        this.val = val;
        this.next = null;
    }
}

// Doubly Linked List Node
class DoublyListNode {
    int val;
    DoublyListNode prev;
    DoublyListNode next;

    DoublyListNode(int val) {
        this.val = val;
        this.prev = null;
        this.next = null;
    }
}

Singly Linked List

public class SinglyLinkedList {
    private ListNode head;
    private ListNode tail;
    private int size;

    public SinglyLinkedList() {
        head = null;
        tail = null;
        size = 0;
    }

    // Insert at head - O(1)
    public void addFirst(int val) {
        ListNode newNode = new ListNode(val);
        newNode.next = head;
        head = newNode;
        if (tail == null) {
            tail = head;
        }
        size++;
    }

    // Insert at tail - O(1) with tail pointer
    public void addLast(int val) {
        ListNode newNode = new ListNode(val);
        if (tail == null) {
            head = tail = newNode;
        } else {
            tail.next = newNode;
            tail = newNode;
        }
        size++;
    }

    // Insert at index - O(n)
    public void addAt(int index, int val) {
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException();
        }
        if (index == 0) {
            addFirst(val);
            return;
        }
        if (index == size) {
            addLast(val);
            return;
        }

        ListNode prev = getNode(index - 1);
        ListNode newNode = new ListNode(val);
        newNode.next = prev.next;
        prev.next = newNode;
        size++;
    }

    // Delete at head - O(1)
    public int removeFirst() {
        if (head == null) {
            throw new NoSuchElementException();
        }
        int val = head.val;
        head = head.next;
        if (head == null) {
            tail = null;
        }
        size--;
        return val;
    }

    // Delete at tail - O(n) for singly linked
    public int removeLast() {
        if (head == null) {
            throw new NoSuchElementException();
        }
        if (head == tail) {
            return removeFirst();
        }

        ListNode prev = head;
        while (prev.next != tail) {
            prev = prev.next;
        }
        int val = tail.val;
        prev.next = null;
        tail = prev;
        size--;
        return val;
    }

    // Get by index - O(n)
    public int get(int index) {
        return getNode(index).val;
    }

    private ListNode getNode(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException();
        }
        ListNode current = head;
        for (int i = 0; i < index; i++) {
            current = current.next;
        }
        return current;
    }

    // Search - O(n)
    public boolean contains(int val) {
        ListNode current = head;
        while (current != null) {
            if (current.val == val) {
                return true;
            }
            current = current.next;
        }
        return false;
    }

    public int size() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }
}

Doubly Linked List

public class DoublyLinkedList {
    private DoublyListNode head;
    private DoublyListNode tail;
    private int size;

    public void addFirst(int val) {
        DoublyListNode newNode = new DoublyListNode(val);
        if (head == null) {
            head = tail = newNode;
        } else {
            newNode.next = head;
            head.prev = newNode;
            head = newNode;
        }
        size++;
    }

    public void addLast(int val) {
        DoublyListNode newNode = new DoublyListNode(val);
        if (tail == null) {
            head = tail = newNode;
        } else {
            newNode.prev = tail;
            tail.next = newNode;
            tail = newNode;
        }
        size++;
    }

    // Delete given node - O(1)
    public void removeNode(DoublyListNode node) {
        if (node.prev != null) {
            node.prev.next = node.next;
        } else {
            head = node.next;
        }

        if (node.next != null) {
            node.next.prev = node.prev;
        } else {
            tail = node.prev;
        }
        size--;
    }

    // Delete at tail - O(1) for doubly linked
    public int removeLast() {
        if (tail == null) {
            throw new NoSuchElementException();
        }
        int val = tail.val;
        if (head == tail) {
            head = tail = null;
        } else {
            tail = tail.prev;
            tail.next = null;
        }
        size--;
        return val;
    }
}

Common Patterns & Problems

Reverse Linked List

// Iterative - O(n) time, O(1) space
public ListNode reverseIterative(ListNode head) {
    ListNode prev = null;
    ListNode current = head;

    while (current != null) {
        ListNode next = current.next;
        current.next = prev;
        prev = current;
        current = next;
    }
    return prev;
}

// Recursive - O(n) time, O(n) space (call stack)
public ListNode reverseRecursive(ListNode head) {
    if (head == null || head.next == null) {
        return head;
    }

    ListNode newHead = reverseRecursive(head.next);
    head.next.next = head;
    head.next = null;
    return newHead;
}

Detect Cycle (Floyd's Algorithm)

// Detect if cycle exists - O(n) time, O(1) space
public boolean hasCycle(ListNode head) {
    if (head == null) return false;

    ListNode slow = head;
    ListNode fast = head;

    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) {
            return true;
        }
    }
    return false;
}

// Find cycle start
public ListNode detectCycle(ListNode head) {
    if (head == null) return null;

    ListNode slow = head;
    ListNode fast = head;

    // Find meeting point
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) {
            break;
        }
    }

    if (fast == null || fast.next == null) {
        return null;  // No cycle
    }

    // Find cycle start
    slow = head;
    while (slow != fast) {
        slow = slow.next;
        fast = fast.next;
    }
    return slow;
}

Find Middle Element

// O(n) time, O(1) space
public ListNode findMiddle(ListNode head) {
    ListNode slow = head;
    ListNode fast = head;

    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;  // For even length, returns second middle
}

Merge Two Sorted Lists

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode dummy = new ListNode(0);
    ListNode current = dummy;

    while (l1 != null && l2 != null) {
        if (l1.val <= l2.val) {
            current.next = l1;
            l1 = l1.next;
        } else {
            current.next = l2;
            l2 = l2.next;
        }
        current = current.next;
    }

    current.next = (l1 != null) ? l1 : l2;
    return dummy.next;
}

Remove Nth Node From End

public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode fast = dummy;
    ListNode slow = dummy;

    // Move fast n+1 steps ahead
    for (int i = 0; i <= n; i++) {
        fast = fast.next;
    }

    // Move both until fast reaches end
    while (fast != null) {
        fast = fast.next;
        slow = slow.next;
    }

    slow.next = slow.next.next;
    return dummy.next;
}

Check Palindrome

public boolean isPalindrome(ListNode head) {
    if (head == null || head.next == null) return true;

    // Find middle
    ListNode slow = head, fast = head;
    while (fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }

    // Reverse second half
    ListNode secondHalf = reverseIterative(slow.next);

    // Compare
    ListNode p1 = head, p2 = secondHalf;
    while (p2 != null) {
        if (p1.val != p2.val) return false;
        p1 = p1.next;
        p2 = p2.next;
    }

    return true;
}

LRU Cache (Doubly Linked List + HashMap)

class LRUCache {
    private Map<Integer, DoublyListNode> cache;
    private DoublyListNode head, tail;
    private int capacity;

    class DoublyListNode {
        int key, value;
        DoublyListNode prev, next;
        DoublyListNode(int k, int v) { key = k; value = v; }
    }

    public LRUCache(int capacity) {
        this.capacity = capacity;
        cache = new HashMap<>();
        head = new DoublyListNode(0, 0);
        tail = new DoublyListNode(0, 0);
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
        if (!cache.containsKey(key)) return -1;
        DoublyListNode node = cache.get(key);
        moveToHead(node);
        return node.value;
    }

    public void put(int key, int value) {
        if (cache.containsKey(key)) {
            DoublyListNode node = cache.get(key);
            node.value = value;
            moveToHead(node);
        } else {
            DoublyListNode newNode = new DoublyListNode(key, value);
            cache.put(key, newNode);
            addToHead(newNode);
            if (cache.size() > capacity) {
                DoublyListNode removed = removeTail();
                cache.remove(removed.key);
            }
        }
    }

    private void addToHead(DoublyListNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(DoublyListNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void moveToHead(DoublyListNode node) {
        removeNode(node);
        addToHead(node);
    }

    private DoublyListNode removeTail() {
        DoublyListNode node = tail.prev;
        removeNode(node);
        return node;
    }
}

Tips & Tricks

Dummy Head: - Use dummy node to simplify edge cases - Avoids special handling for head operations - Return dummy.next as the actual head

Two Pointers: - Fast/slow for middle, cycle detection - Gap of n for finding nth from end

Common Edge Cases: - Empty list (head == null) - Single node - Two nodes - Cycle at head or tail

Java Collections: - LinkedList<E> - doubly linked - Implements List, Deque interfaces - Use for stack/queue implementations

Interview Tips: - Draw pictures - linked lists are visual - Track pointers carefully - Consider using recursion for elegance - Don't forget to update all pointers