Skip to main content

Linked Lists

Linked List Two Pointer Problems

Written by Updated

You cannot index into a linked list, so every question about position is answered with two pointers moving at different speeds. Learn the three shapes and you have covered most of what gets asked.

Why speed differences work

If one pointer moves twice as fast as another, then when the fast one reaches the end, the slow one is exactly halfway. Every trick here is a variation on that.

Find the middle

Fast and slow

javascript

function middle(head) {
  let slow = head
  let fast = head

  while (fast && fast.next) {
    slow = slow.next
    fast = fast.next.next
  }

  return slow
}

const demoList = { value: 1, next: { value: 2, next: { value: 3, next: null } } }
console.log(middle(demoList).value)  // 2

The condition checks both fast and fast.next. Drop either and an even-length list throws on null.next.

Detect a cycle

If the list loops, a fast pointer eventually laps a slow one. If it does not loop, the fast pointer reaches the end. There is no third outcome.

Floyd's cycle detection

javascript

function hasCycle(head) {
  let slow = head
  let fast = head

  while (fast && fast.next) {
    slow = slow.next
    fast = fast.next.next
    if (slow === fast) return true
  }

  return false
}

const demoB = { value: 2, next: null }
const demoA = { value: 1, next: demoB }
console.log(hasCycle(demoA))  // false
demoB.next = demoA
console.log(hasCycle(demoA))  // true

O(n) time, O(1) space. A Set of visited nodes also works and is easier to explain, but costs O(n) memory - mention both if you are asked.

Nth node from the end

Start one pointer n steps ahead. When it reaches the end, the other is n from the end. One pass, no length calculation.

Gap of n

javascript

function nthFromEnd(head, n) {
  let lead = head

  for (let i = 0; i < n; i++) {
    if (!lead) return null   // list shorter than n
    lead = lead.next
  }

  let trail = head
  while (lead) {
    lead = lead.next
    trail = trail.next
  }

  return trail
}

const demoList = { value: 1, next: { value: 2, next: { value: 3, next: null } } }
console.log(nthFromEnd(demoList, 2).value)  // 2

The dummy head trick

Any problem that might remove the first node gets fiddly, because there is no previous node to update. A dummy node in front removes the special case entirely:

Removing without special cases

javascript

function removeValue(head, target) {
  const dummy = new Node(null, head)
  let current = dummy

  while (current.next) {
    if (current.next.value === target) {
      current.next = current.next.next   // skip it
    } else {
      current = current.next
    }
  }

  return dummy.next   // may differ from the original head
}

// Node comes from the linked list lesson.
class Node {
  constructor(value, next = null) { this.value = value; this.next = next }
}

let demoNode = removeValue(new Node(1, new Node(2, new Node(1))), 1)
const demoOut = []
while (demoNode) { demoOut.push(demoNode.value); demoNode = demoNode.next }
console.log(demoOut)  // [2]

Returning dummy.next rather than head is essential - the original head may be the node that was removed.

Why fast and slow finds the middle

If one pointer advances two nodes for every one the other takes, then when the fast pointer reaches the end the slow pointer has covered exactly half the distance. No length count, no second pass.

The same asymmetry detects a cycle. Inside a loop the fast pointer gains one node per step on the slow one, so the gap closes by exactly one each time and they must eventually meet. If there is no cycle, fast simply runs off the end.

Middle, cycle, and nth from the end

javascript

function middleNode(head) {
  let slow = head
  let fast = head
  while (fast && fast.next) {
    slow = slow.next
    fast = fast.next.next
  }
  return slow
}

function hasCycle(head) {
  let slow = head
  let fast = head
  while (fast && fast.next) {
    slow = slow.next
    fast = fast.next.next
    if (slow === fast) return true
  }
  return false
}

// Start fast n nodes ahead, then move together.
function nthFromEnd(head, n) {
  let lead = head
  for (let i = 0; i < n; i++) {
    if (!lead) return null
    lead = lead.next
  }
  let trail = head
  while (lead) {
    lead = lead.next
    trail = trail.next
  }
  return trail
}

const demoList = { value: 1, next: { value: 2, next: { value: 3, next: null } } }
console.log(middleNode(demoList).value)     // 2
console.log(hasCycle(demoList))             // false
console.log(nthFromEnd(demoList, 1).value)  // 3

All three are one pass and O(1) extra space. The pattern behind them is the same: create a known gap between two pointers, then move both at the same rate.