LC Patterns — Arrays & Strings
lc-patterns-<topic>.md · Prev: binary-search, two-pointers, sliding-window, 1d-dp · Code: C++◆ The Master Insight
Arrays & strings is the vocabulary topic. Most problems here aren't one giant algorithm — they're a small bag of representation tricks you combine: precompute totals (prefix sums), use a value as an index, turn different-looking things into one canonical form (sorted key), shuffle things in place (reversal/transpose), or carefully follow a spec (parsing). The real skill being tested is choosing the representation that makes the problem easy — then the code is short.
- Pair-finding with order, or squeezing an array in place →
two-pointers - A contiguous subarray with a constraint (longest/shortest/count) →
sliding-window - Sorted data, or a yes/no test that flips once →
binary-search - Choices with overlapping subproblems →
1d-dp
§0C++ Notes & Universal Pitfalls
The traps that cost wrong answers
1. Prefix-sum overflow. With n=10⁵ values up to 10⁹, a running total reaches 10¹⁴ — far past int. Make prefix arrays long long from the start.
2. size_t underflow. v.size()-1 on an empty vector wraps to a giant positive number and crashes. Guard with (int)v.size()-1. C++-only trap.
3. Off-by-one in prefix indexing. Use size n+1 with pre[0]=0 and pre[i]=pre[i-1]+a[i-1]; sum of [l,r] is pre[r+1]-pre[l]. Write it as a comment; never improvise mid-loop.
4. String building cost. s = s + t in a loop is O(n²). Use s += t and reserve(). C++ std::string is already mutable — no StringBuilder needed.
5. Mutating while iterating. Erasing from a vector in a forward loop skips elements. Iterate backwards or build fresh.
6. The edge triad. Run every solution against three inputs: empty, single element, all-identical. This topic hides most bugs in exactly these three.
1Prefix Sums (and Prefix/Suffix Products)
What it is: Add up the array once into a "running total" array, so any range sum [l,r] becomes one subtraction: pre[r+1] − pre[l]. The product version: "product of everything except me" = (product to my left) × (product to my right) — two passes, no division.
Intuition: A range sum is the difference of two running totals — everything up to r, minus everything before l. Works for any operation that can be "undone" (sum and XOR both can). When it can't be undone cleanly (products with a zero, or min/max), switch to the two-sided version: build a total from the left and one from the right, and combine.
vector<int> productExceptSelf(vector<int>& a) {
int n = a.size();
vector<int> res(n, 1);
for (int i = 1; i < n; ++i) res[i] = res[i-1] * a[i-1]; // res[i] = product of everything to the left
long long suf = 1;
for (int i = n - 1; i >= 0; --i) { res[i] *= suf; suf *= a[i]; } // multiply in the right side
return res;
}
Challenges & pitfalls
The n+1 indexing convention (§0.3); overflow (§0.1); noticing XOR versions (same trick, ^ instead of +, since XOR undoes itself); and remembering 238 bans division on purpose — the division shortcut dies the moment a zero appears.
- Build running total, one longer than the array, starting at 0:
pre = [0,2,6,7,10,15]. - Sum of
a[1..3]=pre[4] − pre[1]=10 − 2= 8. Check: 4+1+3 = 8. ✓ - After the one-time build, every query is O(1) — no matter how many they ask.
- Pass 1 (left→right): product to the left of each spot →
res = [1,1,2,6]. - Pass 2 (right→left): carry running product
sufand multiply in →[24,12,8,6]. ✓
2Prefix Sum + Hash Map (first-occurrence trick)
What it is: Count (or find) subarrays whose sum is exactly k — even with negatives, where sliding windows fail. Walk once keeping a running sum. A subarray [j+1..i] sums to k exactly when pre[i] − pre[j] = k. So at each step ask the map: "have I seen an earlier running-sum equal to pre[i] − k?"
Intuition: This turns a "which two endpoints?" search into a "walk forward and remember what I've seen" scan. It's literally Two Sum, played on running sums. 525 turns each 0 into −1 so "equal 0s and 1s" becomes "sum = 0"; 974 stores the running sum mod k; "longest such subarray" versions store the first index a running-sum appeared at.
C++ · Subarray Sum Equals K (LC 560)int subarraySum(vector<int>& a, int k) {
unordered_map<long long, int> seen{{0, 1}}; // "empty prefix" seen once — lets subarrays starting at index 0 count
long long pre = 0; int cnt = 0;
for (int x : a) {
pre += x;
if (auto it = seen.find(pre - k); it != seen.end()) cnt += it->second; // ask before recording
++seen[pre];
}
return cnt;
}
| see x | pre | look for pre − k | found? | cnt |
|---|---|---|---|---|
| 1 | 1 | 1 − 3 = −2 | — | 0 |
| 2 | 3 | 3 − 3 = 0 | yes (×1) | 1 |
| 3 | 6 | 6 − 3 = 3 | yes (×1) | 2 |
[1,2] and [3]. The {0:1} seed is what lets [1,2] (starts at index 0) count.Challenges & pitfalls
The {0,1} seed (leave it out and you silently miss every subarray starting at index 0 — the #1 bug); always ask the map before inserting the current sum; C++ % on a negative is negative, so normalize with ((x % k) + k) % k in 974; store a count vs a first index depending on "how many" vs "longest".
- Replace each
0with−1: "equal 0s and 1s" becomes "sum = 0." Array is conceptually[−1, 1, −1]. - Store the first index each running sum appears at. Seed: sum
0first appears at index−1. - i=1: sum
0, seen at −1 → length1 − (−1) = 2. Best = 2 (subarray[0,1]). Storing first occurrence maximizes the span. ✓
3Difference Array (batch range updates)
What it is: The mirror image of prefix sums. For many updates "add v to every element in [l,r]," don't touch every element — record the two edges: diff[l] += v and diff[r+1] -= v. One prefix-sum pass turns the edges back into final values. So m updates + one pass = O(n + m) instead of O(n·m).
Intuition: A range update changes nothing in the middle — only the two boundaries (where the added value switches on and off). Store where things change, and "add it all up" at the end. It's an event timeline: car-pooling is "+v get on at l, −v get off at r."
vector<int> corpFlightBookings(vector<vector<int>>& bookings, int n) {
vector<long long> diff(n + 1, 0);
for (auto& b : bookings) { diff[b[0]-1] += b[2]; diff[b[1]] -= b[2]; } // 1-indexed problem → shift to 0-indexed
vector<int> res(n);
long long run = 0;
for (int i = 0; i < n; ++i) { run += diff[i]; res[i] = run; } // add up the edges
return res;
}
Challenges & pitfalls
The right edge is exclusive → diff[r+1], which is why the array is size n+1 (this off-by-one is the whole pattern); watch 1-indexed statements vs your 0-indexed array; recognizing the pattern at all ("lots of range additions, read at the end"); and if reads and updates are interleaved, this breaks — that's Fenwick/segment-tree territory (name it, don't build it unless asked).
- Treat the road as a timeline.
[2,1,5]:diff[1]+=2,diff[5]-=2.[3,3,7]:diff[3]+=3,diff[7]-=3. - Sweep: at location 1 → 2 aboard; at location 3 → 2+3 = 5 aboard.
- 5 > capacity 4 → return false. ✓
4Index Marking / Cyclic Sort (array as its own hash map)
What it is: When values are bounded by the array's size (values in [1, n]), the array acts as its own lookup table. Mark "value v present" by changing position v−1 (e.g. flip its sign), or cyclic-sort each value to its home slot so a[i] == i+1. O(n) time, O(1) extra space — the follow-up that rules out "just use a hash set."
Intuition: A value→info map costs O(n) memory. But if values fit in [1,n], the array's own indices are that key space — the slot tells you the value, and the sign bit (or swap position) is free storage you already own. In First Missing Positive, once every value is home, the first slot not holding its expected value points straight at the answer.
int firstMissingPositive(vector<int>& a) {
int n = a.size();
for (int i = 0; i < n; ++i)
while (a[i] >= 1 && a[i] <= n && a[a[i]-1] != a[i])
swap(a[i], a[a[i]-1]); // send a[i] to its home slot
for (int i = 0; i < n; ++i)
if (a[i] != i + 1) return i + 1; // first slot that isn't holding its own value
return n + 1;
}
i+1: slot 1 holds −1, expected 2 → answer 2Challenges & pitfalls
The swap guard a[a[i]-1] != a[i] stops an infinite loop on duplicates — the bug of this pattern; be ready to explain why while-swap is still O(n) ("each swap puts one value in its final home, so ≤ n swaps"); if you sign-flip instead, read abs(a[i]) everywhere; mutating destroys the input — ask if that's allowed.
- For each value
v, go to slotv−1and flip that number negative to mark "vshowed up." - After marking, any slot still positive was never visited — its index+1 is missing.
- Slots for 5 and 6 stay positive → missing = [5, 6]. No hash set needed. ✓
5In-Place Rearrangement via Reversal / Transpose
What it is: Rotating an array by k, rotating a matrix 90°, reversing words — all done in place by combining simple reversals. Rotate-by-k = reverse the whole thing, then reverse the first k, then the rest. Rotate a matrix = transpose, then reverse each row. Reverse words = reverse the whole string, then reverse each word back.
Intuition: Reversing undoes itself and needs no extra space, and rotations/word-swaps break down neatly into a few reversals. Don't trust the formula blindly — follow one element: track where a[0] ends up after each reversal; if it lands where it should, the whole thing is right.
void rotate(vector<int>& a, int k) {
int n = a.size(); k %= n;
reverse(a.begin(), a.end());
reverse(a.begin(), a.begin() + k);
reverse(a.begin() + k, a.end());
}
// Matrix 90° clockwise: transpose (swap a[i][j], a[j][i] for j > i), then reverse each row.
Challenges & pitfalls
Do k %= n first (if k > n the naive version breaks); in transpose, only swap for j > i (the full range swaps everything back); counter-clockwise is transpose + reverse columns — derive it by tracking a corner, don't memorize; reversing words also means collapsing multiple spaces (in 151, the space cleanup is the actual hard part).
6Matrix Traversal & In-Place State Encoding
What it is: Two related skills. (a) Walking a matrix in an unusual order (spiral, diagonal) by tracking shrinking boundaries. (b) Updating a matrix in place when a new value depends on old neighbors — done by storing both old and new value in one cell (an extra bit or sentinel), decoding in a second pass.
top → 1 2 3 right ↓ 6 9
bottom ← 8 7 left ↑ 4
center 5
= [1,2,3,6,9,8,7,4,5]
Intuition: (a) A spiral is four edge-walks — top → right → bottom → left — the boundary shrinking after each side. The loop structure is the answer; simulate it. (b) Game of Life has a chicken-and-egg problem: read old values while writing new. It goes away if one cell holds both — store old + 2·new (bit 0 = old, bit 1 = new), then a final >> 1 pass reveals the new grid. Same idea as 73's trick: use the first row/column as scratch space.
int top = 0, bot = m - 1, lef = 0, rig = n - 1;
while (top <= bot && lef <= rig) {
for (int j = lef; j <= rig; ++j) out.push_back(a[top][j]); ++top; // top row, →
for (int i = top; i <= bot; ++i) out.push_back(a[i][rig]); --rig; // right col, ↓
if (top <= bot) for (int j = rig; j >= lef; --j) out.push_back(a[bot][j]); --bot; // bottom row, ←
if (lef <= rig) for (int i = bot; i >= top; --i) out.push_back(a[i][lef]); ++lef; // left col, ↑
}
Challenges & pitfalls
The two mid-loop re-checks (if (top<=bot) and if (lef<=rig)) — without them a leftover single row/column prints twice (the classic spiral bug); 73's subtlety is the first column needs its own separate flag, since [0][0] can't mark both first-row and first-column at once; encoding tricks must stay decodable (% 2, >> 1) and be explained aloud.
7Intervals: Sort, Then Sweep
What it is: Problems about ranges [start, end] — merge overlapping ones, insert a new one, or count how many to delete so none overlap. Almost all yield to: sort by start (or by end for greedy-deletion), then one linear pass carrying the "current" interval, deciding at each step: overlap, or start a new group?
[1,3] and [2,6] overlap (3 ≥ 2) → extend end to max(3,6)=6. answer [[1,6],[8,10],[15,18]]Intuition: Once sorted by start, an interval can only overlap the one right before it — so cur.end ≥ next.start is the complete overlap test, and merging is cur.end = max(cur.end, next.end). The greedy cousin (435): to keep the most non-overlapping intervals, sort by end and always keep the earliest finisher — it leaves the most room. Overlap test to memorize: two intervals overlap when max(s1,s2) ≤ min(e1,e2).
vector<vector<int>> merge(vector<vector<int>>& iv) {
sort(iv.begin(), iv.end()); // by start (then end)
vector<vector<int>> out;
for (auto& in : iv) {
if (!out.empty() && out.back()[1] >= in[0]) out.back()[1] = max(out.back()[1], in[1]); // overlap → extend
else out.push_back(in); // gap → new group
}
return out;
}
Challenges & pitfalls
Touching endpoints — is [1,2],[2,3] an overlap? (≥ vs > — read it, state your reading aloud); sort-by-start (merge) vs sort-by-end (greedy count) — picking wrong almost works, so the bug is sneaky; 57 without re-sorting is three phases; the "minimum meeting rooms" variant escalates to an event sweep or min-heap — recognize when you've crossed that line.
- Sort by end:
[1,2] [2,3] [1,3] [3,4]. - Keep
[1,2];[2,3]no overlap → keep;[1,3]starts at 1 < 3 → overlaps → remove (count 1);[3,4]no overlap → keep. - Removed 1. Keeping the earliest finisher minimizes removals. ✓
8Canonical-Key Hashing & Grouping
What it is: Group or compare things that are "the same after some transformation" — anagrams, isomorphic strings, mirrored patterns — by turning each item into a single canonical key (sorted letters, a letter-count signature, a first-occurrence encoding) and hashing on that key. Equivalent items produce the same key, so they land in the same bucket automatically.
tea → aet
tan → ant
ate → aet
nat → ant
bat → abt
Intuition: Pairwise "same-except-for-rearrangement" is slow (O(n²)). But normalize each item to one standard form and equal items collide for free. Isomorphic strings / word patterns → encode each char by the index where it first appeared ("egg" → 0,1,1; "add" → 0,1,1 → match ⇒ isomorphic). First-occurrence encoding automatically enforces the two-way matching an isomorphism needs.
C++ · Group Anagrams (LC 49)vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> g;
for (auto& s : strs) {
string key(26, 0);
for (char c : s) ++key[c - 'a']; // the letter-count signature is the canonical key
g[key].push_back(s);
}
vector<vector<string>> out;
for (auto& [k, v] : g) out.push_back(move(v));
return out;
}
Challenges & pitfalls
An isomorphism must hold in both directions ("ab"→"aa" fails only on the reverse mapping — first-occurrence encoding sidesteps this); count-array keys must be genuinely hashable (a string of counts works; text counts need separators — "1,11" must not collide with "11,1"); don't hardcode a 26-letter alphabet without checking (Unicode? uppercase?).
- "egg" → e=0, g=1, g=1 →
[0,1,1]. "add" → a=0, d=1, d=1 →[0,1,1]. Same → isomorphic. ✓ - "foo" →
[0,1,1]. "bar" → b=0, a=1, r=2 →[0,1,2]. Different → not isomorphic. ✓
9String Parsing & Simulation
What it is: No clever algorithm — just a spec you implement exactly: atoi (skip whitespace, read a sign, read digits, clamp on overflow), comparing version numbers (split, compare number by number, missing parts count as 0), multiplying big numbers digit by digit. Graded on careful edge-case handling and clean state, not cleverness.
res > (INT_MAX − d)/10 fires before the multiply → clamp to INT_MAX.Intuition: These test whether you can turn an English spec into ordered steps without dropping a case. Method: (1) restate the spec as a numbered list of phases; (2) list edge inputs straight from the spec ("", " ", "+-2", "words 42", overflow "2147483648"); (3) code one phase at a time. Overflow idiom: check res > (INT_MAX − digit) / 10 before multiplying. Multiply-strings: digits at i and j land in result position i + j + 1 (carry into i + j).
int res = 0;
while (i < n && isdigit(s[i])) {
int d = s[i] - '0';
if (res > (INT_MAX - d) / 10) // would overflow on the next step → clamp per spec
return sign == 1 ? INT_MAX : INT_MIN;
res = res * 10 + d; ++i;
}
Challenges & pitfalls
Coding before listing edge cases (the spec is the test suite); the overflow check must go before the multiply, always; leading zeros in multiply-strings ("0"×"0"="0"); implicit zeros in version compare ("1.0" equals "1.0.0"); narrate the phases as you code — silent simulation is impossible for an interviewer to follow.
"1.01"vs"1.001": split on., compare as integers → 1=1, then 01→1 = 001→1. All equal → 0."1.0"vs"1.0.0": first two equal; missing third part treated as0, and0==0→ 0, they're equal. ✓
10Hash-Set Membership Tricks (complement lookup & smart starts)
What it is: One pass with an unordered_set/unordered_map replacing a nested loop. Two Sum asks the map "have I seen target − x?" Longest Consecutive Sequence (128) gets O(n) on unsorted data by only starting to count a run at its head — a number x where x−1 is not in the set.
Intuition: A hash lookup is basically a free inner loop. The deeper trick in 128 is accounting for the work: walking x, x+1, x+2, … looks quadratic, but only sequence heads start a walk, and every element is walked once across the whole run → amortized O(n). "Only start at heads" is a general de-dup idea: make each piece of work belong to exactly one owner. (Two Sum here vs Two Sum II: unsorted + need indices → hash map; already sorted → two pointers.)
int longestConsecutive(vector<int>& a) {
unordered_set<int> s(a.begin(), a.end());
int best = 0;
for (int x : s) {
if (s.count(x - 1)) continue; // not a head — someone else's run counts this one
int y = x;
while (s.count(y + 1)) ++y; // walk the run forward
best = max(best, y - x + 1);
}
return best;
}
Challenges & pitfalls
Iterate the set, not the array (array duplicates re-run walks and break the O(n) argument); be ready to defend the amortized bound aloud (interviewers push there); unordered_* is O(n) worst-case per op with a bad hash — mention that, or reserve(); in Two Sum, query before you insert, or an element matches itself when i == j.
- Map value→index. See
2(i=0): need7, absent → store2→0. - See
7(i=1): need2, in map at index 0 → answer [0,1]. Querying before inserting stops a number pairing with itself. ✓
▤Recognition Cheat Sheet
| # | Pattern | Trigger phrase | Core trick |
|---|---|---|---|
| 1 | Prefix sums/products | "range sum", "product except self" | range query = subtract two running totals |
| 2 | Prefix + hash map | "subarray sum exactly k", negatives present | look up the complementing prefix, seed {0:1} |
| 3 | Difference array | "add v to range [l,r], many times" | record edges, add up once |
| 4 | Index marking / cyclic sort | "values in [1,n]", "O(1) space", "missing/duplicate" | array indexes itself; swap each value home |
| 5 | Reversal composition | "rotate in place", "reverse words" | rotation = three reversals; 90° = transpose + reverse |
| 6 | Matrix traversal/encoding | "spiral", "in place" with neighbor reads | shrinking boundaries; pack two states in one cell |
| 7 | Intervals | "merge/insert/remove overlapping" | sort by start (merge) or end (greedy), then sweep |
| 8 | Canonical-key hashing | "group anagrams", "isomorphic" | normalize each item to one standard key |
| 9 | Parsing/simulation | "implement atoi", "compare versions" | spec → phases; overflow guard before multiply |
| 10 | Hash-set membership | "two sum", "longest consecutive", O(n) unsorted | complement lookup; only start runs at heads |