Part 9 · Lesson 36 · read
Reverse and Fast/Slow Pointers
Reverse a list, find the middle, and check if the list has a loop (a cycle).
On this page
Reversing a list
Reversing a linked list means making the last node the first one and the first node the last one, by changing the links. It is the most common linked-list question. The idea is easy: turn every arrow around. The hard part is doing it without losing the rest of the list. You walk down the list one node at a time. For each node, you make its next point backwards instead of forwards. Three variables do the job:
cur— the node we are turning around right now.prev— the node before it. It becomescur's newnext. It is also the head of the reversed part we have built so far.next— a saved copy ofcur.next. We need it because the link is about to be overwritten.
function reverseList(head) { let prev = null; let cur = head; while (cur !== null) { const next = cur.next; // 1. save the rest of the list before we cut the link cur.next = prev; // 2. point this node backwards prev = cur; // 3. prev steps forward cur = next; // 4. cur steps forward } return prev; // prev is the new head}console.log(toArray(reverseList(fromArray([1, 2, 3, 4])))); // [4, 3, 2, 1]console.log(toArray(reverseList(fromArray([7])))); // [7]console.log(toArray(reverseList(null))); // []const next = cur.next for a reason. If you write cur.next = prev first, you lose your only way to reach the rest of the list. Then the loop cannot continue. It is like cutting a rope before you have a grip on the other end.Traced: reversing 1 → 2 → 3
prev will become the head of the reversed part. cur is the node we are about to turn around.
▶let prev = null, cur = head;2while (cur !== null) {3 const next = cur.next;4 cur.next = prev;5 prev = cur;6 cur = next;7}8return prev;| reversed | [] |
| rest | [1, 2, 3] |
| prev | "null" |
| cur | 1 |
It takes O(n) time (the work grows in step with the list length) and O(1) extra space (only a few variables, however long the list is). It reuses the same nodes and only changes arrows.
The recursive version
Recursion (a function that calls itself) reverses the list from the back. Trust the function to reverse everything after the head. Then only one job is left: make the second node point back at the head.
function reverseList(head) { if (head === null || head.next === null) return head; // 0 or 1 node: already reversed const newHead = reverseList(head.next); // reverse everything after head head.next.next = head; // the node after head must now point back at head head.next = null; // head becomes the tail return newHead;}console.log(toArray(reverseList(fromArray([1, 2, 3, 4])))); // [4, 3, 2, 1]Take 1 → 2 → 3. The call on 3 returns 3 (the base case, the simplest case that stops the recursion). Back in the call on 2, head.next.next = head makes 3 point at 2, and head.next = null cuts the link 2 → 3. Back in the call on 1, the same step makes 2 point at 1. This takes O(n) time. It also uses O(n) stack space. The call stack is the computer's list of unfinished function calls, and it holds one entry (frame) per node. A very long list can overflow it. The loop version has no such risk, so write the loop version first.
Slow and fast pointers: the middle
To find the middle of a list, you could count the nodes and then walk half of that. That needs two passes. The slow and fast pointer trick (two pointers that walk the same list at different speeds) needs only one. Start both pointers at the head. Each round, slow moves one node and fast moves two. It is like two runners where one is twice as fast. When fast reaches the end, slow has covered half the distance. So it is at the middle.
function middleNode(head) { let slow = head, fast = head; while (fast !== null && fast.next !== null) { slow = slow.next; // 1 step fast = fast.next.next; // 2 steps } return slow; // when fast reaches the end, slow is halfway}console.log(middleNode(fromArray([1, 2, 3, 4, 5])).val); // 3console.log(middleNode(fromArray([1, 2, 3, 4])).val); // 3 (the second of the two middles)| Round | slow | fast | Continue? |
|---|---|---|---|
| start | 1 | 1 | fast and fast.next exist: yes |
| 1 | 2 | 3 | yes |
| 2 | 3 | 5 | fast.next is null: stop |
When the list has an even number of nodes, the loop ends with fast = null and slow on the second middle node. If you need the first middle node, start fast one step ahead, or stop when fast.next.next is null.
Detecting a cycle (Floyd's algorithm)
A list has a cycle (a loop) if some node's next points back to an earlier node. Then walking never reaches null. One easy way to detect this is to remember every node you have seen in a Set (a collection that keeps each item once). But that costs O(n) memory. Floyd's tortoise and hare uses the same slow and fast pair and needs only O(1) memory. If there is a cycle, the fast pointer catches up with the slow one from behind (like a fast runner lapping a slow runner on a track), and they meet. If there is no cycle, fast simply reaches the end.
function hasCycle(head) { let slow = head, fast = head; while (fast !== null && fast.next !== null) { slow = slow.next; fast = fast.next.next; if (slow === fast) return true; // they can only meet if the list loops } return false; // fast fell off the end: no cycle}const list = fromArray([1, 2, 3, 4]);console.log(hasCycle(list)); // falselist.next.next.next.next = list.next; // 4 now points back to 2: 1 -> 2 -> 3 -> 4 -> 2 ...console.log(hasCycle(list)); // trueWhy the fast pointer must catch the slow one
Once both pointers are inside the loop, look at the gap between them, counted along the loop. Each round, fast moves two and slow moves one. So the gap changes by exactly one. A gap that shrinks by one each round must reach zero. It cannot jump over zero. That is why they meet, and why a step of 2 is safe. With steps of 1 and 3, the pointers could step past each other forever in some loops. After slow enters the loop, the meeting takes at most about one loop length of extra rounds. So the total is O(n).
Putting it together: palindrome list
Is 1 → 2 → 2 → 1 the same forwards and backwards? (A list like this is called a palindrome.) You cannot walk a singly linked list backwards. So combine the two tricks. First, find the middle with slow and fast. Second, reverse the second half in place (without making a copy). Third, compare the two halves, starting from the outside. This takes O(n) time and O(1) space. Copying the values into an array is simpler, but it uses O(n) space.
function isPalindrome(head) { // 1. find the middle let slow = head, fast = head; while (fast !== null && fast.next !== null) { slow = slow.next; fast = fast.next.next; } // 2. reverse the second half let prev = null, cur = slow; while (cur !== null) { const next = cur.next; cur.next = prev; prev = cur; cur = next; } // 3. compare the first half with the reversed second half let a = head, b = prev; while (b !== null) { if (a.val !== b.val) return false; a = a.next; b = b.next; } return true;}console.log(isPalindrome(fromArray([1, 2, 2, 1]))); // trueconsole.log(isPalindrome(fromArray([1, 2, 3, 2, 1]))); // trueconsole.log(isPalindrome(fromArray([1, 2, 3]))); // falseThis version leaves the second half of the list reversed. If the caller still needs the original list, reverse that half again before you return. Say this in an interview. Interviewers like to hear it.
Reversing only part of a list
To reverse positions left to right, you need a dummy head (an extra fake node at the front, because the section may start at node 1). You also need a careful order of pointer moves. The code below repeatedly takes the node after start and moves it to the front of the section. This way, start moves to the back by itself.
// Reverse the nodes from position left to right (1-based).function reverseBetween(head, left, right) { const dummy = new ListNode(0, head); let before = dummy; for (let i = 1; i < left; i++) before = before.next; // the node just before the section const start = before.next; // will end up as the tail of the section for (let i = 0; i < right - left; i++) { const moving = start.next; // take the node after start... start.next = moving.next; // ...unlink it... moving.next = before.next; // ...and put it at the front of the section before.next = moving; } return dummy.next;}console.log(toArray(reverseBetween(fromArray([1, 2, 3, 4, 5]), 2, 4))); // [1, 4, 3, 2, 5]Practice questions
Before you code, draw three or four nodes and move the arrows with a pencil. You can see most mistakes on paper.
Given the head of a singly linked list, reverse the list and return the new head.
Hint 1
next, save where the rest of the list is.Hint 2
prev that starts as null. The first node must end up pointing at null.Show all 3 solutions with explanations
- Start
prev = null,cur = head. - Save
next = cur.next, pointcur.nextatprev. - Move
prevandcurforward. Whencuris null,previs the new head.
function reverseList(head) { let prev = null, cur = head; while (cur !== null) { const next = cur.next; cur.next = prev; prev = cur; cur = next; } return prev;}console.log(toArray(reverseList(fromArray([1, 2, 3, 4, 5])))); // [5, 4, 3, 2, 1]console.log(toArray(reverseList(fromArray([1, 2])))); // [2, 1]console.log(toArray(reverseList(null))); // []O(n) time, O(1) space. You visit each node once, and only the arrows change.
Reverse the rest of the list (everything after the head). Then make the old second node point back at the head. Last, cut the head's own link.
function reverseList(head) { if (head === null || head.next === null) return head; const newHead = reverseList(head.next); head.next.next = head; head.next = null; return newHead;}console.log(toArray(reverseList(fromArray([1, 2, 3, 4, 5])))); // [5, 4, 3, 2, 1]O(n) time, but also O(n) call-stack space (the memory that holds unfinished function calls). A list of 100,000 nodes can overflow the stack in JavaScript.
Read the values into an array. Then write them back in reverse order. It is easy, but it changes the values instead of moving the nodes.
function reverseList(head) { const vals = []; for (let c = head; c !== null; c = c.next) vals.push(c.val); for (let c = head; c !== null; c = c.next) c.val = vals.pop(); return head;}console.log(toArray(reverseList(fromArray([1, 2, 3])))); // [3, 2, 1]O(n) time and O(n) space. This is fine as a first answer. But the interviewer will almost always ask next, "now do it in O(1) space".
Use the loop version. Also know the recursive version and its stack cost. (LeetCode 206.)
Return the middle node of a linked list. If there are two middle nodes, return the second one.
Hint 1
Show all 2 solutions with explanations
Pass 1 counts the nodes (n). Pass 2 walks floor(n / 2) steps (floor rounds down to a whole number).
function middleNode(head) { let n = 0; for (let c = head; c !== null; c = c.next) n++; let mid = head; for (let i = 0; i < Math.floor(n / 2); i++) mid = mid.next; return mid;}console.log(middleNode(fromArray([1, 2, 3, 4, 5])).val); // 3console.log(middleNode(fromArray([1, 2, 3, 4, 5, 6])).val); // 4O(n) time, O(1) space, two passes.
Each round, move slow one step and fast two steps. When fast reaches the end, slow is in the middle.
function middleNode(head) { let slow = head, fast = head; while (fast !== null && fast.next !== null) { slow = slow.next; fast = fast.next.next; } return slow;}console.log(middleNode(fromArray([1, 2, 3, 4, 5])).val); // 3console.log(middleNode(fromArray([1, 2, 3, 4, 5, 6])).val); // 4Same O(n) time and O(1) space, but in one pass. You will reuse this loop in almost every slow and fast pointer problem.
Either one is accepted. The slow and fast version is what the question wants to see. (LeetCode 876.)
Return true if the linked list contains a cycle.
Hint 1
Hint 2
Show all 2 solutions with explanations
Keep a Set (a collection with no repeats) of nodes. If the next node is already in it, there is a cycle. (Nodes are objects, so the Set checks whether it is the very same node, not just the same value.)
function hasCycle(head) { const seen = new Set(); for (let c = head; c !== null; c = c.next) { if (seen.has(c)) return true; seen.add(c); } return false;}const a = fromArray([3, 2, 0, -4]);console.log(hasCycle(a)); // falsea.next.next.next.next = a.next; // -4 points back at 2console.log(hasCycle(a)); // trueO(n) time, O(n) space.
Slow moves 1 step and fast moves 2 steps. Inside a cycle, fast gets one step closer to slow each round. So it must meet slow.
function hasCycle(head) { let slow = head, fast = head; while (fast !== null && fast.next !== null) { slow = slow.next; fast = fast.next.next; if (slow === fast) return true; } return false;}const a = fromArray([3, 2, 0, -4]);a.next.next.next.next = a.next;console.log(hasCycle(a)); // trueconsole.log(hasCycle(fromArray([1, 2]))); // falseO(n) time, O(1) space. Compare the nodes with ===, not their values. Two different nodes can hold the same value.
Floyd's version is best. If you are not sure, start with the Set version and then improve it. (LeetCode 141.)
Return true if the values of a linked list read the same forwards and backwards.
Hint 1
Show all 2 solutions with explanations
Put the values in an array. Then compare them with two pointers, one from each end, moving towards the middle.
function isPalindrome(head) { const vals = []; for (let c = head; c !== null; c = c.next) vals.push(c.val); for (let i = 0, j = vals.length - 1; i < j; i++, j--) { if (vals[i] !== vals[j]) return false; } return true;}console.log(isPalindrome(fromArray([1, 2, 2, 1]))); // trueconsole.log(isPalindrome(fromArray([1, 2]))); // falseO(n) time, O(n) space.
- Find the middle with slow and fast pointers.
- Reverse the list from the middle onwards.
- Walk from the head and from the reversed half at the same time, and compare the values.
function isPalindrome(head) { let slow = head, fast = head; while (fast !== null && fast.next !== null) { slow = slow.next; fast = fast.next.next; } let prev = null, cur = slow; while (cur !== null) { const next = cur.next; cur.next = prev; prev = cur; cur = next; } for (let a = head, b = prev; b !== null; a = a.next, b = b.next) { if (a.val !== b.val) return false; } return true;}console.log(isPalindrome(fromArray([1, 2, 3, 2, 1]))); // trueconsole.log(isPalindrome(fromArray([1, 2, 3]))); // falseO(n) time, O(1) space. When the length is odd, the middle node ends up in the reversed half. The comparison stops when the reversed half is used up, so the middle node is only compared with itself.
The array copy is a good first answer. Then offer the O(1)-space version as the improvement, and say that it changes the list. (LeetCode 234.)
Reverse the nodes at positions left to right (1-based) and return the head.
Hint 1
Hint 2
left. From there, you change the links inside the section.Show all 2 solutions with explanations
- Walk to
before, the node in front of the section. - Reverse
right − left + 1nodes with the normal loop. - Reconnect the pieces: link
beforeto the new section head, and link the old section head to the node that came after the section.
function reverseBetween(head, left, right) { const dummy = new ListNode(0, head); let before = dummy; for (let i = 1; i < left; i++) before = before.next; const sectionTail = before.next; // will be last in the reversed section let prev = null, cur = sectionTail; for (let i = 0; i < right - left + 1; i++) { const next = cur.next; cur.next = prev; prev = cur; cur = next; } before.next = prev; // new head of the section sectionTail.next = cur; // the rest of the list return dummy.next;}console.log(toArray(reverseBetween(fromArray([1, 2, 3, 4, 5]), 2, 4))); // [1, 4, 3, 2, 5]console.log(toArray(reverseBetween(fromArray([5]), 1, 1))); // [5]O(n) time, O(1) space. It uses the reverse loop you already know.
Keep start where it is. Again and again, take the node after it and put it right after before.
function reverseBetween(head, left, right) { const dummy = new ListNode(0, head); let before = dummy; for (let i = 1; i < left; i++) before = before.next; const start = before.next; for (let i = 0; i < right - left; i++) { const moving = start.next; start.next = moving.next; moving.next = before.next; before.next = moving; } return dummy.next;}console.log(toArray(reverseBetween(fromArray([1, 2, 3, 4, 5]), 2, 4))); // [1, 4, 3, 2, 5]Same cost. It makes a single pass over the section and needs no final reconnect step. The downside is that it is easy to put the four assignments in the wrong order.
The first one is easier to get right when you are under pressure. (LeetCode 92.)
Swap every two nodes that sit next to each other, and return the head. Do not change the values inside the nodes. Change only the links.
Hint 1
prev pointer that sits just before each pair.Show all 2 solutions with explanations
prevsits just before a pair. Seta = prev.nextandb = a.next.- Change the links so the order becomes
prev → b → a → (rest). - Move
prevtoa, which is now the second node of the pair.
function swapPairs(head) { const dummy = new ListNode(0, head); let prev = dummy; while (prev.next !== null && prev.next.next !== null) { const a = prev.next, b = a.next; a.next = b.next; b.next = a; prev.next = b; prev = a; } return dummy.next;}console.log(toArray(swapPairs(fromArray([1, 2, 3, 4])))); // [2, 1, 4, 3]console.log(toArray(swapPairs(fromArray([1, 2, 3])))); // [2, 1, 3]console.log(toArray(swapPairs(null))); // []O(n) time, O(1) space.
Swap the first two nodes. Let the recursion handle the rest of the list, starting from the third node.
function swapPairs(head) { if (head === null || head.next === null) return head; const second = head.next; head.next = swapPairs(second.next); second.next = head; return second;}console.log(toArray(swapPairs(fromArray([1, 2, 3, 4])))); // [2, 1, 4, 3]The code is shorter, but it uses O(n) stack space.
Use the loop. The question usually does not allow swapping values (a.val with b.val). (LeetCode 24.)
A happy number is a number that reaches 1 by the process below. Again and again, replace n with the sum of the squares of its digits. Return true if it reaches 1. Return false if it loops forever.
Hint 1
Show all 2 solutions with explanations
Keep making the next number. If it is 1, return true. If you have seen it before, you are in a loop, so return false.
function digitSquares(n) { let sum = 0; while (n > 0) { const d = n % 10; sum += d * d; n = Math.floor(n / 10); } return sum;}function isHappy(n) { const seen = new Set(); while (n !== 1 && !seen.has(n)) { seen.add(n); n = digitSquares(n); } return n === 1;}console.log(isHappy(19)); // trueconsole.log(isHappy(2)); // falseThe numbers cannot keep growing. For a large n, the sum of the digit squares is much smaller than n. So the Set stays small.
Think of the sequence as a "list" where each number points to the next one. Run slow and fast along it. If they meet at 1, the number is happy. If they meet anywhere else, it is a loop.
function digitSquares(n) { let sum = 0; while (n > 0) { const d = n % 10; sum += d * d; n = Math.floor(n / 10); } return sum;}function isHappy(n) { let slow = n, fast = digitSquares(n); while (fast !== 1 && slow !== fast) { slow = digitSquares(slow); fast = digitSquares(digitSquares(fast)); } return fast === 1;}console.log(isHappy(19)); // trueconsole.log(isHappy(2)); // falseO(1) extra space. The lesson of this question: slow and fast pointers work on any sequence where "next = f(current)" (the next item comes from a rule applied to the current item), not just on linked lists. (LeetCode 202.)
The Set is fine. Also mention Floyd's version as the option that needs only O(1) space.
Make it stick
- Write the loop version of reverse from memory. Say what
prev,curandnextare each for. - Explain why the slow and fast loop condition is
fast !== null && fast.next !== null. - Explain why the fast pointer cannot jump over the slow pointer inside a cycle.
- List the three steps of the palindrome-list solution.
Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.
What's next
Lesson 37 merges two sorted lists. Then it uses the slow and fast pointers and the gap trick again for harder questions: where a cycle begins, removing the n-th node from the end, where two lists meet, adding two numbers stored in lists, and sorting a list.
Practise on LeetCode
Real interview problems that use exactly what this lesson teaches. Solve them in JavaScript after the practice questions above.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Part of the study guide
Related series