Linked Lists

beginner linked-list pointers data-structure

A linked list is a linear data structure where elements aren’t stored in contiguous memory. Instead, each element (called a node) holds two things: the data and a pointer (or reference) to the next node.

In simple language, think of a linked list like a scavenger hunt. Each clue tells us where to find the next clue. We can only move forward by following the chain — we can’t jump to the 5th clue directly.

Why Use Linked Lists?

Arrays are great, but they have a weakness: inserting or deleting in the middle requires shifting everything over. That’s O(n). Linked lists can insert or delete in O(1) if we already have a pointer to the right spot.

The trade-off? We lose random access. Getting the 5th element means following 5 pointers. That’s O(n) instead of O(1) with arrays.

Singly Linked List

Each node points to the next. The last node points to null.

Singly Linked List
head -->
10 | next
-->
20 | next
-->
30 | next
-->
null

Doubly Linked List

Each node has pointers to both the next AND previous nodes. We can traverse in both directions.

Doubly Linked List
null
<-->
prev | 10 | next
<-->
prev | 20 | next
<-->
null

Node Definition

// Singly linked list node
class ListNode {
  constructor(val, next = null) {
    this.val = val;
    this.next = next;
  }
}

// Doubly linked list node
class DListNode {
  constructor(val, prev = null, next = null) {
    this.val = val;
    this.prev = prev;
    this.next = next;
  }
}
# Singly linked list node
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

# Doubly linked list node
class DListNode:
    def __init__(self, val=0, prev=None, next=None):
        self.val = val
        self.prev = prev
        self.next = next
// Singly linked list node
class ListNode {
    int val;
    ListNode next;
    ListNode(int val) { this.val = val; }
    ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}

// Doubly linked list node
class DListNode {
    int val;
    DListNode prev, next;
    DListNode(int val) { this.val = val; }
}

Core Operations

Insert at Head — O(1)

// Insert a new node at the beginning
function insertAtHead(head, val) {
  const newNode = new ListNode(val);
  newNode.next = head; // point new node to old head
  return newNode;       // new node is now the head
}
# Insert a new node at the beginning
def insert_at_head(head, val):
    new_node = ListNode(val)
    new_node.next = head  # point new node to old head
    return new_node       # new node is now the head
// Insert a new node at the beginning
static ListNode insertAtHead(ListNode head, int val) {
    ListNode newNode = new ListNode(val);
    newNode.next = head; // point new node to old head
    return newNode;       // new node is now the head
}

Delete a Node — O(n) search + O(1) delete

// Delete first node with given value
function deleteNode(head, val) {
  const dummy = new ListNode(0, head); // dummy to handle head deletion
  let prev = dummy;

  while (prev.next) {
    if (prev.next.val === val) {
      prev.next = prev.next.next; // skip over the node
      break;
    }
    prev = prev.next;
  }
  return dummy.next;
}
# Delete first node with given value
def delete_node(head, val):
    dummy = ListNode(0, head)  # dummy to handle head deletion
    prev = dummy

    while prev.next:
        if prev.next.val == val:
            prev.next = prev.next.next  # skip over the node
            break
        prev = prev.next
    return dummy.next
// Delete first node with given value
static ListNode deleteNode(ListNode head, int val) {
    ListNode dummy = new ListNode(0, head); // dummy to handle head deletion
    ListNode prev = dummy;

    while (prev.next != null) {
        if (prev.next.val == val) {
            prev.next = prev.next.next; // skip over the node
            break;
        }
        prev = prev.next;
    }
    return dummy.next;
}

Pro tip: The dummy node trick is hugely useful. It eliminates the edge case of deleting the head node. We’ll use it constantly in linked list problems.

Reverse a Linked List

This is the #1 most common linked list interview question. The idea: we walk through the list and flip each pointer to point backward instead of forward.

// Reverse a singly linked list -- O(n) time, O(1) space
function reverseList(head) {
  let prev = null;
  let curr = head;

  while (curr) {
    const next = curr.next; // save next before we break the link
    curr.next = prev;       // reverse the pointer
    prev = curr;            // move prev forward
    curr = next;            // move curr forward
  }
  return prev; // prev is the new head
}
# Reverse a singly linked list -- O(n) time, O(1) space
def reverse_list(head):
    prev = None
    curr = head

    while curr:
        next_node = curr.next  # save next before we break the link
        curr.next = prev       # reverse the pointer
        prev = curr            # move prev forward
        curr = next_node       # move curr forward
    return prev  # prev is the new head
// Reverse a singly linked list -- O(n) time, O(1) space
static ListNode reverseList(ListNode head) {
    ListNode prev = null;
    ListNode curr = head;

    while (curr != null) {
        ListNode next = curr.next; // save next before we break the link
        curr.next = prev;          // reverse the pointer
        prev = curr;               // move prev forward
        curr = next;               // move curr forward
    }
    return prev; // prev is the new head
}

Three pointers is all we need: prev, curr, and next. Memorize this pattern — it shows up everywhere.

Detect a Cycle — Floyd’s Algorithm

If a linked list has a cycle (a node’s next points back to an earlier node), we’ll loop forever trying to reach null. Floyd’s tortoise and hare algorithm detects this using two pointers moving at different speeds.

The slow pointer moves 1 step at a time. The fast pointer moves 2 steps. If there’s a cycle, they’ll eventually meet. If there’s no cycle, fast reaches null.

// Detect cycle in a linked list
function hasCycle(head) {
  let slow = head;
  let fast = head;

  while (fast && fast.next) {
    slow = slow.next;       // move 1 step
    fast = fast.next.next;  // move 2 steps
    if (slow === fast) return true; // they met -- cycle exists!
  }
  return false; // fast reached null -- no cycle
}
# Detect cycle in a linked list
def has_cycle(head):
    slow = head
    fast = head

    while fast and fast.next:
        slow = slow.next       # move 1 step
        fast = fast.next.next  # move 2 steps
        if slow == fast:
            return True  # they met -- cycle exists!
    return False  # fast reached null -- no cycle
// Detect cycle in a linked list
static boolean hasCycle(ListNode head) {
    ListNode slow = head;
    ListNode fast = head;

    while (fast != null && fast.next != null) {
        slow = slow.next;       // move 1 step
        fast = fast.next.next;  // move 2 steps
        if (slow == fast) return true; // they met -- cycle exists!
    }
    return false; // fast reached null -- no cycle
}

Why does this work? Think of two runners on a circular track. The faster one will always lap the slower one. Same idea here.

Merge Two Sorted Lists

Another interview classic. We have two sorted linked lists and want to merge them into one sorted list.

// Merge two sorted linked lists
function mergeTwoLists(l1, l2) {
  const dummy = new ListNode(0); // dummy head
  let tail = dummy;

  while (l1 && l2) {
    if (l1.val <= l2.val) {
      tail.next = l1;
      l1 = l1.next;
    } else {
      tail.next = l2;
      l2 = l2.next;
    }
    tail = tail.next;
  }
  tail.next = l1 || l2; // attach remaining nodes
  return dummy.next;
}
# Merge two sorted linked lists
def merge_two_lists(l1, l2):
    dummy = ListNode(0)  # dummy head
    tail = dummy

    while l1 and l2:
        if l1.val <= l2.val:
            tail.next = l1
            l1 = l1.next
        else:
            tail.next = l2
            l2 = l2.next
        tail = tail.next
    tail.next = l1 or l2  # attach remaining nodes
    return dummy.next
// Merge two sorted linked lists
static ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode dummy = new ListNode(0); // dummy head
    ListNode tail = dummy;

    while (l1 != null && l2 != null) {
        if (l1.val <= l2.val) {
            tail.next = l1;
            l1 = l1.next;
        } else {
            tail.next = l2;
            l2 = l2.next;
        }
        tail = tail.next;
    }
    tail.next = (l1 != null) ? l1 : l2; // attach remaining
    return dummy.next;
}

Key Patterns to Remember

PatternWhen to Use
Dummy nodeWhen the head might change (deletion, merging)
Two pointers (slow/fast)Cycle detection, finding middle node
Prev/Curr/Next pointersReversing a list
RecursionWhen iterative feels awkward (merge, reverse)

Complexity Cheat Sheet

OperationSingly Linked ListDoubly Linked List
Access by indexO(n)O(n)
Insert at headO(1)O(1)
Insert at tailO(n) / O(1) with tail pointerO(1) with tail pointer
Delete (given node)O(n) to find prevO(1) — we have prev pointer
SearchO(n)O(n)

The main takeaway: linked lists trade random access for efficient insertion/deletion. In interviews, watch for problems where we need to manipulate pointers — that’s where linked lists shine.