- Home
- /
- Tutorials
- /
- DSA Tutorial
- /
- Linked List Two Pointer Problems
Linked Lists
Linked List Two Pointer Problems
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) // 2The 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)) // trueO(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) // 2The 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) // 3All 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.
