The night-before sheet: spot the pattern, write it the way you already wrote it, say why it's safe.
π There's no Two Pointers group in your DSA thoughts yet. The related ones (Arrays #1, #2, Math #3, JS #1) are linked where they apply.
"Two fingers on the data instead of two loops. Each finger only moves forward, so the whole thing is one pass."
The skill is the move rule: which finger moves, and why moving it can never skip an answer. Once you can say that in one sentence, the code is a few lines.
| The problem says⦠| Pattern | Picture it as | LC |
|---|---|---|---|
| sorted array + pair hitting a target, palindrome, container, rain water | Converging | πΆββοΈβ βπΆ two people walking toward each other from both ends | 167, 11, 42, 680, 977, 881 |
| 3 or 4 numbers summing to a target, closest sum, count triplets | k-Sum | π pin one number, 2Sum the rest | 15, 16, 18, 259 |
| linked list: cycle, middle, n-th from end, palindrome, reorder | Fast & Slow | π’π two runners on a track: the fast one laps the slow one | 141, 142, 876, 19, 234, 143, 160 |
| in place, O(1) space: remove, move zeroes, dedupe Β· or two sorted arrays | Write pointer | π a scanner hands books over, a shelver keeps only the good ones | 283, 26, 80, 88, 392, 350, 844 |
| sort 0/1/2, partition around a pivot in one pass | Three-way partition | ποΈποΈποΈ three bins: 0s left, 2s right, 1s stay in the middle | 75, 2161 |
"Squeeze from both ends; drop the one that can't help."
nums[i] + nums[j] is too small, then nums[i] with the biggest partner left is still too small. So nums[i] is in no answer: drop it (i++). Same for j when the sum is too big. Every step throws away one element and never a good one.nums[i] fails even with the biggest partner). Too big rules out its whole column.π‘ Sum too big β j--. Too small β i++. Equal β done.
function twoSum(nums: number[], target: number): number[] {
let i = 0;
let j = nums.length - 1;
while(i < j) {
const sum = nums[i] + nums[j];
if(sum > target) j--; // too big β only j can make it smaller
else if(sum < target) i++; // too small β only i can make it bigger
else return [i + 1, j + 1];
}
return [];
};
| step | i | j | sum ([2,7,11,15], target 18) | move |
|---|---|---|---|---|
| 1 | 0 | 3 | 2 + 15 = 17 < 18 | i++ |
| 2 | 1 | 3 | 7 + 15 = 22 > 18 | j-- |
| 3 | 1 | 2 | 7 + 11 = 18 β | return [2, 3] |
π‘ Area = shorter wall Γ width. Move the shorter wall: it caps the water, so keeping it can only give less.
function maxArea(height: number[]): number {
let i = 0;
let j = height.length - 1;
let maxArea = 0;
while(i < j) {
const minHei = Math.min(height[i], height[j]);
const width = j - i;
maxArea = Math.max(maxArea, minHei * width);
// move the SHORTER wall: it caps the water, and moving the taller one can't help
if(height[i] < height[j]) i++;
else j--;
}
return maxArea;
};
π‘ Water above a bar = min(leftMax, rightMax) β height. Move the side with the smaller max: that max is the bottleneck, so its water is already known.
[0,1,0,2,1,0,1,3,2,1,2,1], water 6. Water over a bar = min(leftMax, rightMax) β height. The side with the smaller max is the limit, so move that side.function trap(height: number[]): number {
const n = height.length;
let left = 0;
let right = n - 1;
let leftMaxHei = height[0];
let rightMaxHei = height[n - 1];
let res = 0;
while(left < right) {
// the side with the smaller max is the bottleneck, so its water is known
if(leftMaxHei < rightMaxHei) {
left++;
leftMaxHei = Math.max(leftMaxHei, height[left]);
res += leftMaxHei - height[left];
} else {
right--;
rightMaxHei = Math.max(rightMaxHei, height[right]);
res += rightMaxHei - height[right];
}
}
return res;
};
π‘ Normal palindrome check. On the first mismatch you get one delete: try skipping the left char or the right char.
function validPalindrome(s: string): boolean {
function isPalin(i: number, j: number): boolean {
while(i < j) {
if(s[i] !== s[j]) return false;
i++;
j--;
}
return true;
}
let i = 0;
let j = s.length - 1;
while(i < j) {
if(s[i] !== s[j]) {
// one delete allowed: skip the left char OR the right char
return isPalin(i + 1, j) || isPalin(i, j - 1);
}
i++;
j--;
}
return true;
};
π‘ Negatives make the biggest square sit at one of the two ends. Compare the ends and fill the result from the back.
function sortedSquares(nums: number[]): number[] {
const n = nums.length;
const res: number[] = Array(n).fill(0);
let i = 0;
let j = n - 1;
let k = n - 1; // fill from the BACK: the biggest square is at an end
while(i < j) {
const sq1 = nums[i] ** 2;
const sq2 = nums[j] ** 2;
if(sq1 <= sq2) {
res[k--] = sq2;
j--;
} else {
res[k--] = sq1;
i++;
}
}
res[0] = nums[i] ** 2; // i === j: the last one is the smallest
return res;
};
π‘ Sort. The heaviest always leaves now; take the lightest along if they fit together.
function numRescueBoats(people: number[], limit: number): number {
const sorted = [...people].sort((a, b) => b - a); // heaviest first
let boats = 0;
let i = 0; // heaviest left
let j = sorted.length - 1; // lightest left
while(i < j) {
if(sorted[i] + sorted[j] <= limit) j--; // lightest fits with the heaviest
boats++; // the heaviest always leaves
i++;
}
if(i === j) boats++; // one person left over
return boats;
};
sum < target β every j' in (i, j] also works, so count += j - i; i++. That's LC 259 3Sum Smaller and the idea behind 923."3Sum = a for-loop around 2Sum."
π‘ Sort. For each i, run the 167 squeeze on the right part. Skip equal values (for i, and for left/right after a hit) so no triplet repeats.
function threeSum(nums: number[]): number[][] {
const n = nums.length;
const sorted = [...nums].sort((a, b) => a - b);
const res: number[][] = [];
for(let i = 0; i < n; i++) {
// same value as before β its triplets are already found
if(i > 0 && sorted[i] === sorted[i - 1]) continue;
let left = i + 1;
let right = n - 1;
while(left < right) {
const sum = sorted[i] + sorted[left] + sorted[right];
if(sum < 0) {
left++;
} else if(sum > 0) {
right--;
} else {
res.push([sorted[i], sorted[left], sorted[right]]);
while(left < right && sorted[left] === sorted[left + 1]) left++;
while(left < right && sorted[right] === sorted[right - 1]) right--;
left++;
right--;
}
}
}
return res;
};
| Variant | Change from 3Sum |
|---|---|
| 16 3Sum Closest | No dedupe needed; keep the sum with the smallest |sum β target|, move like 167. |
| 18 4Sum | Two outer loops (fix two numbers), same squeeze inside, skip duplicates at both levels. O(nΒ³). |
| 259 3Sum Smaller | Count, don't list: sum < target β count += right - left; left++. |
| 923 3Sum With Multiplicity | Count with duplicates: on a hit, count the run of equal values (or use a value-count map). |
"Slow moves 1, fast moves 2. If there's a loop, the hare laps the tortoise."
null. Both take O(n) steps, O(1) space.π‘ while(fast && fast.next); meet β cycle, fast hits null β no cycle.
function hasCycle(head: ListNode | null): boolean {
let slow = head;
let fast = head;
while(fast && fast.next) {
fast = fast.next.next;
slow = slow.next;
if(slow === fast) return true;
}
return false;
};
π‘ After they meet, send one pointer back to head; move both 1 step at a time; they meet at the cycle start.
Why: head β start is the same distance as meeting point β start (going round the loop). So two walkers at speed 1 arrive together.
function detectCycle(head: ListNode | null): ListNode | null {
let slow = head;
let fast = head;
while(fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if(slow === fast) {
fast = head; // restart one from head, both at speed 1
while(slow !== fast) {
fast = fast.next;
slow = slow.next;
}
return slow; // they meet at the cycle start
}
}
return null;
};
π‘ When fast reaches the end, slow is in the middle.
function middleNode(head: ListNode | null): ListNode | null {
let slow = head;
let fast = head;
while(fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
return slow; // even length β the SECOND middle
};
π‘ When a pointer runs off its list, jump it to the other listβs head. Both walk A + B, so they meet at the join, or both reach null.
null together and the loop ends.function getIntersectionNode(headA: ListNode | null, headB: ListNode | null): ListNode | null {
let l1 = headA;
let l2 = headB;
// each walks A + B; same total length, so they line up at the join (or both hit null)
while(l1 !== l2) {
l1 = l1 ? l1.next : headB;
l2 = l2 ? l2.next : headA;
}
return l1;
};
π‘ Middle β reverse the second half β compare the halves node by node.
function isPalindrome(head: ListNode | null): boolean {
// 1 find the middle
let slow = head;
let fast = head;
while(fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
// 2 reverse from the middle to the end
let prev = null;
let curr = slow;
while(curr) {
const next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
// 3 compare the two halves
let first = head;
let second = prev;
while(first && second) {
if(first.val !== second.val) return false;
first = first.next;
second = second.next;
}
return true;
};
π‘ Middle β cut and reverse the second half β zip: one from the front, one from the back.
function reorderList(head: ListNode | null): void {
if(head === null || head.next === null) return;
// 1 find the middle
let slow = head;
let fast = head;
while(fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
// 2 reverse from mid to end (and cut the list in two)
let prev = null;
let curr = slow.next;
slow.next = null;
while(curr) {
const nextNode = curr.next;
curr.next = prev;
prev = curr;
curr = nextNode;
}
// 3 zip the two halves
let first = head;
let second = prev;
while(second !== null) {
const firstNext = first.next;
const secondNext = second.next;
first.next = second;
second.next = firstNext;
first = firstNext;
second = secondNext;
}
};
π‘ Your version: count the length, then stop on the node before the one to delete. len === n means delete the head.
fast an n-step head start, then move both until fast.next is null; slow is now just before the node to delete. A dummy node before head removes the "delete the head" special case.function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
let len = 0;
let curr = head;
while(curr) {
len++;
curr = curr.next;
}
if(len === n) return head.next; // removing the head itself
let steps = len - n - 1; // stop on the node BEFORE the one to delete
curr = head;
while(steps--) curr = curr.next;
curr.next = curr.next.next;
return head;
};
i β nums[i], reuse 142)."Read scans everything; write marks where the next keeper goes. Everything left of write is done."
read), another shelves only the keepers (write). The shelf behind write is always finished, and write never passes read, so nothing unread is overwritten.nums[0..write) is the finished answer and write β€ read always, so you never overwrite something you haven't read yet.write = finished (non-zeros, in order) Β· grey = zeros already passed Β· white = not read yet. write β€ read, so nothing unread is ever overwritten.π‘ Non-zero β swap it to write, write++. Zeroes end up at the back, order kept.
function moveZeroes(nums: number[]): void {
let write = 0;
for(let read = 0; read < nums.length; read++) {
if(nums[read] !== 0) {
[nums[read], nums[write]] = [nums[write], nums[read]];
write++;
}
}
};
π‘ Keep a value if it differs from the last kept one (nums[write - 1]). Return write, the count.
80 (keep at most 2): compare with nums[write - 2] instead.
function removeDuplicates(nums: number[]): number {
let write = 1; // the next slot to fill; nums[0] is always kept
for(let read = 1; read < nums.length; read++) {
if(nums[read] !== nums[write - 1]) {
[nums[read], nums[write]] = [nums[write], nums[read]];
write++;
}
}
return write;
};
π One finger on each array, like merging two sorted piles of cards: compare the two top cards, take one, move only that finger.
Two sorted inputs, a pointer in each, move the one that's behind. This is the merge step of merge sort.
π‘ Fill nums1 from the back: those slots are empty, so nothing gets overwritten. Leftover nums1 is already in place.
function merge(nums1: number[], m: number, nums2: number[], n: number): void {
let i = m - 1;
let j = n - 1;
let k = m + n - 1; // fill from the BACK: those slots are empty, nothing gets overwritten
while(i >= 0 && j >= 0) {
if(nums1[i] >= nums2[j]) nums1[k--] = nums1[i--];
else nums1[k--] = nums2[j--];
}
while(j >= 0) nums1[k--] = nums2[j--]; // leftover nums1 is already in place
};
π‘ Walk t; move sβs pointer only on a match. All of s matched β true.
function isSubsequence(s: string, t: string): boolean {
let sIdx = 0;
for(let tIdx = 0; tIdx < t.length && sIdx < s.length; tIdx++) {
if(t[tIdx] !== s[sIdx]) continue;
sIdx++;
}
return sIdx === s.length;
};
π‘ Sort both. Smaller value moves; equal β take it and move both.
function intersect(nums1: number[], nums2: number[]): number[] {
const sorted1 = [...nums1].sort((a, b) => a - b);
const sorted2 = [...nums2].sort((a, b) => a - b);
const res: number[] = [];
let i = 0;
let j = 0;
while(i < sorted1.length && j < sorted2.length) {
if(sorted1[i] < sorted2[j]) i++;
else if(sorted1[i] > sorted2[j]) j++;
else {
res.push(sorted1[i]); // match β take it, move BOTH
i++;
j++;
}
}
return res;
};
π‘ Walk both strings from the end, skipping chars erased by #; compare the survivors one by one.
function backspaceCompare(s: string, t: string): boolean {
let i = s.length - 1;
let j = t.length - 1;
// walk back to the next char that survives the backspaces
function nextValid(s: string, idx: number): number {
let backspace = 0;
while(idx >= 0) {
if(s[idx] === '#') backspace++;
else if(backspace > 0) backspace--;
else return idx;
idx--;
}
return idx;
}
while(i >= 0 || j >= 0) {
i = nextValid(s, i);
j = nextValid(t, j);
if(i < 0 && j < 0) return true;
if(i < 0 || j < 0) return false;
if(s[i] !== t[j]) return false;
i--;
j--;
}
return true;
};
"Three bins: 0s go left, 2s go right, 1s stay in the middle."
[0, left) = 0s Β· [left, mid) = 1s Β· [mid, right] = not checked yet Β· (right, end] = 2s.[2,0,2,1,1,0], part way: left = 2, mid = right = 3. After a swap with right, the value that lands at mid is still unchecked, which is why mid stays put.π‘ 0 β swap left, move left and mid. 1 β mid++. 2 β swap right, right-- and donβt move mid.
function sortColors(nums: number[]): void {
let left = 0; // next slot for a 0
let mid = 0; // the scanner
let right = nums.length - 1; // next slot for a 2
while(mid <= right) {
if(nums[mid] === 0) {
[nums[left], nums[mid]] = [nums[mid], nums[left]];
left++;
mid++; // what came from left is already checked (a 0 or a 1)
} else if(nums[mid] === 1) {
mid++;
} else {
[nums[right], nums[mid]] = [nums[mid], nums[right]];
right--; // NO mid++: the value from the right is unchecked
}
}
};
2161 Partition Around a Pivot keeps the original order, so it's a stable 3-pass (less, equal, greater) rather than swaps.
Not really two-pointer patterns, but asked in the same rounds. Know the one line.
π‘ k %= n, then reverse all, reverse the first k, reverse the rest.
function rotate(nums: number[], k: number): void {
const n = nums.length;
k = k % n;
function reverse(start: number, end: number) {
while(start < end) {
[nums[start], nums[end]] = [nums[end], nums[start]];
start++;
end--;
}
}
reverse(0, n - 1); // [1..7] β [7..1]
reverse(0, k - 1); // first k back in order
reverse(k, n - 1); // the rest back in order
};
π‘ Find the pivot from the right (first nums[i] < nums[i + 1]), swap it with the rightmost bigger value, reverse the suffix.
function nextPermutation(nums: number[]): void {
const n = nums.length;
function reverse(s: number, e: number): void {
while(s < e) {
[nums[s], nums[e]] = [nums[e], nums[s]];
s++;
e--;
}
}
// 1 pivot: first nums[i] < nums[i + 1] from the right
let pivot = -1;
for(let i = n - 2; i >= 0; i--) {
if(nums[i] < nums[i + 1]) {
pivot = i;
break;
}
}
if(pivot === -1) { // fully descending β wrap to the smallest
reverse(0, n - 1);
return;
}
// 2 swap with the rightmost value bigger than the pivot
for(let i = n - 1; i > pivot; i--) {
if(nums[pivot] < nums[i]) {
[nums[pivot], nums[i]] = [nums[i], nums[pivot]];
break;
}
}
// 3 the suffix is descending β reverse it to make it the smallest
reverse(pivot + 1, n - 1);
};
π‘ Left pass stores the product of everything to the left; right pass multiplies in everything to the right. No division.
suffix array, so the extra space is O(n). For O(1) extra, swap the array for one running variable: let suffix = 1; for (i = n - 1 β 0) { ans[i] *= suffix; suffix *= nums[i] }.function productExceptSelf(nums: number[]): number[] {
const n = nums.length;
const suffix = Array(n + 1).fill(1);
const ans = Array(n).fill(1);
// ans[i] = product of everything LEFT of i
for(let i = 1; i < n; i++) {
ans[i] = ans[i - 1] * nums[i - 1];
}
// Γ product of everything RIGHT of i
for(let i = n - 1; i >= 0; i--) {
suffix[i] = suffix[i + 1] * nums[i];
ans[i] = ans[i] * suffix[i + 1];
}
return ans;
};
suffix[n] = 1 means "nothing after the last index", like your Array(n + 1).fill(1) here.If you can say the trick for each in one sentence, you're ready.
| # | Problem | Pattern | The trick |
|---|---|---|---|
| 167 | Two Sum II | πΆ Converging | Too big β j--, too small β i++. |
| 1 | Two Sum (unsorted) | ποΈ Hash map | Need indices β map value β index; not two pointers. |
| 11 | Container With Most Water | πΆ Converging | Move the shorter wall. |
| 42 | Trapping Rain Water | πΆ Converging | Move the side with the smaller max; water = max β height. |
| 680 | Valid Palindrome II | πΆ Converging | On the first mismatch, try skipping left or right once. |
| 977 | Squares of a Sorted Array | πΆ Converging | Biggest square is at an end; fill from the back. |
| 881 | Boats to Save People | πΆ Converging | Heaviest leaves; lightest joins if it fits. |
| 15 | 3Sum | π k-Sum | Sort, fix i, squeeze the rest, skip duplicates. |
| 16 | 3Sum Closest | π k-Sum | Same squeeze, track the closest sum. |
| 259 | 3Sum Smaller | π k-Sum | count += right - left. |
| 141 / 142 | Linked List Cycle I / II | π’π Fast & Slow | Meet β cycle; reset one to head β they meet at the start. |
| 876 | Middle of the List | π’π Fast & Slow | Fast at the end β slow in the middle. |
| 19 | Remove Nth From End | π’π Fast & Slow | Stop on the node before; one pass = n-step head start. |
| 160 | Intersection of Two Lists | π’π Fast & Slow | At the end, switch to the other head. |
| 234 / 143 | Palindrome List / Reorder List | π’π Fast & Slow | Middle β reverse second half β compare / zip. |
| 287 | Find the Duplicate Number | π’π Fast & Slow | Index β value is a linked list with a cycle; run 142. |
| 283 | Move Zeroes | π Write pointer | Swap non-zeros to write. |
| 26 / 80 | Remove Duplicates I / II | π Write pointer | Compare with nums[write - 1] / nums[write - 2]. |
| 88 | Merge Sorted Array | π Two arrays | Fill from the back. |
| 392 | Is Subsequence | π Two arrays | Move s only on a match. |
| 844 | Backspace String Compare | π Two arrays | Walk from the end, skip what # erases. |
| 75 | Sort Colors | ποΈ Three-way | After swapping with right, don't move mid. |
| Bug | Input that exposes it |
|---|---|
while(i <= j) instead of <: pairs an element with itself | [3,4], target 6 β returns index 0 twice (3 + 3) |
| Converging on unsorted data | [4,1,3], target 4 β tries 4+3, 4+1, stops; misses 1+3 |
| 3Sum without the duplicate skips | [0,0,0,0] β [0,0,0] listed more than once |
Sort Colors: mid++ after swapping with right | [1,2,0] β gives [1,0,2] (the 0 from the right is never checked) |
Fast/slow without fast && fast.next | [1,2] β crash on fast.next.next |
| Returning the meeting point as the cycle start | 1β2β3β4β2 β they meet at 4, the start is 2 |
| Remove Nth: forgetting the "delete the head" case | [1], n = 1 β steps is β1 and the walk crashes on null; return head.next first |
Rotate: forgetting k %= n | [1,2], k = 3 β reverse indexes out of range; JS doesn't throw, the array silently grows to [undefined, 1, 2] |
| Merge Sorted Array filled from the front | [4,5,6,0,0,0] + [1,2,3] β overwrites 4, 5, 6 before they're used |
arr[100] and arr[-1] are undefined, so index bugs give wrong answers, not crashes.| Pattern | Time | Space |
|---|---|---|
| Converging (already sorted) | O(n) | O(1) Β· add O(n log n) if you sort first |
| 3Sum / 4Sum | O(nΒ²) / O(nΒ³) | O(1) besides the sort |
| Fast & slow | O(n) | O(1) |
| Write pointer, three-way | O(n) | O(1) |
| Two arrays | O(m + n) | O(1) in place, O(m + n) for a new array |
Why it's linear: each pointer only moves forward and never resets, so total work β€ total distance the pointers can travel.
Cover the right column (it's blurred; hover or tap to reveal). Say the answer out loud first.
| Prompt | Answer |
|---|---|
| 167: sum too small. Which pointer moves, and why is it safe? | i++. nums[i] with the biggest partner left is still too small, so it's in no answer. |
| 11: which wall moves? | The shorter one. It caps the area; moving the taller wall only shrinks the width. |
| 42: which side moves? | The side with the smaller max; that max decides its water. |
| Unsorted array, need indices of a pair summing to target? | Hash map (LC 1). Sorting would lose the indices. |
| How does 3Sum become 2Sum? | Sort, fix i, squeeze left/right on the rest; skip equal values. |
| Can two pointers count pairs? | Yes. Sorted, sum < target β count += right β left, then left++. |
| Find where a linked-list cycle starts? | After they meet, one goes back to head; both move 1; they meet at the start. |
| Palindrome linked list in O(1) space? | Middle β reverse the second half β compare. |
| Intersection of two lists in O(1) space? | At the end of a list, jump to the other head; they meet at the join. |
| Merge Sorted Array in place: from which end? | The back. Those slots are empty. |
| Sort Colors: when don't you move mid? | After swapping with right: the value that came in is unchecked. |
| Rotate Array in O(1) space? | k %= n; reverse all, reverse first k, reverse the rest. |
The code on this page is your own accepted LeetCode code, trimmed (brute-force blocks, header comments and edge cases the constraints rule out are removed) and re-tested on the LeetCode examples.