The first 18 rows look like 18 unrelated tricks. They are not. Strip the titles away and 9 ideas cover all of them — a write pointer, a running best, a cancelling XOR, a hashmap holding the complement. Learn the 9; the 18 follow.
This is not a list of problems. It is 14 learning units, and every one runs the same four beats. Go in order and the problems stop feeling random.
18 PROBLEMS · EVERY ONE LINKS TO LEETCODE TO SOLVE
Click any problem to jump straight to it. Solved ones turn green. Press I from anywhere to come back here. The number is the sheet's row, not your position — the deck teaches in the order that builds, so you will meet them slightly out of numerical sequence.
Nobody is asked to “use a write pointer” in an interview. You are handed a paragraph and expected to hear which pattern it permits — and the phrases that give it away are astonishingly consistent.
the answer is shorter than the input, or is the input rearranged
TWO INDICES — r READS, w COMMITSO(n) time · O(1) spacethe noise is paired and the signal is not
XOR EVERYTHING, OR USE THE n(n+1)/2 IDENTITYO(n) time · O(1) spaceorder does not matter and duplicates must be skipped
SORT, FIX k−2 INDICES, CLOSE WITH TWO POINTERSO(nk−1) time · O(1) spacethe answer is a run, not a subset
KADANE IF MAXIMISING · PREFIX SUM + HASHMAP IF COUNTINGO(n) time · O(1) or O(n) spacea threshold that guarantees at most k−1 answers exist
BOYER–MOORE VOTING, THEN A VERIFY PASSO(n) time · O(1) spacenothing is searched — the answer is a formula from index to index
TRANSPOSE + REVERSE, OR FOUR SHRINKING BOUNDSO(n²) time · O(1) spaceBefore writing anything, read the input bound and it tells you the complexity you are allowed. Type a value for n and the row that survives lights up.
THE GOLD-EDGED ROWS ARE WHERE THIS TOPIC LIVES · PAIR-CHECKING DIES AT 10⁴ · EVERY OPTIMAL SOLUTION HERE IS ONE PASS OR ONE SORT
All 14 units, covering every Easy and Medium row on the sheet. The eight Hard rows are deck 2 — compositions of exactly these nine patterns, worth nothing until they are automatic.
An array is scanned by two indices, r and w, with w starting at 0 and only advancing when a value is kept. Can w ever overtake r?
Never. Each iteration advances r exactly once and w at most once, so w ≤ r is an invariant of the loop. That single inequality is the whole safety proof for writing in place: the slot w is about to overwrite has already been read by r, so no unread data can ever be destroyed. Every in-place compaction problem in this deck rests on it.
You are given constraints n ≤ 10⁵ and asked for a contiguous subarray. Which complexity should you be aiming for before you write a single line?
O(n) or O(n log n). At n = 10⁵ a quadratic solution is 10¹⁰ operations against a budget of roughly 10⁸ per second — two orders of magnitude over, every time. Reading the bound first tells you the shape of the answer before you have any idea what the answer is, and it is the single most reliable move in the whole topic. n ≤ 10³ is where the quadratic brute force becomes legal. This budget is not an arrays fact — Sorting (step 02) derives the same 10⁸-per-second figure, and every CONSTRAINTS slide in every deck reads off it.
This is meant to count how many times the maximum appears. It compiles, it runs, and on [3, 7, 7, 2] it returns 1 instead of 2. Which line is wrong?
int best = a[0], cnt = 0; for (int v : a) { if (v > best) { best = v; cnt = 1; } if (v == best) cnt++; }
It must be else if. When v > best fires, it already sets cnt = 1 — and then the very next line sees v == best (because best was just assigned v) and increments it to 2. The count for every fresh maximum is inflated by exactly one, so on [3,7,7,2] you get 7 counted as 3 rather than 2. No crash, no warning, a plausible number. Two independent ifs over the same variable you just mutated is the most reliable way to write a silently wrong single-pass loop.
Almost every “do it in place” array problem is the same machine: two indices walking one array at different speeds. One reads everything. The other only moves when something is worth keeping. The array behind the write index is the answer; the array ahead of the read index is untouched input; and the region between them is garbage nobody will look at again.
HOW DO YOU BUILD A SHORTER ARRAY INSIDE THE ARRAY YOU WERE GIVEN?
The lecture insists the comparison is a[r] != a[w-1] and not a[r] != a[r-1]. On a SORTED array both happen to work. Why does the lecture still refuse the second one?
a[w-1] is the last value you kept. “Is this a duplicate?” means “does this match what I last committed?” — which is a question about the answer being built, not about the input. On sorted input the two coincide, so the wrong version passes and you learn nothing. The moment the problem changes to “allow each value at most twice” you need a[w-2], and the a[r-1] form has no equivalent at all. Write the version that says what you mean and the variants become one-line edits.
Run the loop on [1, 1, 2, 2, 2, 3]. At the moment r reaches the last element (value 3), what is w, and what does the array look like?
w = 2, with the array reading [1, 2, 2, 2, 2, 3]. Only 1 and 2 have been committed, so w counts two keepers and slot 1 was overwritten with 2. Everything from index 2 onward is stale — the original values are partly still there and they are meaningless. This is exactly why the visualiser strikes that region out rather than dimming it: it is not “less important” data, it is not data. Step the MECHANISM slide in PREDICT mode and it asks you this every iteration.
One line is wrong. It compiles, and on [1,2,3] (no duplicates at all) it returns the right answer. Which line, and what input exposes it?
int w = 1; for (int r = 1; r < n; r++) if (a[r] != a[w - 1]) a[w] = a[r]; w++; // commit return w;
Missing braces. C++ binds only the first statement to the if, so a[w] = a[r] is conditional but w++ is not — w advances on duplicates too. On [1,2,3] every element is kept anyway, so the bug is invisible and the answer is right. Feed it [1,1,2] and it returns 3. This is the archetypal array bug: correct on the input you tested, wrong on the input that mattered, with no crash to tell you.
Same problem, one word changed: each value may now appear at most twice. What is the minimal edit?
Start w at 2 and compare against a[w-2]. That is the whole edit, and it generalises: allow each value at most k times is w = k and a[r] != a[w-k]. The reason it works is the reason drill 1 insisted on a[w-1]: because the comparison is against the answer, asking “is this value already in my last k keepers” is a single index change. LeetCode 80 is this exact problem, and people who wrote the a[r-1] version rewrite it from scratch.
Two indices walk the same array. r reads every element; w only ever advances when something is kept. Read w as “how many keepers have I committed” rather than “where am I” and two facts fall out for free: a[w-1] is the last keeper, and return w is the answer's length. w can never overtake r, which is the entire proof that overwriting in place is safe.
The statement hands you the algorithm's two hardest constraints for free: the array is already sorted (so duplicates are adjacent, and you never need a set) and you must return a length rather than an array (so the answer lives in a prefix of the input). Together those two phrases mean “two indices, one pass, O(1) space” before you have read the examples.
Keep a write index w meaning how many unique values I have committed so far. Read across with r. Since the array is sorted, a value is new exactly when it differs from the last one you kept — so compare against a[w−1], commit when they differ, and leave w alone when they match. The values behind w are the answer, the values ahead of r are untouched input, and everything in between is debris that no one will ever read.
// Two indices, one array. r reads everything; w commits only keepers. // The invariant w <= r is what makes writing in place safe: the slot w // overwrites was already read by r, so no unread data can be destroyed. int removeDuplicates(vector<int>& a) { if (a.empty()) return 0; int w = 1; // a[0] is a keeper by definition for (int r = 1; r < a.size(); r++) if (a[r] != a[w - 1]) // differs from the LAST KEPT value? a[w++] = a[r]; // commit it, then widen the answer return w; // w counted keepers, so it IS the length }
// Two indices, one array. r reads everything; w commits only keepers. // The invariant w <= r is what makes writing in place safe: the slot w // overwrites was already read by r, so no unread data can be destroyed. public int removeDuplicates(int[] a) { if (a.length == 0) return 0; int w = 1; // a[0] is always a keeper for (int r = 1; r < a.length; r++) if (a[r] != a[w - 1]) // new vs the last value KEPT a[w++] = a[r]; return w; // w is a COUNT, so it IS the length }
# Two indices, one list. r reads everything; w commits only keepers. # The invariant w <= r is what makes writing in place safe: the slot w # overwrites was already read by r, so no unread data can be destroyed. def remove_duplicates(a): if not a: return 0 w = 1 # a[0] is a keeper by definition for r in range(1, len(a)): if a[r] != a[w - 1]: # differs from the LAST KEPT value? a[w] = a[r] # commit it, then widen the answer w += 1 return w # w counted keepers, so it IS the length
Comparing a[r] != a[r-1] instead of a[r] != a[w-1] passes every test on this problem, because on sorted input the two are the same value. It is still the wrong line, and the cost is deferred: LeetCode 80 (“at most twice”) is a one-character edit from the a[w-1] version and a rewrite from the other one. The second trap is louder — forgetting braces around a[w]=a[r]; w++; makes w advance on duplicates too, which returns a plausible over-count and never crashes.
The walkthrough for #01 Remove Duplicates from Sorted Array. Watch it, then go straight back and write it yourself.
Two problems, one skeleton, and the difference between them is worth more than either. Rotation is index algebra — nothing is compared, the answer is a formula. Move Zeroes is the write pointer again, but swapping rather than copying, because a rearrangement must not lose the values it displaces. Both refuse the obvious temporary array, and both refuse it for the same reason: O(1) space is in the statement.
HOW DO YOU REARRANGE AN ARRAY IN PLACE WITHOUT LOSING WHAT YOU OVERWRITE?
Why does reversing the whole array first, then reversing the two pieces, produce a rotation? What does the first reversal actually accomplish?
The first reversal fixes the blocks, not the elements. Rotating right by k means the last k values must end up in front. Reversing everything does exactly that — the tail block is now at the front and the head block at the back — but each block reads backwards, because reversal reverses. The two inner reversals are therefore corrections, not tricks. Once you see it as “move the blocks, then unscramble each”, you can re-derive it at a whiteboard instead of remembering three lines.
rotate([1,2,3,4,5,6,7], k = 10). What happens without the k %= n line, and what is the correct output?
a.begin() + 10 on a 7-element vector is an iterator past the end, and passing it to reverse is undefined behaviour — which in practice often does not crash, it just scribbles. With k %= n, k becomes 3 and the answer is [5,6,7,1,2,3,4]. LeetCode 189's constraints explicitly permit k larger than the array, so this is not a theoretical edge case — it is in the tests.
This Move Zeroes variant is O(n), returns an array with all zeros at the end, and is still wrong. What breaks?
int w = 0, r = a.size() - 1; while (w < r) { if (a[w] == 0) swap(a[w], a[r--]); // pull a value from the back else w++; }
It scrambles the order. Swapping from the back moves whatever happens to be at the end into an early slot, so [0,1,0,3,12] can yield [12,1,3,0,0] — every zero correctly at the end, and the non-zeros in the wrong sequence. LeetCode 283 says “maintaining the relative order”, and that clause is the entire reason the forward write-pointer version exists. An O(n) in-place answer that ignores a stability requirement is a wrong answer that looks like a clever one.
Move Zeroes uses swap(a[w++], a[r]) where Remove Duplicates used a[w++] = a[r]. Why can Move Zeroes not simply copy?
Because the two problems return different things. Remove Duplicates returns a length — the tail is explicitly garbage, so overwriting is free. Move Zeroes returns the whole array, so every element must still be present at the end; the zero sitting at slot w has to go somewhere, and the swap posts it to index r, which the reader has already passed. Copy when the tail is disposable, swap when it is not. That one question tells you which form to write.
The naive rotation needs a copy. This does not, and the reason is worth watching rather than memorising: reversing the whole array puts the right blocks in the right places and the wrong order inside each. The two inner reversals are corrections, not tricks. Every element moves exactly twice.
Identical skeleton, one change: the keeper test is a property of the value rather than a comparison with its neighbour, and the write is a swap. That matters — the zero displaced from slot w is not destroyed, it is posted forward to index r, which the reader has already passed. Nothing can be re-read, so one pass is enough.
“Rotate in place” plus an explicit follow-up asking for O(1) space. Nothing is searched and nothing is compared, so this is not an algorithm problem at all — it is index algebra. The other tell is in the constraints: k is allowed to be far larger than the array, which is the statement quietly telling you to write k %= n.
Rotating right by k means the last k values must end up in front. Reverse the whole array and exactly that happens — the two blocks swap sides — but each block is now internally backwards, because reversal reverses. So reverse each block again to correct it. Three reversals, every element moved exactly twice, no second array anywhere.
// Reversal rotation. The first reversal puts the right BLOCKS in the // right places; the other two fix the order inside each block. void rotate(vector<int>& a, int k) { int n = a.size(); k %= n; // k may exceed n - this is in the tests reverse(a.begin(), a.end()); // 1. blocks swap sides, both backwards reverse(a.begin(), a.begin() + k); // 2. fix the block now at the front reverse(a.begin() + k, a.end()); // 3. fix the block now at the back }
// Reversal rotation. The first reversal puts the right BLOCKS in the // right places; the other two fix the order inside each block. public void rotate(int[] a, int k) { int n = a.length; k %= n; // k may exceed n - this is in the tests reverse(a, 0, n - 1); // 1. the whole array reverse(a, 0, k - 1); // 2. the first k reverse(a, k, n - 1); // 3. everything after } private void reverse(int[] a, int l, int r) { while (l < r) { int t = a[l]; a[l++] = a[r]; a[r--] = t; } }
# Reversal rotation. The first reversal puts the right BLOCKS in the # right places; the other two fix the order inside each block. def rotate(a, k): n = len(a) k %= n # k may exceed n - this is in the tests def rev(lo, hi): while lo < hi: a[lo], a[hi] = a[hi], a[lo] lo, hi = lo + 1, hi - 1 rev(0, n - 1) # 1. blocks swap sides, both backwards rev(0, k - 1) # 2. fix the block now at the front rev(k, n - 1) # 3. fix the block now at the back
Omitting k %= n. LeetCode 189's constraints explicitly allow k greater than the array length, and a.begin() + k past the end is undefined behaviour — which usually does not crash. It corrupts quietly, passes your hand-written examples, and fails the judge's larger tests with no indication why. The runner-up trap is reaching for the “cyclic replacement” solution: it is also O(1) space, and it needs a GCD argument to know how many cycles to run. Three reversals need no such argument, which is exactly why they are the version worth memorising.
The walkthrough for #02 Rotate Array. Watch it, then go straight back and write it yourself.
Two phrases, and the second one is the whole problem. “Move all 0's to the end” sounds like a partition; “while maintaining the relative order of the non-zero elements” forbids the obvious back-to-front partition and forces a forward write pointer. Add “in place, without making a copy” and the shape is fully determined before you see an example.
It is unit 01's write pointer with one change. w marks the oldest slot still owed a non-zero value; r reads everything. When r finds a non-zero, swap it into w and advance both. The swap matters — this is a rearrangement, not a compaction, so the zero displaced from w must survive. It travels to index r, which the reader has already passed, so it can never be examined again.
// The write pointer again, but SWAPPING because every element must // survive: the zero displaced from w travels to r, which is already read. void moveZeroes(vector<int>& a) { int w = 0; // oldest slot still owed a non-zero for (int r = 0; r < a.size(); r++) if (a[r] != 0) swap(a[w++], a[r]); // order kept: written as they were read }
// The write pointer again, but SWAPPING because every element must // survive: the zero displaced from w travels to r, which is already read. public void moveZeroes(int[] a) { int w = 0; // oldest slot still owed a non-zero for (int r = 0; r < a.length; r++) if (a[r] != 0) { int t = a[w]; a[w] = a[r]; a[r] = t; // swap, never copy w++; } }
# The write pointer again, but SWAPPING because every element must # survive: the zero displaced from w travels to r, which is already read. def move_zeroes(a): w = 0 # oldest slot still owed a non-zero for r in range(len(a)): if a[r] != 0: a[w], a[r] = a[r], a[w] # order kept: written as they were read w += 1
The tempting O(n) alternative — swap zeros with values pulled from the back — moves fewer elements and destroys the relative order the problem demands. It produces an array with every zero correctly at the end, so it looks right in the debugger and fails the judge. The word doing the work is maintaining, and it is easy to read past. When a rearrangement problem mentions relative order, the forward write pointer is the only shape that preserves it for free.
The walkthrough for #03 Move Zeroes. Watch it, then go straight back and write it yourself.
Three sheet rows and one idea underneath two of them: when the noise is paired and the signal is not, something cancels. XOR cancels equal values to zero, and the arithmetic identity n(n+1)/2 cancels a full range against a punctured one. Max Consecutive Ones rides along because it is the same one-pass shape at its very simplest — a running counter and a best-so-far, which is the seed Kadane grows from in unit 07.
HOW DO YOU FIND THE ONE ODD ELEMENT WITHOUT A HASH MAP AND WITHOUT SORTING?
Why does XOR find the single element regardless of how the array is ordered, when a sort-and-scan would need the order fixed first?
Commutativity and associativity. a^b^a = a^a^b = 0^b = b — you may rearrange the folds freely, so a pair cancels whether its two members are adjacent or at opposite ends. That is precisely the property a sort would have had to manufacture, at a cost of O(n log n). This is the general lesson worth taking: when an operation is commutative, order is not information, and any work you do to impose order is wasted.
Missing Number on [3, 0, 1] using the sum identity. What is computed, and what is the risk that the XOR version does not have?
n(n+1)/2 = 6, actual sum = 4, so the answer is 2. The risk is arithmetic: at n = 10⁵ the expected sum is about 5×10⁹, which is past INT_MAX. The XOR version never grows — every intermediate value stays bounded by the largest element — so it is overflow-proof by construction. Both are O(n) and O(1); the XOR one is simply harder to get wrong, which is a real reason to prefer it and a good thing to say out loud in an interview.
Max Consecutive Ones. This returns 3 on [1,1,0,1,1,1], which happens to be right, and returns 4 on [1,1,1,1], which is not. Which line?
int run = 0, best = 0; for (int v : a) { if (v == 1) run++; else run = 0; best = max(best, run + 1); }
The + 1 inflates every answer by one. On [1,1,1,1] it returns 5 for an array of length 4 — and the reason it looked right on the first example is pure coincidence. This is the shape of bug this deck keeps returning to: an off-by-one that produces a plausible number rather than a crash, verified against one example that happened to agree. Also note the reset really is to 0 and not 1: a zero breaks the run entirely, it does not start a new one.
Watch the columns, not the values. XOR is parity per bit position: a column with an even number of 1s zeroes out. Since every value except one appears twice, every column's contribution cancels except the loner's. It is commutative and associative, which is exactly why the array never needs sorting and why the answer is order-independent.
“Contains n distinct numbers in the range [0, n]” — a known, complete range with exactly one hole. That phrasing is the signal for a cancellation identity: either XOR everything against every index, or subtract the actual sum from the closed-form expected sum. The follow-up asking for O(1) space rules out the seen-array that everyone reaches for first.
You know exactly what the array should have contained: 0 through n. So compare the full range against what is actually there and whatever fails to cancel is the missing value. XOR does this without ever growing — fold in every index and every element, and each present value appears exactly twice and vanishes. The sum identity n(n+1)/2 does the same job with arithmetic, at the price of an intermediate that can overflow.
// XOR the full range against what is actually present. Every value // that IS there gets folded twice and cancels; the hole is folded once. // Preferred over n(n+1)/2 because no intermediate can ever overflow. int missingNumber(vector<int>& a) { int n = a.size(), x = 0; for (int i = 0; i < n; i++) x ^= i ^ a[i]; // index and value, both folded return x ^ n; // index n has no element to pair with }
// XOR the full range against what is actually present. Every value // that IS there gets folded twice and cancels; the hole is folded once. // Preferred over n(n+1)/2 because no intermediate can ever overflow. public int missingNumber(int[] a) { int n = a.length, x = 0; for (int i = 0; i < n; i++) x ^= i ^ a[i]; // index against value return x ^ n; // and the index n itself }
# XOR the full range against what is actually present. Every value # that IS there gets folded twice and cancels; the hole is folded once. # Python ints never overflow, so the sum version is safe here -- but the # XOR version is the one that ports to C++ unchanged. def missing_number(a): n, x = len(a), 0 for i, v in enumerate(a): x ^= i ^ v # index and value, both folded return x ^ n # index n has no element to pair with
The sum version, n*(n+1)/2 - sum(a), is correct mathematics and an overflow waiting to happen: at n = 10⁵ the expected sum is about 5×10⁹, comfortably past INT_MAX. In C++ it wraps and returns a plausible wrong number with no warning; in Python it is fine, which is exactly how the habit survives long enough to fail in an interview. The XOR version is bounded by the largest element by construction — same complexity, no arithmetic hazard, and it generalises directly to Single Number.
The walkthrough for #04 Missing Number. Watch it, then go straight back and write it yourself.
The cheapest problem in the deck, and worth doing attentively because it is the seed of Kadane. “Maximum number of consecutive 1's” is a running quantity plus a best-so-far — the exact shape unit 07 generalises to arbitrary sums. If you can state why the reset is to 0 and not 1, you already understand the harder version.
Carry a run length. Every 1 extends it by one; every 0 ends it, so it resets to zero. Record the best after every element rather than only at the zeros — otherwise a run that reaches the end of the array is never recorded at all, which is the classic off-by-one here.
// The seed of Kadane: a running quantity and a best-so-far. // best is updated on EVERY element, so a streak that runs to the end // of the array is still recorded. int findMaxConsecutiveOnes(vector<int>& a) { int run = 0, best = 0; for (int v : a) { run = (v == 1) ? run + 1 : 0; // a 0 ends the streak entirely best = max(best, run); // record here, not at the break } return best; }
// The seed of Kadane: a running quantity and a best-so-far. // best is updated on EVERY element, so a streak that runs to the end // of the array is still recorded. public int findMaxConsecutiveOnes(int[] a) { int run = 0, best = 0; for (int v : a) { run = (v == 1) ? run + 1 : 0; // extend, or reset to nothing best = Math.max(best, run); // record BEFORE moving on } return best; }
# The seed of Kadane: a running quantity and a best-so-far. # best is updated on EVERY element, so a streak that runs to the end # of the array is still recorded. def find_max_consecutive_ones(a): run = best = 0 for v in a: run = run + 1 if v == 1 else 0 # a 0 ends the streak entirely best = max(best, run) # record here, not at the break return best
Updating best only when a 0 is met loses any streak that reaches the end of the array — [1,1,1] returns 0. It is invisible on every example that happens to end in a zero, which is most hand-written ones. The mirror-image mistake is resetting run = 1 instead of 0 on a zero: a zero breaks the run, it does not start a new one, and that version over-counts by one on every streak.
The walkthrough for #05 Max Consecutive Ones. Watch it, then go straight back and write it yourself.
“Every element appears twice except for one” plus “linear runtime and constant extra space”. The pairing is stated outright and the space bound rules out both the hash set and the sort. Once a problem tells you the noise is even and the signal is odd, XOR is not a trick you recall — it is the definition of what you were asked for.
XOR is parity, computed independently in every bit position. Folding the whole array into one accumulator, each bit column counts how many 1s appeared there, modulo two. Every duplicated value contributes to each column an even number of times and therefore contributes nothing. What remains, column by column, is exactly the bits of the single element.
// XOR is parity per bit column. A value appearing twice contributes // an even number of 1s to every column, so it contributes nothing at all. // Order is irrelevant: XOR is commutative AND associative. int singleNumber(vector<int>& a) { int x = 0; // 0 is XOR's identity for (int v : a) x ^= v; // equal pairs cancel to 0 return x; // only the loner survives }
// XOR is parity per bit column. A value appearing twice contributes // an even number of 1s to every column, so it contributes nothing at all. // Order is irrelevant: XOR is commutative AND associative. public int singleNumber(int[] a) { int x = 0; // 0 is XOR's identity for (int v : a) x ^= v; return x; }
# XOR is parity per bit column. A value appearing twice contributes # an even number of 1s to every column, so it contributes nothing at all. # Order is irrelevant: XOR is commutative AND associative. from functools import reduce from operator import xor def single_number(a): return reduce(xor, a, 0) # 0 is XOR's identity
This one rarely goes wrong; the trap is where the pattern stops working. XOR needs the noise to be paired. If every other element appears three times (LeetCode 137), pairs never form and a plain fold returns garbage — you need per-bit counting modulo 3 instead. If there are two single elements (LeetCode 260), the fold gives you their XOR and you must split the array on any set bit of it. Knowing the boundary of a pattern is worth more than knowing the pattern.
The walkthrough for #06 Single Number. Watch it, then go straight back and write it yourself.
The single most important reframe in the whole topic. “Do two values sum to t?” is a question about pairs, and there are n²/2 of them. “Have I already seen t − a[i]?” is a question about one value, and a hash map answers it in O(1). Nothing about the array changed; the question changed, and the complexity followed. Almost every O(n²) → O(n) improvement in this deck is that same move.
HOW DO YOU FIND A PAIR WITHOUT EVER LOOKING AT A PAIR?
The lecture stresses storing a[i] after checking for its complement. What goes wrong if you store first?
The element pairs with itself. With a = [3, 5] and t = 6, storing 3 before checking means the lookup for 6 − 3 = 3 succeeds immediately and you return [0, 0] — a single element used twice, which the problem forbids. Checking first guarantees the map only ever contains strictly earlier indices, so any hit is a genuine pair. The invariant is “the map holds the past, never the present”, and one line's ordering enforces it.
On [2, 7, 11, 15, 3, 6] with target 21, at which index does the algorithm return, and what is in the map at that moment?
i = 5. Walking through: 2 needs 19 (absent), 7 needs 14 (absent), 11 needs 10 (absent), 15 needs 6 (absent — 6 has not been reached yet), 3 needs 18 (absent), and finally 6 needs 15, which was stored at index 3. The answer is [3, 5]. Notice that the pair was half discovered at i = 3 and completed at i = 5 — the map is what carries that half-knowledge forward, which is why one pass suffices. This is the exact trace the MECHANISM slide walks.
The array is already sorted and the problem asks for the two values, not their indices. What changes, and why is it now better?
Two pointers, O(1) space. This is LeetCode 167, and the difference is the whole reason both problems exist. The hash map buys O(1) lookup at a cost of O(n) memory; a sorted array already gives you a decidable move at every step, so you pay nothing. And the reason Two Sum itself cannot do this is the word indices — sorting would destroy them. Read the output type before choosing the technique: “return indices” forbids sorting, “return values” permits it, and that single word decides your space complexity.
“Do two values sum to t?” is a question about pairs, and there are n²/2 of them. “Have I already seen t − a[i]?” is a question about one value, and a hash map answers it in O(1). That reframe is the entire algorithm — everything else is bookkeeping. Watch the order of the two operations: check, then store. Storing first lets an element pair with itself.
“Return the indices of the two numbers” — and that one word is doing enormous work. It forbids sorting, which is what would otherwise buy you two pointers and O(1) space. With sorting off the table and the array unordered, the only way to beat O(n²) is to buy O(1) lookup with memory, which means a hash map.
Stop asking about pairs. For the current element there is exactly one value that could complete it: t − a[i]. So the real question is “have I already walked past that value?” — a question about a single number, which a hash map answers instantly. Walk once, and for each element check for its complement before storing itself, so the map only ever holds strictly earlier indices.
// The reframe: "do two values sum to t" is a question about PAIRS. // "Have I seen t - a[i]" is a question about ONE value, and a map answers // that in O(1). Check BEFORE storing, or a[i] pairs with itself. vector<int> twoSum(vector<int>& a, int t) { unordered_map<int, int> seen; // value -> index it was seen at for (int i = 0; i < a.size(); i++) { int need = t - a[i]; // the ONE partner that works if (seen.count(need)) // has it already gone past? return {seen[need], i}; seen[a[i]] = i; // store AFTER checking } return {}; }
// The reframe: "do two values sum to t" is a question about PAIRS. // "Have I seen t - a[i]" is a question about ONE value, and a map answers // that in O(1). Check BEFORE storing, or a[i] pairs with itself. public int[] twoSum(int[] a, int t) { Map<Integer, Integer> seen = new HashMap<>(); // value -> its index for (int i = 0; i < a.length; i++) { Integer j = seen.get(t - a[i]); // CHECK first if (j != null) return new int[]{j, i}; seen.put(a[i], i); // then store } return new int[]{-1, -1}; }
# The reframe: "do two values sum to t" is a question about PAIRS. # "Have I seen t - a[i]" is a question about ONE value, and a dict answers # that in O(1). Check BEFORE storing, or a[i] pairs with itself. def two_sum(a, t): seen = {} # value -> index it was seen at for i, v in enumerate(a): need = t - v # the ONE partner that works if need in seen: # has it already gone past? return [seen[need], i] seen[v] = i # store AFTER checking return []
Storing before checking. With a = [3, 5] and t = 6, storing 3 first means the lookup for 3 succeeds against the element you are standing on, and you return [0, 0] — one element used twice. The check-then-store order makes it impossible by construction, because the map then holds only strictly earlier indices. The other trap is reflexive sorting: it is the right instinct on almost every other problem in this deck, and here it destroys the very thing you were asked to return.
The walkthrough for #07 Two Sum. Watch it, then go straight back and write it yourself.
Sorting three distinct values does not need a sort. Keep three regions and one unclassified gap, and each element is examined once. The invariant is a claim about index ranges rather than elements: everything before low is a 0, everything before mid is a 1, everything after high is a 2, and the gap between mid and high has never been looked at. Every line of the algorithm follows from protecting that sentence.
HOW DO YOU SORT THREE VALUES IN ONE PASS, WITHOUT COUNTING THEM FIRST?
On a[mid] == 2 you swap with high and decrement it — but you do not advance mid. Why not?
The incoming value is unknown. Everything after high is known to be a 2, but a[high] itself sits inside the unclassified region — nobody has looked at it. Advancing mid would step straight over an element you have never examined, and if it is a 0 it is now stranded in the middle region forever. Contrast the 0 case: the value coming back from low was already classified as a 1, so it needs no second look and mid may safely advance. The rule is not about 0s and 2s; it is about whether the incoming value has been seen.
Trace [2, 0, 1] with low = mid = 0, high = 2. What are the first two actions?
Swap to the back, then reclassify the same slot. The 2 goes to index 2 and the 1 comes forward, giving [1,0,2] with high = 1. Because mid stayed at 0, the very next step examines that freshly-arrived 1 — which is exactly the point of not advancing. Had mid moved, the 1 would sit unclassified in front of low, and the final array would be wrong while still looking plausibly grouped. Step the MECHANISM slide in PREDICT mode; it asks you which cell each swap targets.
This uses mid < high as the loop condition. On which kind of input does it silently fail?
while (mid < high) { if (a[mid] == 0) swap(a[low++], a[mid++]); else if (a[mid] == 1) mid++; else swap(a[mid], a[high--]); }
The last element is skipped. When mid and high meet there is still exactly one unclassified slot, and mid < high exits before examining it. On [1, 0] you get [1, 0] back — untouched and wrong. It passes plenty of tests, because the final element is often already in the right place by luck. The unclassified region is [mid, high] inclusive at both ends, so the loop must be <=. Write the invariant down and the loop condition is not a guess.
The invariant is a claim about ranges: everything before low is 0, everything before mid is 1, everything after high is 2, and the gap between mid and high has never been looked at. That last clause is why swapping a 2 to the back leaves mid where it is: the value that came back is unclassified, so advancing past it would skip an element unseen.
“Sort an array of 0s, 1s and 2s” with the follow-up “one pass, constant space, without using the library sort”. A tiny fixed value set plus a one-pass constraint is the Dutch-flag signature. Counting sort also solves it in O(n) but needs two passes — the one-pass clause exists specifically to rule that out.
Maintain three regions and one gap. Everything before low is 0, everything before mid is 1, everything after high is 2, and the stretch from mid to high has never been examined. Read a[mid] and place it: a 0 swaps to the front and both low and mid advance; a 1 is already home so only mid advances; a 2 swaps to the back and high shrinks — but mid holds still, because the value that just arrived from high has never been looked at.
// Three regions and one unclassified gap [mid, high]. The invariant // is a claim about RANGES, and every line below protects it. void sortColors(vector<int>& a) { int low = 0, mid = 0, high = a.size() - 1; while (mid <= high) { // <= : [mid, high] is INCLUSIVE if (a[mid] == 0) swap(a[low++], a[mid++]); // both advance else if (a[mid] == 1) mid++; // already home else swap(a[mid], a[high--]); // mid does NOT move } }
// Three regions and one unclassified gap [mid, high]. The invariant // is a claim about RANGES, and every line below protects it. public void sortColors(int[] a) { int low = 0, mid = 0, high = a.length - 1; while (mid <= high) { // <= : [mid, high] is INCLUSIVE if (a[mid] == 0) { int t = a[low]; a[low++] = a[mid]; a[mid++] = t; // both advance } else if (a[mid] == 1) { mid++; // already where it belongs } else { int t = a[mid]; a[mid] = a[high]; a[high--] = t; // mid STAYS } } }
# Three regions and one unclassified gap [mid, high]. The invariant # is a claim about RANGES, and every line below protects it. def sort_colors(a): low, mid, high = 0, 0, len(a) - 1 while mid <= high: # <= : [mid, high] is INCLUSIVE if a[mid] == 0: a[low], a[mid] = a[mid], a[low] # both advance low, mid = low + 1, mid + 1 elif a[mid] == 1: mid += 1 # already home else: a[mid], a[high] = a[high], a[mid] # mid does NOT move high -= 1
Advancing mid after swapping a 2 to the back. The value pulled in from high is unclassified — nobody has looked at it — so stepping over it can strand a 0 in the middle region permanently. Contrast the 0 case, where the value returned from low is known to be a 1 and needs no second look. The second trap is writing mid < high: the unclassified range is inclusive at both ends, so the final element is never examined and [1,0] comes back untouched.
The walkthrough for #08 Sort Colors. Watch it, then go straight back and write it yourself.
A genuinely surprising algorithm: it finds the majority element in one pass with a single counter and no memory of what it has seen. The idea is annihilation — pair up two unequal elements and discard both. If one value holds more than half the array it cannot be fully cancelled, so it must be what survives. But read that carefully: it is the only possible survivor, not proof that a majority exists. That gap is where the second pass lives, and it is not optional.
HOW DO YOU FIND A VALUE OCCURRING MORE THAN n/2 TIMES USING ONE COUNTER?
Run the voting loop on [1, 2, 3], which has no majority element. What does it produce, and what does that tell you?
It proposes 3, with a count of 1 and total confidence. Trace it: 1 is adopted, 2 cancels it to zero, 3 is adopted because the count is zero. The algorithm has no way to know it failed. This is the single most important sentence about Boyer–Moore: it identifies the only value that COULD be a majority, and says nothing about whether one exists. LeetCode 169 guarantees a majority so the missing verify pass never bites — and then the same habit walks straight into 229, where it produces wrong answers.
On [2, 2, 1, 3, 2], what are the candidate and count after the fourth element (value 3)?
Candidate 2, count 0. Step through: 2 adopted (count 1), 2 agrees (count 2), 1 disagrees (count 1), 3 disagrees (count 0). The candidate variable still reads 2, but with a count of zero it carries no information at all — the prefix [2,2,1,3] has cancelled exactly, two against two. The fifth element then re-adopts 2, and it happens to be correct. Whenever the count is 0, ignore the candidate: it is stale, not a partial answer.
This drops the verification because “the problem guarantees a majority exists”. On LeetCode 169 that reasoning holds. Where does the habit actually cost you?
int cand = 0, cnt = 0; for (int v : a) { if (cnt == 0) cand = v; cnt += (v == cand) ? 1 : -1; } return cand; // no verify
On 169 alone this is defensible. The cost is the habit: its twin, LeetCode 229, asks for values appearing more than ⌊n/3⌋ times and makes no guarantee that any exist — so the two surviving candidates must both be counted, and frequently neither qualifies. That problem is deck 2's unit 02. Write the verify pass even when it is provably redundant, because what you are really learning is that voting produces candidates, and that is the part that transfers.
Unequal elements annihilate in pairs. A value holding more than half the array cannot be fully cancelled, so it is the only possible survivor — and the emphasis belongs on possible. Run this on [1,2,3] and it proposes 3 with complete confidence. The verification pass is not a safety check bolted on afterwards; it is the half of the algorithm that turns a candidate into an answer.
“More than ⌊n/2⌋ times” is a threshold, and a threshold is what makes cancellation work: at most one such value can exist, so pairing off unequal elements cannot destroy it. Pair that with a follow-up asking for O(1) space — which rules out the frequency map everybody writes first — and Boyer–Moore is the only remaining shape.
Imagine deleting two unequal elements at a time. Each deletion removes at most one copy of the majority value and at least one non-majority value, so a value holding strictly more than half can never be exhausted first. One counter simulates this: hold a candidate, increment when an element agrees, decrement when it disagrees, and adopt a new candidate whenever the counter reaches zero — because a zero means the prefix cancelled exactly and the problem restarts from there.
// Unequal elements annihilate in pairs. A value holding more than half // cannot be fully cancelled, so it is the ONLY POSSIBLE survivor -- which // is not the same as proof that it IS the majority. Hence pass two. int majorityElement(vector<int>& a) { int cand = 0, cnt = 0; for (int v : a) { if (cnt == 0) cand = v; // prefix cancelled: restart here cnt += (v == cand) ? 1 : -1; // agree, or annihilate } int c = 0; for (int v : a) c += (v == cand); // VERIFY -- this IS the algorithm return c > a.size() / 2 ? cand : -1; }
// Unequal elements annihilate in pairs. A value holding more than half // cannot be fully cancelled, so it is the ONLY POSSIBLE survivor -- which // is not the same as proof that it IS the majority. Hence pass two. public int majorityElement(int[] a) { int cand = 0, cnt = 0; for (int v : a) { if (cnt == 0) cand = v; // prefix cancelled: restart here cnt += (v == cand) ? 1 : -1; // agree, or annihilate } int c = 0; for (int v : a) if (v == cand) c++; // VERIFY -- this IS the algorithm return c > a.length / 2 ? cand : -1; }
# Unequal elements annihilate in pairs. A value holding more than half # cannot be fully cancelled, so it is the ONLY POSSIBLE survivor -- which # is not the same as proof that it IS the majority. Hence pass two. def majority_element(a): cand, cnt = None, 0 for v in a: if cnt == 0: cand = v # prefix cancelled: restart here cnt += 1 if v == cand else -1 # agree, or annihilate return cand if a.count(cand) > len(a) // 2 else -1 # VERIFY
Skipping the verification pass. LeetCode 169 guarantees a majority exists, so the shortened version is accepted and the habit forms. Then [1,2,3] confidently returns 3, and its twin — Majority Element II at ⌊n/3⌋, which makes no such guarantee — starts producing wrong answers for a reason that is no longer visible. The second trap is reading the candidate while the count is zero: at that moment the variable is stale, not a partial answer.
The walkthrough for #09 Majority Element. Watch it, then go straight back and write it yourself.
Kadane is not a formula, it is one question asked once per element: is the run I am carrying still worth extending, or is this element better off starting fresh? Keep two quantities apart — cur, the best run ending exactly here, and best, the best run seen anywhere — and the algorithm writes itself. Best Time to Buy and Sell Stock is the same loop with the running quantity renamed, which is why it sits in this unit.
HOW DO YOU FIND THE BEST CONTIGUOUS RUN WITHOUT TRYING EVERY RUN?
Why must best be initialised to a[0] rather than to 0?
All-negative input. On [-3, -1, -2] the correct answer is −1, the least-bad single element. Initialising best = 0 returns 0 — the sum of the empty subarray, which the problem does not allow. It is invisible on every example containing a positive number, which is most hand-written ones, and LeetCode 53's test suite very much includes the negative case. Initialise from the data, not from a convenient constant.
Run Kadane on [-2, 1, -3, 4, -1, 2, 1, -5, 4]. At i = 3 (value 4), what happens to cur, and why?
It restarts at 4. After index 2 the carried cur is −2 (from 1 + (−3)). Extending gives −2 + 4 = 2; starting over gives 4. Four wins, so the whole prefix is discarded. This is the moment the algorithm's one decision is visible, and it is also where the final answer's subarray begins — the run [4, −1, 2, 1] summing to 6. Step the MECHANISM slide in PREDICT mode; it asks you this at every element.
This tracks only one variable instead of two. It returns 6 on the standard example, which is correct. Where does it fail?
int cur = a[0]; for (int i = 1; i < n; i++) cur = max(a[i], cur + a[i]); return cur; // no best
cur answers “best run ending here”, and returning it after the loop answers “best run ending at the last index”. On [5, -10, 3] it returns 3 when the answer is 5. It happens to be right on the standard example only because that example's best run reaches near the end. Two variables answer two different questions, and the second one — record the maximum as you go — is the half people drop.
Best Time to Buy and Sell Stock is this same loop. What replaces cur, and what replaces the extend-or-restart test?
The running quantity changes and the skeleton does not. Track the cheapest price seen; the profit available today is price[i] − cheapest, and you record the best of those exactly as Kadane records best. The reset — “today is the new cheapest” — is the same drop-a-prefix-that-hurts move. Measured as a code diff it changes four lines, which is why the next slide renders it as a diff rather than as a fresh solution: seeing those two lines swap is the whole lesson.
Kadane is not a formula to memorise, it is a single question repeated: is the run I am carrying still worth extending, or is this element better off starting fresh? Everything else follows. Watch the two quantities stay separate — cur is the best run ending here, best is the best run anywhere so far, and collapsing them into one variable is the most common way to break it.
“Contiguous subarray” plus “largest sum”. Contiguous means position matters and you may not sort or reorder anything; largest means you are optimising, not counting. That pair is Kadane's exact signature. If the word had been “subsequence” this would be a different problem entirely, and if it had been “count” it would be prefix sums.
Walk once and carry one number: the best sum of a subarray that ends exactly at the current element. At each step there are only two candidates for that number — extend the previous run by this element, or start a new run here — so take the better. Separately record the largest value that quantity has ever taken. The insight is that a prefix with a negative sum can only ever hurt whatever follows, so the moment carrying it costs more than dropping it, you drop it.
// One decision per element: extend the run, or start over here. // cur = best run ENDING AT i. best = best run ANYWHERE so far. // Collapsing those two into one variable is how this usually breaks. int maxSubArray(vector<int>& a) { int cur = a[0], best = a[0]; // NOT 0 - handles all-negative input for (int i = 1; i < a.size(); i++) { cur = max(a[i], cur + a[i]); // a prefix that hurts is dropped best = max(best, cur); // record on EVERY element } return best; }
// One decision per element: extend the run, or start over here. // cur = best run ENDING AT i. best = best run ANYWHERE so far. // Collapsing those two into one variable is how this usually breaks. public int maxSubArray(int[] a) { int cur = a[0], best = a[0]; // NOT 0 - handles all-negative for (int i = 1; i < a.length; i++) { cur = Math.max(a[i], cur + a[i]); // restart, or extend best = Math.max(best, cur); } return best; }
# One decision per element: extend the run, or start over here. # cur = best run ENDING AT i. best = best run ANYWHERE so far. # Collapsing those two into one variable is how this usually breaks. def max_sub_array(a): cur = best = a[0] # NOT 0 - handles all-negative input for v in a[1:]: cur = max(v, cur + v) # a prefix that hurts is dropped best = max(best, cur) # record on EVERY element return best
Initialising best = 0 returns 0 on an all-negative array, where the correct answer is the least-bad single element. It is invisible on every example containing a positive, and LeetCode 53 tests it. The second trap is returning cur instead of best: cur answers “best run ending at the last index”, so on [5, -10, 3] it returns 3 rather than 5. Both produce a plausible number and neither crashes.
The walkthrough for #10 Maximum Subarray. Watch it, then go straight back and write it yourself.
“Buy on one day, sell on a later day” is an ordering constraint, which is what makes this a one-pass problem rather than a max-minus-min. You cannot simply take the largest price minus the smallest, because the smallest might come afterwards. What you need at each day is the cheapest price so far — a running quantity, exactly like Kadane's.
This is the previous problem with the running quantity renamed. Instead of carrying the best run ending here, carry the cheapest price seen so far. Today's best possible profit is then today's price minus that cheapest, and the answer is the largest such value over the whole walk. The reset — “today is the new cheapest” — is the same drop-a-prefix-that-hurts move, since a higher earlier price can never help a later sale.
-int maxSubArray(vector<int>& a) {- int cur = a[0], best = a[0]; // NOT 0 - handles all-negative input+int maxProfit(vector<int>& a) {+ int cheap = a[0], best = 0; // 0: not trading is always allowed for (int i = 1; i < a.size(); i++) {- cur = max(a[i], cur + a[i]); // a prefix that hurts is dropped- best = max(best, cur); // record on EVERY element+ cheap = min(cheap, a[i]); // the best day to have bought+ best = max(best, a[i] - cheap); // record before moving on } return best; }
-public int maxSubArray(int[] a) {- int cur = a[0], best = a[0]; // NOT 0 - handles all-negative+public int maxProfit(int[] a) {+ int cheap = a[0], best = 0; // 0: not trading is always allowed for (int i = 1; i < a.length; i++) {- cur = Math.max(a[i], cur + a[i]); // restart, or extend- best = Math.max(best, cur);+ cheap = Math.min(cheap, a[i]); // the best day to have bought+ best = Math.max(best, a[i] - cheap); // record before moving on }- return best;+ return best; // Kadane on the difference array }
-def max_sub_array(a):- cur = best = a[0] # NOT 0 - handles all-negative input+def max_profit(a):+ cheap, best = a[0], 0 # 0: not trading is always allowed for v in a[1:]:- cur = max(v, cur + v) # a prefix that hurts is dropped- best = max(best, cur) # record on EVERY element+ cheap = min(cheap, v) # the best day to have bought+ best = max(best, v - cheap) # record before moving on return best
Updating best before cheap. If you compute the profit first and only then lower the cheapest price, a single-day array or a strictly decreasing run can produce a profit from buying and selling on the same day. Update cheap first and the ordering constraint enforces itself. The other trap is max(a) − min(a), which ignores order entirely and returns a profit for a trade you could not have made — on [7, 6, 4, 3, 1] it reports 6 where the answer is 0.
The walkthrough for #11 Best Time to Buy and Sell Stock. Watch it, then go straight back and write it yourself.
The write pointer again, doubled. Two destination cursors start at 0 and 1 and each advances by two, so positives fill the even slots and negatives the odd ones without either cursor ever colliding with the other. No bounds check, no interleaving logic — the arithmetic does the work. Relative order survives for free, because values are written in exactly the order they were read.
HOW DO YOU ALTERNATE TWO CATEGORIES WITHOUT SHUFFLING ANYTHING?
Why can the two destination cursors never collide, with no bounds check between them?
Even and odd are disjoint sets. pos starts at 0 and adds 2 forever, so it visits 0, 2, 4…; neg starts at 1 and visits 1, 3, 5…. No value is in both sequences, so no slot can be written twice — and that is a property of the arithmetic, not something the code has to check. This is a small but general idea: choose an indexing scheme that makes the invariant impossible to violate and you delete the guard clause entirely.
On [3, 1, -2, -5, 2, -4], what is the result, and where does the value 1 end up?
[3, −2, 1, −5, 2, −4], with 1 at index 2. The positives in read order are 3, 1, 2 and they take slots 0, 2, 4; the negatives are −2, −5, −4 and they take 1, 3, 5. Notice that 1 stays after 3 and before 2, exactly as in the input — relative order within each sign is preserved because each cursor writes in read order. The result also starts with a positive, which the problem requires.
The harder variant drops the guarantee that positives and negatives are equally numerous. Why does this approach break, and what replaces it?
The stepping-by-two scheme assumes n/2 of each. With three positives and one negative in an array of four, pos would want slots 0, 2, 4 — and 4 is out of bounds. The fix is to stop being clever: collect the two groups, alternate while both are non-empty, then append whatever remains in its original order. It is O(n) either way. Knowing which guarantee a neat trick depends on is what stops you reaching for it when the guarantee is gone.
Two destination cursors, one at 0 and one at 1, each stepping by two. Positives land on even slots and negatives on odd ones, and because the cursors move in twos they can never collide — no bounds check needed, no interleaving logic. Relative order survives for free, because values are written in exactly the order they were read.
“Alternating signs, starting with positive” and “preserve the relative order within positives and within negatives”. The ordering clause is the whole problem — without it you could sort or partition freely. The easy variant also guarantees equal counts of each sign, and that guarantee is what licenses the neat two-cursor trick.
Allocate the result and keep two destination cursors: one at index 0 for positives, one at index 1 for negatives. Walk the input once, and send each value to its own cursor, advancing that cursor by two. Because one cursor only ever visits even indices and the other only odd ones, they cannot collide — no bounds checking, no interleaving logic. Writing in read order is what preserves relative order within each sign, for free.
// Two destination cursors, each stepping by TWO. One visits only even // indices and the other only odd ones, so they are disjoint by construction // and no bounds check between them is ever needed. vector<int> rearrangeArray(vector<int>& a) { vector<int> out(a.size()); int pos = 0, neg = 1; // first even slot, first odd slot for (int v : a) { if (v > 0) { out[pos] = v; pos += 2; } else { out[neg] = v; neg += 2; } } return out; // order kept within each sign }
// Two destination cursors, each stepping by TWO. One visits only even // indices and the other only odd ones, so they are disjoint by construction // and no bounds check between them is ever needed. public int[] rearrangeArray(int[] a) { int[] out = new int[a.length]; int pos = 0, neg = 1; // even slots / odd slots for (int v : a) { if (v > 0) { out[pos] = v; pos += 2; } else { out[neg] = v; neg += 2; } } return out; }
# Two destination cursors, each stepping by TWO. One visits only even # indices and the other only odd ones, so they are disjoint by construction # and no bounds check between them is ever needed. def rearrange_array(a): out = [0] * len(a) pos, neg = 0, 1 # first even slot, first odd slot for v in a: if v > 0: out[pos] = v; pos += 2 else: out[neg] = v; neg += 2 return out # order kept within each sign
Reusing this when the counts are not equal. The stepping-by-two scheme silently assumes exactly n/2 of each sign; with three positives and one negative, pos reaches an index past the end. LeetCode 2149 guarantees the balance, so the code is correct here — but the follow-up variant removes the guarantee, and then you must alternate only while both groups remain and append the leftovers in order. Know which guarantee your trick is standing on, or you will reach for it when it is gone.
The walkthrough for #12 Rearrange Array Elements by Sign. Watch it, then go straight back and write it yourself.
The one genuinely non-obvious construction in this deck, and it stops being a trick the moment you hear the argument. A suffix that is already descending is the largest arrangement of its own values — nothing inside it can be improved. So the change has to happen at the last position that still has something bigger to its right. Swap in the smallest value that beats it, then make everything after it as small as possible.
WHAT IS THE VERY NEXT ARRANGEMENT IN DICTIONARY ORDER, AND HOW DO YOU GET THERE?
Why is the final step a reverse of the suffix rather than a sort of it?
Reversing a descending sequence sorts it. The pivot search stopped precisely because everything to its right was non-increasing, and the swap cannot break that property — the incoming value is smaller than the one it replaced but still larger than everything after it. So the suffix is still descending, and one reversal makes it ascending, which is the smallest arrangement. You get an O(n) step where a sort would have cost O(n log n), purely because you knew something about the data.
Run it on [1, 3, 5, 4, 2]. What is the pivot, what does it swap with, and what is the final answer?
Pivot 3 at index 1. Scanning right to left: 4 ≥ 2 keeps going, 5 ≥ 4 keeps going, 3 < 5 stops — so i = 1. The suffix [5, 4, 2] is descending, confirming it is maximal. The rightmost value above 3 is 4 at index 3, so swap to get [1, 4, 5, 3, 2], then reverse the suffix to get [1, 4, 2, 3, 5]. The MECHANISM slide walks exactly this and asks you to pick the swap partner.
This picks the swap partner by scanning from the LEFT of the suffix instead of the right. What does it produce?
int j = i + 1; while (j < n && a[j] > a[i]) j++; // first value above the pivot swap(a[i], a[j - 1]);
It overshoots. The suffix is descending, so scanning from the left finds the largest value exceeding the pivot, while you want the smallest one — the minimum possible increase. On [1, 3, 5, 4, 2] it would swap 3 with 5, giving [1, 5, 2, 3, 4], which is a later permutation than [1, 4, 2, 3, 5]. Still a valid permutation, still larger, still wrong. “Next” means smallest step up, and on a descending suffix that is the rightmost qualifying value.
The only non-obvious construction here, and it stops being a trick once you see the argument. A descending suffix is already the largest arrangement of its own values, so nothing inside it can be improved — the change must happen at the last position that still has something bigger to its right. Swap in the smallest such value, then make the suffix as small as possible by reversing it.
“Next permutation in lexicographic order” and “in place, constant extra memory”. “Next” is the operative word: you are not generating permutations or sorting, you are making the smallest possible increase. That phrasing always points at a local, constructive rule rather than a search — and the O(1) space clause rules out generating anything.
Scan from the right for the first position whose value is smaller than its neighbour — the pivot. Everything after it is descending, which means that suffix is already the largest arrangement of its own values and nothing inside it can improve. So the pivot is the last place with room to grow. Swap it with the smallest value to its right that still beats it (on a descending suffix, that is the rightmost such value), then reverse the suffix to make it as small as possible.
// A descending suffix is ALREADY the largest arrangement of its values, // so the change must happen at the last position with room to grow. void nextPermutation(vector<int>& a) { int n = a.size(), i = n - 2; while (i >= 0 && a[i] >= a[i + 1]) i--; // find the pivot if (i >= 0) { int j = n - 1; while (a[j] <= a[i]) j--; // RIGHTMOST value above it swap(a[i], a[j]); // smallest possible increase } reverse(a.begin() + i + 1, a.end()); // descending -> ascending }
// A descending suffix is ALREADY the largest arrangement of its values, // so the change must happen at the last position with room to grow. public void nextPermutation(int[] a) { int n = a.length, i = n - 2; while (i >= 0 && a[i] >= a[i + 1]) i--; // find the pivot if (i >= 0) { int j = n - 1; while (a[j] <= a[i]) j--; // smallest value ABOVE it int t = a[i]; a[i] = a[j]; a[j] = t; } for (int l = i + 1, r = n - 1; l < r; l++, r--) { // reverse, not sort: int t = a[l]; a[l] = a[r]; a[r] = t; // the suffix descends } }
# A descending suffix is ALREADY the largest arrangement of its values, # so the change must happen at the last position with room to grow. def next_permutation(a): n = len(a) i = n - 2 while i >= 0 and a[i] >= a[i + 1]: i -= 1 # find the pivot if i >= 0: j = n - 1 while a[j] <= a[i]: j -= 1 # RIGHTMOST value above it a[i], a[j] = a[j], a[i] # smallest possible increase a[i + 1:] = reversed(a[i + 1:]) # descending -> ascending
Scanning for the swap partner from the left of the suffix finds the largest value above the pivot instead of the smallest, producing a permutation that is larger than the input but not the next one — valid-looking and wrong. The second trap is forgetting the no-pivot case: on a fully descending array i ends at −1, and reverse(a.begin() + 0, a.end()) correctly yields the first permutation. Writing i + 1 without checking i >= 0 around the swap is what breaks it.
The walkthrough for #13 Next Permutation. Watch it, then go straight back and write it yourself.
A hash set, and one guard clause that turns a nested loop into a linear algorithm. Put every value in a set, then walk runs — but only start walking from a value with no left neighbour. That single check means each run is traversed exactly once across the whole execution, so the inner while does O(n) total work no matter how it looks. This is also the unit where “consecutive” and “contiguous” get separated for good.
HOW IS A LOOP INSIDE A LOOP STILL O(n)?
The code has a while nested inside a for, yet it is O(n). What makes that true?
Each run is walked exactly once, ever. The guard if (s.count(v-1)) continue; means only a run's head does any walking, and a run of length k is walked from its head for k steps and never touched again. Summing over all runs gives at most n total steps in the inner loop across the entire execution. O(1) lookup is necessary but not sufficient — remove the guard and every element walks its whole run, which is genuinely O(n²). The MECHANISM slide counts the walk steps so you can watch them stay under n.
On [100, 4, 200, 1, 3, 2], which values actually start a walk?
100, 200 and 1. For 4, the value 3 is present, so it is skipped instantly; likewise 3 (2 present) and 2 (1 present). 100 walks a run of length 1, 200 the same, and 1 walks 1→2→3→4 for a length of 4, which is the answer. Total inner-loop work: 6 steps for 6 elements. Notice the three skipped values cost one set lookup each and nothing more — that is the guard paying for itself.
A candidate says “longest run” and reaches for Kadane. Why is that the wrong pattern here?
Two different words. Contiguous means adjacent in the array — that is Kadane's world, and position matters. Consecutive here means adjacent in value, and the array's ordering is irrelevant: [100, 4, 200, 1, 3, 2] has its answer scattered across non-adjacent positions. Mistaking one for the other sends you down a pattern that cannot represent the question. This is the single most valuable habit in the deck — read the statement's nouns precisely, because they select the pattern.
This widget exists to make one claim believable. There is a while nested inside a for, and the whole thing is linear — because of the guard: only a value with no left neighbour starts a walk, so every run is walked exactly once in its entire lifetime. Watch the TOTAL STEPS WALKED counter against the array length; it never exceeds it. Remove the guard and the same code is O(n²).
“Consecutive sequence” — consecutive in value, not contiguous in position, so the array's order carries no information and you are free to hash it. Add the explicit O(n) requirement and sorting is ruled out too, which leaves a set. The word to notice is the one that is absent: nothing says subarray, so nothing ties the answer to adjacency.
Put every value in a hash set so membership is O(1). Then, for each value, ask whether it is the head of a run by checking if v−1 is absent. Only heads walk forward counting v+1, v+2, and so on. That guard is what keeps the nested loop linear: a run of length k is walked exactly once in the algorithm's entire lifetime, from its own head, so the total inner work across all runs is at most n.
// A while inside a for, and still O(n) -- because of ONE guard. // Only a value with no left neighbour starts a walk, so every run is // traversed exactly once across the whole execution. Drop the guard and // the identical code becomes O(n^2). int longestConsecutive(vector<int>& a) { unordered_set<int> s(a.begin(), a.end()); // dedupes for free int best = 0; for (int v : s) { if (s.count(v - 1)) continue; // not a head - skip int x = v, len = 1; while (s.count(x + 1)) { x++; len++; } // walk the run once best = max(best, len); } return best; }
// A while inside a for, and still O(n) -- because of ONE guard. // Only a value with no left neighbour starts a walk, so every run is // traversed exactly once across the whole execution. Drop the guard and // the identical code becomes O(n^2). public int longestConsecutive(int[] a) { Set<Integer> s = new HashSet<>(); for (int v : a) s.add(v); int best = 0; for (int v : s) { if (s.contains(v - 1)) continue; // THE guard: only run starts int len = 1; while (s.contains(v + len)) len++; best = Math.max(best, len); } return best; }
# A while inside a for, and still O(n) -- because of ONE guard. # Only a value with no left neighbour starts a walk, so every run is # traversed exactly once across the whole execution. Drop the guard and # the identical code becomes O(n^2). def longest_consecutive(a): s = set(a) # dedupes for free best = 0 for v in s: if v - 1 in s: continue # not a head - skip x, length = v, 1 while x + 1 in s: # walk the run once x, length = x + 1, length + 1 best = max(best, length) return best
Dropping the v−1 guard leaves code that returns the right answer on every small test and is genuinely O(n²) — on a single run of 10⁵ consecutive values it walks the entire run from every element and times out. The judge reports TLE, not a wrong answer, so it reads as “my solution is too slow” rather than “I omitted the line that makes it fast”. The second trap is reaching for Kadane because the word “longest” appears: Kadane needs contiguity, and this problem deliberately has none.
The walkthrough for #14 Longest Consecutive Sequence. Watch it, then go straight back and write it yourself.
The obvious solution needs a copy of the whole matrix, or two extra arrays. The O(1)-space answer is a small idea with one sharp corner: use the matrix's own first row and first column as the notebook. When a cell is zero, mark the top of its column and the start of its row. The one collision — a[0][0] would have to record both row 0 and column 0 — is handled by lifting column 0 out into a single boolean.
HOW DO YOU REMEMBER WHICH ROWS AND COLUMNS TO ZERO WITHOUT ANY EXTRA MEMORY?
Why does column 0 need a separate boolean when row 0 does not get one?
a[0][0] is the shared corner. It belongs to both the row-marker strip (column 0) and the column-marker strip (row 0), so it can only carry one of the two bits. The convention is to let a[0][0] mean “row 0 contains a zero” and to lift the other question — “does column 0 contain a zero?” — into a standalone boolean. It is not an optimisation; without it one of the two strips is silently corrupted. This single collision is the entire subtlety of the problem, and it is exactly what an interviewer is probing for.
Why must the second pass iterate from the bottom-right corner backwards, rather than top-left forwards?
The markers live in the cells you would overwrite first. If you zeroed the interior top-down and a[0][c] happened to be a marker, you would clear it before the rows below had a chance to consult it. Walking bottom-up and right-to-left visits every interior cell before touching the first row and column, so the notes survive exactly as long as they are needed. Then column 0 is handled last, from the col0 boolean. Order of traversal is load-bearing here — it is not a style choice.
This marks correctly but zeroes the matrix in the FIRST pass, as it finds each zero. What goes wrong?
for (r) for (c) if (a[r][c] == 0) { for (k) a[r][k] = a[k][c] = 0; // zero it now }
A cascade. The moment you zero a row in pass one, every zero you just wrote looks identical to an original zero to the rest of the scan — so their columns get zeroed, then those columns' rows, and the whole matrix collapses to zeros on almost any input. This is the reason for the two-pass structure: separate deciding from doing. Pass one only marks; pass two only acts. Conflating them is the single most common way this problem is failed, and it fails loudly — an all-zero matrix — which at least announces itself.
The obvious solution needs two arrays; the O(1) answer uses the matrix's own first row and column as marker strips. A zero at (r, c) marks a[r][0] and a[0][c] — it leaves a note, it does not clear anything yet. The one collision is a[0][0], which cannot mark both row 0 and column 0, so column 0 gets a single boolean. Watch pass two run bottom-up: the markers live in cells it must read before it overwrites them.
“In place, with constant extra space” on a matrix problem is the whole signal. The naive answer — record which rows and columns to zero in two auxiliary arrays — is O(m+n) space and obvious; the follow-up explicitly asks you to beat it. The moment you are told “no extra memory”, the matrix itself has to become the scratchpad.
Use the first row and first column as marker strips. In pass one, whenever a cell is zero, write a zero to the top of its column and the start of its row — you are leaving notes, not clearing data yet. The single collision is a[0][0], which cannot mark both row 0 and column 0, so column 0's fate is tracked in one separate boolean. In pass two, walk the interior from the bottom-right backwards, zeroing any cell whose row or column marker is set, and handle column 0 last.
// The matrix stores its own bookkeeping: row 0 and column 0 become the // marker strips. a[0][0] is the ONE collision -- it cannot mark both, so // column 0's fate lives in a single boolean. void setZeroes(vector<vector<int>>& a) { int m = a.size(), n = a[0].size(); bool col0 = false; for (int r = 0; r < m; r++) { if (a[r][0] == 0) col0 = true; for (int c = 1; c < n; c++) if (a[r][c] == 0) a[r][0] = a[0][c] = 0; // leave notes, do not clear } for (int r = m - 1; r >= 0; r--) { // bottom-up: read before overwrite for (int c = n - 1; c >= 1; c--) if (a[r][0] == 0 || a[0][c] == 0) a[r][c] = 0; if (col0) a[r][0] = 0; // column 0 handled last } }
// The matrix stores its own bookkeeping: row 0 and column 0 become the // marker strips. a[0][0] is the ONE collision -- it cannot mark both, so // column 0's fate lives in a single boolean. public void setZeroes(int[][] a) { int m = a.length, n = a[0].length; boolean col0 = false; for (int r = 0; r < m; r++) { if (a[r][0] == 0) col0 = true; // column 0 marked separately for (int c = 1; c < n; c++) if (a[r][c] == 0) { a[r][0] = 0; a[0][c] = 0; } } for (int r = m - 1; r >= 0; r--) { // BACKWARDS: markers read last for (int c = n - 1; c >= 1; c--) if (a[r][0] == 0 || a[0][c] == 0) a[r][c] = 0; if (col0) a[r][0] = 0; } }
# The matrix stores its own bookkeeping: row 0 and column 0 become the # marker strips. a[0][0] is the ONE collision -- it cannot mark both, so # column 0's fate lives in a single boolean. def set_zeroes(a): m, n = len(a), len(a[0]) col0 = False for r in range(m): if a[r][0] == 0: col0 = True for c in range(1, n): if a[r][c] == 0: a[r][0] = a[0][c] = 0 # leave notes, do not clear for r in range(m - 1, -1, -1): # bottom-up: read before overwrite for c in range(n - 1, 0, -1): if a[r][0] == 0 or a[0][c] == 0: a[r][c] = 0 if col0: a[r][0] = 0 # column 0 handled last
Zeroing during the first pass. The instant you clear a real row while still scanning, the fresh zeros you wrote are indistinguishable from original ones, and the next iterations wipe their rows and columns too — a cascade that collapses almost any matrix to all zeros. The fix is the two-pass split: pass one only marks, pass two only acts. The second, quieter trap is iterating pass two top-down, which overwrites the first-row markers before the rows beneath have read them.
The walkthrough for #15 Set Matrix Zeroes. Watch it, then go straight back and write it yourself.
Two operations, each trivially correct, whose composition is a 90° rotation — and seeing why is far better than memorising it. Transpose reflects the matrix across its main diagonal, turning rows into columns. Reverse each row then flips left-to-right. Do both and every element lands exactly where a clockwise turn would put it, with no scratch matrix and no index gymnastics.
HOW DO YOU ROTATE A MATRIX 90° WITHOUT ALLOCATING A SECOND ONE?
Why does the transpose loop start its inner index at c = r + 1 instead of c = 0?
Swapping twice is identity. A transpose swaps a[r][c] with a[c][r]. If the inner loop ran the full row, it would later reach position (c, r) and swap them back — every pair touched twice, the matrix unchanged. Starting at c = r + 1 visits each off-diagonal pair exactly once and leaves the diagonal (where r = c) untouched, which is correct because a diagonal element maps to itself. This is the classic in-place-swap off-by-one, and it produces a silently unchanged matrix rather than a crash.
After transposing [[1,2,3],[4,5,6],[7,8,9]], what does row 0 look like, and what is it after the row-reversal step?
[1,4,7], then reversed to [7,4,1]. The transpose turns the first column (1,4,7) into the first row, so row 0 becomes [1,4,7]. That is the correct set of values for the rotated top row, but in the wrong order — a clockwise turn should read [7,4,1]. Reversing the row fixes it. Watch the MECHANISM slide do exactly this: the transpose gathers the right values, the reversal orients them.
You need to rotate 90° anticlockwise instead. What is the minimal change?
Swap the order of the two operations. Clockwise is transpose-then-reverse-rows; anticlockwise is reverse-rows-then-transpose (equivalently, transpose then reverse each column). Both are still two O(1)-space passes. Understanding rotation as a composition of a reflection and a reflection is what lets you derive any of the four orientations on the spot, instead of memorising four separate index formulas — which is the whole reason this unit teaches the why rather than the code.
A 90° clockwise rotation is two reflections in sequence. Transpose swaps a[r][c] with a[c][r], reflecting across the main diagonal; reverse each row then flips left-to-right. The inner loop starts at c = r+1 for a reason worth watching — running the full square swaps every pair twice and leaves the matrix exactly as it began. The diagonal never moves.
“Rotate the image in place” on a square matrix. In place plus square is the tell that this is an index-algebra problem with a two-step decomposition, not something that needs a fresh grid. Nothing is compared and nothing is searched — every element's destination is a pure function of its coordinates.
A 90° clockwise rotation is exactly two reflections done in sequence. First transpose the matrix — swap a[r][c] with a[c][r] — which reflects it across the main diagonal and turns columns into rows. Then reverse each row, which flips it left-to-right. The composition sends every element precisely where a clockwise turn would, and both steps are in place, so no second matrix is ever allocated.
// 90 clockwise = transpose, then reverse each row. Both are reflections, // and their composition is the rotation. The inner loop starts at r+1 so // each pair swaps ONCE -- running the full square would undo the transpose. void rotate(vector<vector<int>>& a) { int n = a.size(); for (int r = 0; r < n; r++) for (int c = r + 1; c < n; c++) swap(a[r][c], a[c][r]); // reflect across the main diagonal for (auto& row : a) reverse(row.begin(), row.end()); // flip left-to-right }
// 90 clockwise = transpose, then reverse each row. Both are reflections, // and their composition is the rotation. The inner loop starts at r+1 so // each pair swaps ONCE -- running the full square would undo the transpose. public void rotate(int[][] a) { int n = a.length; for (int r = 0; r < n; r++) for (int c = r + 1; c < n; c++) { // r+1, not 0 int t = a[r][c]; a[r][c] = a[c][r]; a[c][r] = t; } for (int[] row : a) for (int l = 0, rr = n - 1; l < rr; l++, rr--) { int t = row[l]; row[l] = row[rr]; row[rr] = t; } }
# 90 clockwise = transpose, then reverse each row. Both are reflections, # and their composition is the rotation. The inner loop starts at r+1 so # each pair swaps ONCE -- running the full square would undo the transpose. def rotate(a): n = len(a) for r in range(n): for c in range(r + 1, n): a[r][c], a[c][r] = a[c][r], a[r][c] # reflect across the diagonal for row in a: row.reverse() # flip left-to-right
Starting the transpose's inner loop at 0. Running the full square swaps every off-diagonal pair twice, and two swaps are the identity — so the matrix comes back unchanged after the transpose, and the row-reversal then produces a horizontal mirror instead of a rotation. It does not crash and it looks like a plausible transformation, just the wrong one. Start the inner index at r + 1. The related slip is forgetting the problem is square-only; on a non-square matrix the in-place transpose is not even defined.
The walkthrough for #16 Rotate Image. Watch it, then go straight back and write it yourself.
No cleverness, just four bounds and the discipline to shrink them. Walk the top row left-to-right, the right column top-to-bottom, the bottom row right-to-left, the left column bottom-to-top — then tighten each bound inward and repeat. The only place it goes wrong is the last layer, where a lone remaining row or column can be emitted twice; two guard conditions prevent exactly that.
HOW DO YOU WALK A MATRIX IN A SPIRAL WITHOUT EVER REVISITING A CELL?
After the top row and right column of a layer are walked, why is a top <= bot check needed before walking the bottom row?
The single-row-left case. After walking the top row you do top++; if only one row remained, top is now greater than bot. The bottom-row loop would then walk that same row backwards and emit every value twice. The guard top <= bot catches it. Crucially this is not the same as the outer while condition — the bounds change inside one iteration, so you must re-check. Same logic guards the left column with left <= right. The guards are the problem; the four walks are bookkeeping.
Spiralling [[1,2,3,4],[5,6,7,8],[9,10,11,12]], what are the first six values emitted?
1, 2, 3, 4, 8, 12. Top row left-to-right gives 1, 2, 3, 4; then top shrinks and the right column (index 3) top-to-bottom gives 8, then 12. Next comes the bottom row backwards — 11, 10, 9 — then the left column up. Notice the corners: 4 belongs to the top row, and 8 and 12 to the right column, each emitted exactly once. The MECHANISM slide draws the shrinking frame so you can watch the live region close in.
This omits the two inner guards, keeping only the outer while. On which matrices does it break?
while (top <= bot && left <= right) { // ... top row, right col ... for (c = right; c >= left; c--) out.push(a[bot][c]); bot--; // ... left col, no guard ... }
Odd dimensions expose it. When the layers close on a single leftover row or column, the outer while is still true (the bounds have not crossed yet), so the bottom-row and left-column loops run on a line that the top-row and right-column loops already covered — duplicating it. A 3×3 or any m×n with an odd dimension shows it; a clean 2×2 or 4×4 may not, which is why it slips through hand tests. The outer condition guards entry to a layer; the inner guards protect the two second-half walks within it.
No cleverness — four bounds enclosing the unvisited region, each tightened after its edge is walked. The gold frame is that region closing in. The two guards before the bottom row and the left column are the entire difficulty: on the last layer a lone remaining row or column would otherwise be emitted twice, and those checks are not the same as the outer loop condition.
“Return all elements in spiral order” is a pure traversal problem — no optimisation, no data structure, just the discipline to visit each cell once. The signal is that there is no signal: when a matrix problem asks you only to walk it in some order, the answer is boundary tracking, and the entire difficulty is the edge cases.
Keep four bounds — top, bottom, left, right — enclosing the not-yet-visited region. Walk the top row left-to-right and drop top; the right column top-to-bottom and drop right; the bottom row right-to-left and drop bottom; the left column bottom-to-top and raise left. Repeat until the bounds cross. The only subtlety is the final layer: after shrinking, a single remaining row or column must not be walked twice, which two guard checks prevent.
// Four bounds that only shrink. The two inner guards are the whole // problem: without them a lone final row or column is emitted twice. vector<int> spiralOrder(vector<vector<int>>& a) { int top = 0, bot = a.size() - 1, left = 0, right = a[0].size() - 1; vector<int> out; while (top <= bot && left <= right) { for (int c = left; c <= right; c++) out.push_back(a[top][c]); top++; for (int r = top; r <= bot; r++) out.push_back(a[r][right]); right--; if (top <= bot) { // guard: still a row left? for (int c = right; c >= left; c--) out.push_back(a[bot][c]); bot--; } if (left <= right) { // guard: still a column left? for (int r = bot; r >= top; r--) out.push_back(a[r][left]); left++; } } return out; }
// Four bounds that only shrink. The two inner guards are the whole // problem: without them a lone final row or column is emitted twice. public List<Integer> spiralOrder(int[][] a) { int top = 0, bot = a.length - 1, left = 0, right = a[0].length - 1; List<Integer> out = new ArrayList<>(); while (top <= bot && left <= right) { for (int c = left; c <= right; c++) out.add(a[top][c]); top++; for (int r = top; r <= bot; r++) out.add(a[r][right]); right--; if (top <= bot) { // GUARD for (int c = right; c >= left; c--) out.add(a[bot][c]); bot--; } if (left <= right) { // GUARD for (int r = bot; r >= top; r--) out.add(a[r][left]); left++; } } return out; }
# Four bounds that only shrink. The two inner guards are the whole # problem: without them a lone final row or column is emitted twice. def spiral_order(a): top, bot, left, right = 0, len(a) - 1, 0, len(a[0]) - 1 out = [] while top <= bot and left <= right: for c in range(left, right + 1): out.append(a[top][c]) top += 1 for r in range(top, bot + 1): out.append(a[r][right]) right -= 1 if top <= bot: # guard: still a row left? for c in range(right, left - 1, -1): out.append(a[bot][c]) bot -= 1 if left <= right: # guard: still a column left? for r in range(bot, top - 1, -1): out.append(a[r][left]) left += 1 return out
Dropping the two inner guards. The outer while guards entry to a whole layer, but inside one iteration the bounds shift, and on an odd dimension the last leftover row or column would be walked a second time by the bottom-row or left-column loop. It is invisible on a clean even×even matrix and shows up the moment a dimension is odd — the classic “works on my 4×4, fails the judge”. The two checks top <= bot and left <= right, placed before the second-half walks, are not the same as the loop condition and cannot be folded into it.
The walkthrough for #17 Spiral Matrix. Watch it, then go straight back and write it yourself.
This is Two Sum wearing a different hat. Instead of asking “have I seen the complement of this value”, you ask “have I seen a prefix sum equal to (current prefix − k)” — because two equal-difference prefixes bracket a subarray summing to k. The one change from Two Sum is that the map stores counts, not indices, because you are counting subarrays rather than locating one. And it must be seeded with {0: 1}.
HOW DO YOU COUNT SUBARRAYS SUMMING TO k WHEN A SLIDING WINDOW WILL NOT WORK?
Why must the prefix-count map be initialised with {0: 1} before the loop starts?
It accounts for subarrays that start at the beginning. When the running prefix through index i equals k exactly, the subarray a[0..i] is a valid answer — and it is “closed” by the empty prefix of sum 0. If 0 is not already in the map with a count of 1, that whole class of subarrays goes uncounted, and the answer is short by exactly the number of prefixes equal to k. It is invisible on inputs where no prefix hits k, which is how the bug survives casual testing. The seed is not defensive; it is a real occurrence of the empty prefix.
On [3, 4, 7, 2, -3, 1, 4, 2] with k = 7, how many subarrays sum to 7, and does a sliding window find them all?
Four subarrays: [3,4], [7], [7,2,-3,1] and [1,4,2]. A sliding window relies on the sum growing as the window widens, so that overshooting k means shrinking from the left. The -3 breaks that: the sum is no longer monotonic, so a window can skip right past valid subarrays or shrink when it should not. The prefix-sum-plus-map approach does not care about monotonicity at all, which is exactly why it is the right tool once negatives are on the table. The MECHANISM slide finds all four.
This exact template also solves “longest subarray with sum k” and “count subarrays with XOR = k”. What changes between the three?
The prefix operation and what the map stores are the only knobs. For a count the map holds occurrence counts and you accumulate them; for the longest subarray it holds the earliest index of each prefix, so you can measure spans and keep the widest. Swap the running sum for a running XOR and the complement from sum − k to xor ⊕ k, and the identical skeleton counts XOR-subarrays. These are the two lectures this deck deliberately dropped — they are this one problem with two knobs turned, and recognising that is worth more than solving each fresh.
A subarray sums to k exactly when two prefix sums differ by k — so “is there an earlier prefix equal to (current − k)?” is the same O(1) map lookup Two Sum uses for complements. The map stores counts, not indices, because you are counting subarrays, and it is seeded with {0:1} for the empty prefix. Negatives are handled for free, which is precisely why a sliding window cannot be used here.
“Count the subarrays” summing to k, and — read the constraints — the values may be negative. Those two facts together are the whole signal. “Count” rather than “longest” means the map stores occurrences; negatives means a sliding window is off the table, because the running sum is no longer monotonic as the window grows.
This is Two Sum on prefix sums. Let P(i) be the sum of the first i elements. A subarray a[j+1..i] sums to k exactly when P(i) − P(j) = k, i.e. P(j) = P(i) − k. So as you sweep and maintain the running prefix, ask how many earlier prefixes equal (current − k); each one closes a distinct valid subarray ending here. A hash map of prefix → count answers that in O(1), and it must be seeded with {0: 1} for the empty prefix.
// Two Sum on prefix sums: a[j+1..i] sums to k iff P(j) = P(i) - k. // The map counts prefix OCCURRENCES because we are counting subarrays, // and it is seeded with {0:1} for the empty prefix. Negatives are handled // for free -- which is exactly why a sliding window will not do. int subarraySum(vector<int>& a, int k) { unordered_map<int, int> cnt{{0, 1}}; // the empty prefix counts int sum = 0, total = 0; for (int v : a) { sum += v; total += cnt[sum - k]; // every earlier P(j)=sum-k closes one cnt[sum]++; // record AFTER checking } return total; }
// Two Sum on prefix sums: a[j+1..i] sums to k iff P(j) = P(i) - k. // The map counts prefix OCCURRENCES because we are counting subarrays, // and it is seeded with {0:1} for the empty prefix. Negatives are handled // for free -- which is exactly why a sliding window will not do. public int subarraySum(int[] a, int k) { Map<Integer, Integer> count = new HashMap<>(); count.put(0, 1); // the empty prefix int prefix = 0, total = 0; for (int v : a) { prefix += v; total += count.getOrDefault(prefix - k, 0); // how many starts qualify count.merge(prefix, 1, Integer::sum); } return total; }
# Two Sum on prefix sums: a[j+1..i] sums to k iff P(j) = P(i) - k. # The map counts prefix OCCURRENCES because we are counting subarrays, # and it is seeded with {0:1} for the empty prefix. Negatives are handled # for free -- which is exactly why a sliding window will not do. from collections import defaultdict def subarray_sum(a, k): cnt = defaultdict(int); cnt[0] = 1 # the empty prefix counts running = total = 0 for v in a: running += v total += cnt[running - k] # every earlier P(j)=running-k closes one cnt[running] += 1 # record AFTER checking return total
Forgetting the {0: 1} seed silently undercounts by exactly the number of prefixes that equal k — every subarray starting at index 0 is missed. It is invisible whenever no prefix happens to hit k, so it survives casual testing and fails specific cases. The larger trap is reaching for a sliding window because the phrase “subarray sum” feels window-shaped: with negative numbers the sum is not monotonic in the window width, so shrinking on overshoot is unsound and the window misses answers. If the array were guaranteed non-negative, a window would be valid and cheaper — reading that constraint is what tells you which tool applies.
The walkthrough for #18 Subarray Sum Equals K. Watch it, then go straight back and write it yourself.
“Given an unsorted array, return the length of the longest run of consecutive integers.” n ≤ 10⁵. Which pattern, and why not the obvious one?
Hash set with a left-neighbour guard. Sorting works and is the honest fallback, but the O(n) answer is the point: put everything in a set, then for each value only begin counting if v-1 is absent — that makes each run walked exactly once, so the total work is O(n) despite the nested loop. Kadane is wrong here because the run is consecutive in value, not contiguous in position. “Consecutive” and “contiguous” are different words and they select different patterns.
You need to COUNT subarrays summing to exactly k, not find the longest. What changes?
Store counts, and accumulate. For the longest subarray you store the earliest index a prefix was seen, because earliest gives the longest span. For a count you store how many times that prefix has occurred and add all of them, since every earlier occurrence closes a distinct valid subarray. And note a sliding window is simply invalid once negatives are allowed — the sum is no longer monotonic as the window grows, so shrinking on overshoot loses answers.
Every one of these compiles, runs, and returns a number that looks reasonable. None of them crashes. That is the entry test for this slide — a crash teaches itself; a wrong answer costs you an hour.
Boyer–Moore's survivor is only the only possible majority element, never a proof that one exists. Skip the second counting pass and [1,2,3] confidently returns 3. LeetCode 169 guarantees a majority so it passes; 169's twin 229 does not, and the same habit fails there.
Rotating by k where k > n is legal input on LeetCode 189. Without the modulo, a.begin() + k runs past the end — and because it is undefined behaviour it often does not crash, it just produces a plausible wrong array on the judge's larger tests and passes yours.
Move Zeroes and Rearrange by Sign both say “preserve relative order”. A partition that swaps from the back is O(n) and correct-looking, and it silently reverses the order of the values it moves. The tests that catch it are not the small ones.
Counting subarrays with sum k needs mp[0] = 1 seeded before the loop, or every subarray that starts at index 0 goes uncounted. The answer is off by exactly the number of valid prefixes — small, plausible, and invisible on [1,2,3].
Four ints near 10⁹ sum past 2³¹ and wrap to a negative. The comparison then succeeds against a target it should never have matched. Accumulate in long long; in Reverse Pairs, compare a[i] > 2LL * a[j] and not a[i]/2 > a[j], which truncates.
In 3Sum you must skip duplicates for the fixed index and for both pointers after a hit. Skipping only the fixed one yields duplicate triplets; skipping before recording the hit loses valid ones. Both mistakes produce a list that looks right until the judge diffs it.
Nine patterns, eighteen problems. This is the slide to reread the night before — the right-hand column is the part that actually matters, because recognising the pattern is the whole difficulty.
These nine patterns are the ones every other topic borrows — the two-pointer walk reappears in strings, the prefix map in subarrays and DP, the running best in stocks. Deck 2 stacks them into the eight Hard rows.
Lectures are Striver's A2Z DSA course. Problem links are LeetCode. 14 units from 14 of the playlist's 28 lectures; the Hard rows' 8 lectures are deck 2.
Not a preference — an arithmetic one. Every slide is a fixed 1280 × 720 stage: a graph animating beside the code that drives it, with the problems laid out two columns wide. It scales as a single piece, so on a screen this size the body text comes out around 4px tall.
Shrinking it further would not help, and rebuilding it to reflow would mean losing the thing that makes it worth reading.