DAY 15 / 30 · WEEK 3

Linked Lists

Goal of the day

Concept

Think of a scavenger hunt: each clue tells you where to find the next clue, but nothing tells you where clue #4 is directly — you have to physically walk clue 1 → clue 2 → clue 3 → clue 4, reading each one to learn where the next is hidden. A linked list is exactly that: to reach the 4th node, there's no shortcut, you follow the chain one link at a time.

Every array lesson so far has relied on one fact: arr[i] is direct math — the engine computes a memory address from the base address plus i times the element size, in O(1). That direct math is exactly what makes inserting into the middle of an array expensive: to open up a slot at index i, every element from i onward has to physically shift over, which is O(n).

A linked list gives up direct math entirely. Each element (a node) holds a value and a pointer to the next node — nothing tells you where node 3 lives in memory until you've walked node 0 → node 1 → node 2 → node 3, one .next at a time. There is no arr[3] shortcut.

That trade produces the headline claim of this lesson, stated carefully: if you already have a pointer to the node just before where you want to insert or delete, the operation itself is O(1) — you rewire two pointers and you're done, no shifting. But if you only have an index or a value and have to find that node first, the traversal to reach it costs O(k) (k = how far in), which is the same order of growth as an array shift. The win isn't "linked lists are always faster" — it's "the splice itself is free once you're there."

Diagram: anatomy of a linked list

Anatomy: 5 nodes, each holding a value and a pointer to the next HEAD 10 20 30 40 50 NULL value next Each node stores a value and a pointer to the next node — nothing else. There's no formula for "jump to index 3" like arr[3] — you must follow .next four times from head to reach it. That walk IS the O(n) cost. HEAD marks the first node. The last node's next points to NULL.

Diagram: insert order matters

Insert 25 between 20 and 30 — do ① before ② Before: 20 30 new — not linked 25 25 exists in memory, but nothing points to it yet. ① new.next = prev.next: 20 30 25 new.next now also points at 30 — saved BEFORE prev is touched. Both 20 and 25 point at 30 for this one instant. That's fine. ② prev.next = new: 20 25 30 prev.next = new — 20 now points at 25, then 25 at 30. Nothing lost. Trap: reverse the order (② before ①) — the path to 30 is lost forever.

Diagram: delete order matters

Delete 30 from 20 → 30 → 40 — capture before you overwrite Before: 20 30 40 prev = 20, target = 30, prev.next.next = 40. ① removed = prev.next: 20 30 40 removed = prev.next — a reference to 30 is saved before anything is rewritten. This is the safety net. ② prev.next = removed.next: 20 40 unreachable 30 prev.next = removed.next — 20 now points straight at 40. Trap: skip ① and 30 becomes unreachable, its value lost forever.

Interactive visualization: insert at a given position

prev (traversal position) new node (not linked yet) spliced in / done
Press Play or Step to begin.

Interactive visualization: delete a node by value

prev (traversal position) checking prev.next marked for removal
Press Play or Step to begin.

The code: a singly linked list

class Node {
  constructor(val) {
    this.val = val;
    this.next = null;
  }
}

class LinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }

  insertAtHead(val) {
    const node = new Node(val);
    node.next = this.head;      // save old head as new node's next FIRST
    this.head = node;            // then repoint head
    if (!this.tail) this.tail = node; // list was empty — new node is also the tail
    this.size++;
    return this;
  }

  insertAtTail(val) {
    const node = new Node(val);
    if (!this.head) {
      this.head = node;
      this.tail = node;
    } else {
      this.tail.next = node;
      this.tail = node;
    }
    this.size++;
    return this;
  }

  insertAt(index, val) {
    if (index <= 0 || !this.head) return this.insertAtHead(val);
    if (index >= this.size) return this.insertAtTail(val);
    const node = new Node(val);
    let prev = this.head;
    for (let i = 0; i < index - 1; i++) prev = prev.next; // walk to the node BEFORE index
    node.next = prev.next;   // ① new node points at what prev used to point at
    prev.next = node;         // ② now it's safe to repoint prev
    this.size++;
    return this;
  }

  deleteValue(val) {
    if (!this.head) return false;
    if (this.head.val === val) {
      this.head = this.head.next;
      if (!this.head) this.tail = null; // list is now empty — reset tail too
      this.size--;
      return true;
    }
    let prev = this.head;
    while (prev.next && prev.next.val !== val) prev = prev.next;
    if (!prev.next) return false; // reached the end — value not found
    const removed = prev.next;    // ① capture the reference before touching it
    prev.next = removed.next;      // ② now it's safe to bypass it
    if (!prev.next) this.tail = prev; // removed the old tail — fix the tail pointer
    this.size--;
    return true;
  }

  toArray() {
    const out = [];
    let cur = this.head;
    while (cur) {
      out.push(cur.val);
      cur = cur.next;
    }
    return out;
  }
}

Line by line:

Dry run: insertAt(2, 25) on [10, 20, 30, 40, 50]

StepprevAction
1index 0 (value 10)prev = head — start the walk
2index 1 (value 20)i (0) < index-1 (1) — prev = prev.next
3index 1 (value 20)i (1) is no longer < index-1 (1) — loop stops here
4Create new node, value 25 (not linked yet)
5① new.next = prev.next → 25.next = node(30)
6② prev.next = new → node(20).next = node(25)
7Done: [10, 20, 25, 30, 40, 50]

The loop only ran once — that's the whole O(k) traversal cost for k = 2. Everything from step 4 onward is O(1): two pointer writes, regardless of whether the list had 5 nodes or 5 million.

Complexity

OperationTimeSpaceWhy
Insert at headO(1)O(1)Two pointer writes (new.next, head) — no traversal, regardless of list size.
Insert at tail (with tail pointer)O(1)O(1)tail already references the last node — one pointer write, no walk needed.
Insert at position kO(k)O(1)Walking from head to the node before position k dominates; the splice itself is O(1) once you're there.
Delete by value / position kO(k)O(1)Same story as insert — finding the node before the target costs O(k); the rewire is O(1).
Search / traverse for a valueO(n)O(1)No random access — you must walk one .next at a time from head; worst case visits every node.

k = how far the target position/value is from head (worst case k = n, e.g. deleting the last node by value with no tail-ward search). n = list size. Every operation above is O(1) extra space — nodes are rewired in place, nothing is copied.

Interview corner

How to talk through it out loud: narrate three things, in order, before you touch a pointer. First, which reference you need to save before you change anything — "I need a pointer to the node just before where I'm inserting or deleting." Second, the rewire itself, naming the exact order — "the new node's next first, then the previous node's next — never the reverse, or I lose the rest of the list." Third, the two edge cases you're handling — empty list, and single-node list. Interviewers watching a linked-list solution are listening specifically for whether you say the rewire order out loud before you draw it, because that's exactly where the classic bug lives.

Practice problems

1. Delete Node in a Linked List (Easy — LeetCode: Delete Node in a Linked List)

You're given a reference to a node (guaranteed not the tail) to delete — but no reference to head and no way to reach the node before it.

2. Remove Duplicates from Sorted List (Easy — LeetCode: Remove Duplicates from Sorted List)

Given the head of a sorted linked list, delete all duplicate values so each element appears only once, and return the (still sorted) list.

3. Add Two Numbers (Medium — LeetCode: Add Two Numbers)

Two non-empty linked lists represent non-negative integers, one digit per node, digits stored in reverse order. Add the two numbers and return the sum as a linked list in the same format.

Quiz

1. Why can inserting at the head of a linked list be O(1) while inserting at the front of an array is O(n)?

2. In insertAt(index, val), what determines the real time cost of the operation?

3. Why must you capture a node's next pointer (or the node itself) before reassigning any pointer during a delete?

4. What must be reset when the only node in a linked list is deleted?

5. Given only a reference to a node (not head, not its predecessor), how does the classic "Delete Node in a Linked List" trick remove it?