πŸ‘‰ Two Pointers

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.

🧠 0 · How to think

"Two fingers on the data instead of two loops. Each finger only moves forward, so the whole thing is one pass."

🐌 two loops: every pairβœ“βœ“βœ“βœ“βœ“βœ“βœ“βœ“βœ“βœ“5 items β†’ 10 checks Β· O(nΒ²)
πŸ‘‰ two pointers: one sweep27111519L →← Reach check drops one end5 items β†’ at most 4 checks Β· O(n)
Two loops try every pair. Two pointers on sorted data throw away one item per check, because that item can't be in any answer.

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.

🚢 converging134689L →← RπŸ’πŸ‡ fast & slow123456slow +1fast +2πŸ“š write / read130050writeread β†’πŸ”€ two arrays135246i β†’j β†’πŸ—‘οΈ three-way001?220s ←checkβ†’ 2s
The 5 shapes. Every problem on this sheet is one of them: where the pointers start and which way they move is the whole pattern.

🧭 1 · Spot it

The problem says…PatternPicture it asLC
sorted array + pair hitting a target, palindrome, container, rain waterConvergingπŸšΆβ€β™‚οΈβ†’ β†πŸšΆ two people walking toward each other from both ends167, 11, 42, 680, 977, 881
3 or 4 numbers summing to a target, closest sum, count tripletsk-SumπŸ“Œ pin one number, 2Sum the rest15, 16, 18, 259
linked list: cycle, middle, n-th from end, palindrome, reorderFast & SlowπŸ’πŸ‡ two runners on a track: the fast one laps the slow one141, 142, 876, 19, 234, 143, 160
in place, O(1) space: remove, move zeroes, dedupe Β· or two sorted arraysWrite pointerπŸ“š a scanner hands books over, a shelver keeps only the good ones283, 26, 80, 88, 392, 350, 844
sort 0/1/2, partition around a pivot in one passThree-way partitionπŸ—‘οΈπŸ—‘οΈπŸ—‘οΈ three bins: 0s left, 2s right, 1s stay in the middle75, 2161
Before coding, answer 3 things 1. Where do the pointers start? 2. What moves each one? 3. When do they stop? If you can say why the move never skips an answer, the code writes itself.
⚠️ Wrong tool when… you need the original indices of an unsorted array (sorting loses them β†’ hash map, LC 1) Β· the data can't be sorted and order matters (β†’ sliding window or a map) Β· moving a pointer doesn't reliably move you toward the answer.
πŸ’­ Your thoughts
🎀 Say this before coding "The array is sorted, so I'll put one pointer at each end. If the sum is too small, the left value can't be in any answer, so I move left. Each step throws one element away, so it's O(n) time and O(1) space."

🚢 2 · Converging: both ends, move inward

"Squeeze from both ends; drop the one that can't help."

🧩 The idea πŸšΆβ€β™‚οΈβ†’ β†πŸšΆ Two people walk toward each other along a sorted row. Each step, the one who can't be in any answer steps in (sum too small β†’ the left one, too big β†’ the right one). They meet after at most n steps, and no good pair was ever skipped.
πŸ”‘ Why it's safe In 167, if 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.
i \ j71115271191317118322226← row 2 out↑ column 15 outβ‘  2 + 15 = 17 < 18 β†’ i++β‘‘ 7 + 15 = 22 > 18 β†’ j--β‘’ 7 + 11 = 18 βœ“ (6 pairs, 3 checks)
Sorted means sums grow to the right and downward. A sum that is too small rules out its whole row (that nums[i] fails even with the biggest partner). Too big rules out its whole column.

167 Β· Two Sum II (the template) ⏱️ O(n) Β· πŸ“¦ O(1)

πŸ’‘ 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 [];
};
stepijsum ([2,7,11,15], target 18)move
1032 + 15 = 17 < 18i++
2137 + 15 = 22 > 18j--
3127 + 11 = 18 βœ…return [2, 3]

11 Β· Container With Most Water ⏱️ O(n) Β· πŸ“¦ O(1)

πŸ’‘ 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;
};

42 Β· Trapping Rain Water ⏱️ O(n) Β· πŸ“¦ O(1)

πŸ’‘ 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.

010210132121min(2, 3) βˆ’ 0 = 2
Height [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;
};

680 Β· Valid Palindrome II ⏱️ O(n) Β· πŸ“¦ O(1)

πŸ’‘ 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;
};

977 Β· Squares of a Sorted Array ⏱️ O(n) Β· πŸ“¦ O(n) output

πŸ’‘ 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;
};

881 Β· Boats to Save People ⏱️ O(n log n) Β· πŸ“¦ O(n) sorted copy

πŸ’‘ 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;
};
βž• Counting pairs Converging can also count. Sorted, 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.
πŸ’­ Your thoughts
🎀 Interview lens Probed: why moving a pointer never skips an answer; "what if it's not sorted?" (sort O(n log n) vs hash map O(n), which keeps indices). Escalation: 2Sum β†’ 3Sum β†’ 4Sum; closest sum; count pairs.

πŸ“Œ 3 Β· k-Sum: fix one, then 2Sum the rest

"3Sum = a for-loop around 2Sum."

🧩 The idea πŸ“Œ Pin one number with your thumb, then solve 2Sum on everything to its right with the squeeze. n pins Γ— an O(n) squeeze = O(nΒ²). Sorting first puts equal values side by side, so skipping duplicates is just "same as the one before? skip".

15 Β· 3Sum ⏱️ O(nΒ²) Β· πŸ“¦ O(n) sorted copy

πŸ’‘ 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;
};
VariantChange from 3Sum
16 3Sum ClosestNo dedupe needed; keep the sum with the smallest |sum βˆ’ target|, move like 167.
18 4SumTwo outer loops (fix two numbers), same squeeze inside, skip duplicates at both levels. O(nΒ³).
259 3Sum SmallerCount, don't list: sum < target β†’ count += right - left; left++.
923 3Sum With MultiplicityCount with duplicates: on a hit, count the run of equal values (or use a value-count map).

πŸ’πŸ‡ 4 Β· Fast & Slow: linked lists

"Slow moves 1, fast moves 2. If there's a loop, the hare laps the tortoise."

🧩 The idea πŸ’πŸ‡ Two runners. On a straight road the hare reaches the end (no cycle). On a circular track the hare laps the tortoise (cycle). Same two speeds give the middle: when the hare finishes, the tortoise is halfway.
πŸ”‘ Why they must meet Inside a loop, fast gains exactly 1 step on slow each move, so the gap shrinks 1 at a time and hits 0. No loop β†’ fast reaches null. Both take O(n) steps, O(1) space.

141 Β· Linked List Cycle ⏱️ O(n) Β· πŸ“¦ O(1)

πŸ’‘ 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;
};

142 Β· Linked List Cycle II (where does it start?) ⏱️ O(n) Β· πŸ“¦ O(1)

πŸ’‘ After they meet, send one pointer back to head; move both 1 step at a time; they meet at the cycle start.

123456loop startmeetstepslowfast011123235343455βœ“142: one back to head β†’ 1β†’2β†’3 and 5β†’6β†’3 meet at 3
List 1→…→6, then 6 points back to 3. Fast gains 1 step per move, so it catches slow inside the loop (at 5). For 142, restart one pointer at head; at speed 1 both reach the loop start together.

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;
};

876 Β· Middle of the Linked List ⏱️ O(n) Β· πŸ“¦ O(1)

πŸ’‘ 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
};

160 Β· Intersection of Two Linked Lists ⏱️ O(m + n) Β· πŸ“¦ O(1)

πŸ’‘ 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.

a1a2b1b2b3c2c3c1ABl1: a1 a2 c1 c2 c3 null b1 b2 b3 c1 βœ“l2: b1 b2 b3 c1 c2 c3 null a1 a2 c1 βœ“
Each pointer walks its own list, then the other one: 2 + 3 + 3 nodes either way, so they step onto c1 together. No join β†’ both reach 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;
};
🧩 The combo: middle β†’ reverse β†’ use 234 and 143 are the same three steps: find the middle (876), reverse the second half, then compare (234) or zip (143).

234 Β· Palindrome Linked List ⏱️ O(n) Β· πŸ“¦ O(1)

πŸ’‘ 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;
};

143 Β· Reorder List ⏱️ O(n) Β· πŸ“¦ O(1)

πŸ’‘ 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;
    }
};

19 Β· Remove Nth Node From End ⏱️ O(n) Β· πŸ“¦ O(1)

πŸ’‘ Your version: count the length, then stop on the node before the one to delete. len === n means delete the head.

🎀 Likely follow-up: "one pass?" Give 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;
};
🎀 Interview lens Probed: why they must meet, and deriving the 142 reset. Escalation: cycle β†’ its start β†’ 287 Find the Duplicate Number (the array is a linked list: i β†’ nums[i], reuse 142).

πŸ“š 5 Β· Write pointer: same direction, in place

"Read scans everything; write marks where the next keeper goes. Everything left of write is done."

🧩 The idea πŸ“š One person scans every book (read), another shelves only the keepers (write). The shelf behind write is always finished, and write never passes read, so nothing unread is overwritten.
πŸ”‘ Why it's safe nums[0..write) is the finished answer and write ≀ read always, so you never overwrite something you haven't read yet.
start010312after read 3130012β–² writeβ–² readend131200
🟩 left of write = finished (non-zeros, in order) Β· grey = zeros already passed Β· white = not read yet. write ≀ read, so nothing unread is ever overwritten.

283 Β· Move Zeroes ⏱️ O(n) Β· πŸ“¦ O(1)

πŸ’‘ 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++;
        }
    }
};

26 Β· Remove Duplicates from Sorted Array ⏱️ O(n) Β· πŸ“¦ O(1)

πŸ’‘ 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;
};

πŸ”€ Two arrays, one pointer each

πŸ”€ 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.

88 Β· Merge Sorted Array ⏱️ O(m + n) Β· πŸ“¦ O(1)

πŸ’‘ 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
};

392 Β· Is Subsequence ⏱️ O(n) Β· πŸ“¦ O(1)

πŸ’‘ 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;
};

350 Β· Intersection of Two Arrays II ⏱️ O(m log m + n log n) Β· πŸ“¦ O(m + n)

πŸ’‘ 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;
};

844 Β· Backspace String Compare ⏱️ O(m + n) Β· πŸ“¦ O(1)

πŸ’‘ 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;
};

πŸ—‘οΈ 6 Β· Three-way partition

"Three bins: 0s go left, 2s go right, 1s stay in the middle."

🧩 The idea πŸ—‘οΈπŸ—‘οΈπŸ—‘οΈ Throw 0s against the left wall and 2s against the right wall; 1s stay where they land. Something thrown in from the right hasn't been looked at yet, so look at it before moving on.
πŸ”‘ Picture [0, left) = 0s Β· [left, mid) = 1s Β· [mid, right] = not checked yet Β· (right, end] = 2s.
0011220s1s?2sβ–² leftβ–² midrightloop runs while mid ≀ right: one cell (?) left to check
Your code on [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.

75 Β· Sort Colors (Dutch flag) ⏱️ O(n) Β· πŸ“¦ O(1)

πŸ’‘ 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.

🧰 7 · Array tricks (in the tracker next to two pointers)

Not really two-pointer patterns, but asked in the same rounds. Know the one line.

189 Β· Rotate Array ⏱️ O(n) Β· πŸ“¦ O(1)

πŸ’‘ 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
};

31 Β· Next Permutation ⏱️ O(n) Β· πŸ“¦ O(1)

πŸ’‘ 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);
};

238 Β· Product of Array Except Self ⏱️ O(n) Β· πŸ“¦ O(n) suffix array

πŸ’‘ Left pass stores the product of everything to the left; right pass multiplies in everything to the right. No division.

⚠️ Your note says O(1) This code keeps a 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;
};
πŸ’­ Your thoughts

⭐ 8 · Must-know, one line each

If you can say the trick for each in one sentence, you're ready.

#ProblemPatternThe trick
167Two Sum II🚢 ConvergingToo big β†’ j--, too small β†’ i++.
1Two Sum (unsorted)πŸ—‚οΈ Hash mapNeed indices β†’ map value β†’ index; not two pointers.
11Container With Most Water🚢 ConvergingMove the shorter wall.
42Trapping Rain Water🚢 ConvergingMove the side with the smaller max; water = max βˆ’ height.
680Valid Palindrome II🚢 ConvergingOn the first mismatch, try skipping left or right once.
977Squares of a Sorted Array🚢 ConvergingBiggest square is at an end; fill from the back.
881Boats to Save People🚢 ConvergingHeaviest leaves; lightest joins if it fits.
153SumπŸ“Œ k-SumSort, fix i, squeeze the rest, skip duplicates.
163Sum ClosestπŸ“Œ k-SumSame squeeze, track the closest sum.
2593Sum SmallerπŸ“Œ k-Sumcount += right - left.
141 / 142Linked List Cycle I / IIπŸ’πŸ‡ Fast & SlowMeet β†’ cycle; reset one to head β†’ they meet at the start.
876Middle of the ListπŸ’πŸ‡ Fast & SlowFast at the end β†’ slow in the middle.
19Remove Nth From EndπŸ’πŸ‡ Fast & SlowStop on the node before; one pass = n-step head start.
160Intersection of Two ListsπŸ’πŸ‡ Fast & SlowAt the end, switch to the other head.
234 / 143Palindrome List / Reorder ListπŸ’πŸ‡ Fast & SlowMiddle β†’ reverse second half β†’ compare / zip.
287Find the Duplicate NumberπŸ’πŸ‡ Fast & SlowIndex β†’ value is a linked list with a cycle; run 142.
283Move ZeroesπŸ“š Write pointerSwap non-zeros to write.
26 / 80Remove Duplicates I / IIπŸ“š Write pointerCompare with nums[write - 1] / nums[write - 2].
88Merge Sorted ArrayπŸ”€ Two arraysFill from the back.
392Is SubsequenceπŸ”€ Two arraysMove s only on a match.
844Backspace String CompareπŸ”€ Two arraysWalk from the end, skip what # erases.
75Sort ColorsπŸ—‘οΈ Three-wayAfter swapping with right, don't move mid.

⚠️ 9 · Traps, and the input that shows each one

BugInput 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 start1β†’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
πŸ’­ Your thoughts

Complexity

PatternTimeSpace
Converging (already sorted)O(n)O(1) Β· add O(n log n) if you sort first
3Sum / 4SumO(nΒ²) / O(nΒ³)O(1) besides the sort
Fast & slowO(n)O(1)
Write pointer, three-wayO(n)O(1)
Two arraysO(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.

βœ… 10 Β· Self-check

Cover the right column (it's blurred; hover or tap to reveal). Say the answer out loud first.

PromptAnswer
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.

Write these cold (no peeking)

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.