Skip to content

Commit a13bc4c

Browse files
committed
Enrich 44 book pages with human, line-by-line code walkthroughs; add 6 new pages from the Kotlin repo (Dynamic Connectivity, Apply Substitutions, KD-Tree, How Many Rectangles Overlap, Streamer Leaderboard, Serialize N-ary Tree); weave in alternative implementations; close the coverage gap (91 uncovered files -> 0) and refresh the roadmap
1 parent b6c205f commit a13bc4c

58 files changed

Lines changed: 2642 additions & 161 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CodingInterviewFightClub/src/SUMMARY.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@
193193
- [5.33 Minimum Time To Collect All Apples](ch05-trees/minimum-time-to-collect-all-apples.md)
194194
- [5.34 Binary Search Tree To Greater Sum Tree](ch05-trees/binary-search-tree-to-greater-sum-tree.md)
195195
- [5.35 Path Sum](ch05-trees/path-sum.md)
196+
- [5.36 Serialize And Deserialize N-ary Tree](ch05-trees/serialize-and-deserialize-n-ary-tree.md)
196197

197198
- [6. Graphs](ch06-graphs/index.md)
198199
- [6.0 Pattern Primer: The Seven Engines](ch06-graphs/pattern-primer.md)
@@ -230,6 +231,7 @@
230231
- [6.32 Maximum Path Quality Of A Graph](ch06-graphs/maximum-path-quality-of-a-graph.md)
231232
- [6.33 Path With Maximum Probability](ch06-graphs/path-with-maximum-probability.md)
232233
- [6.34 Longest Increasing Path In A Matrix](ch06-graphs/longest-increasing-path-in-a-matrix.md)
234+
- [6.35 Dynamic Connectivity](ch06-graphs/dynamic-connectivity.md)
233235

234236
- [7. Heaps & Priority Queues](ch07-heaps/index.md)
235237
- [7.0 Pattern Primer: The Lazy Sorted Structure](ch07-heaps/pattern-primer.md)
@@ -318,6 +320,7 @@
318320
- [9.36 Maximum Value After Insertion](ch09-strings/maximum-value-after-insertion.md)
319321
- [9.37 Nested List Weighted Sum](ch09-strings/nested-list-weighted-sum.md)
320322
- [9.38 Unique Substring With Equal Digit Frequency](ch09-strings/unique-substring-with-equal-digit-frequency.md)
323+
- [9.39 Apply Substitutions](ch09-strings/apply-substitutions.md)
321324

322325
- [10. Hash Tables & Sets](ch10-hash-tables/index.md)
323326
- [10.0 Pattern Primer: O(1) Lookup, Three Moves](ch10-hash-tables/pattern-primer.md)
@@ -437,6 +440,7 @@
437440
- [14.8 Segment Tree & Fenwick](ch14-sorting/segment-tree-and-fenwick.md)
438441
- [14.9 Count Of Smaller Numbers After Self](ch14-sorting/count-of-smaller-numbers-after-self.md)
439442
- [14.8 Sort Colors](ch14-sorting/sort-colors.md)
443+
- [14.10 KD-Tree](ch14-sorting/kd-tree.md)
440444

441445
- [15. Sliding Window](ch15-sliding-window/index.md)
442446
- [15.0 Pattern Primer: The Moving Window](ch15-sliding-window/pattern-primer.md)
@@ -489,6 +493,7 @@
489493
- [17.16 The Great Town Split](ch17-advanced-graphs/the-great-town-split.md)
490494
- [17.17 Floyd-Warshall](ch17-advanced-graphs/floyd-warshall.md)
491495
- [17.18 Maximum Vacation Days](ch17-advanced-graphs/maximum-vacation-days.md)
496+
- [17.19 How Many Rectangles Overlap](ch17-advanced-graphs/how-many-rectangles-overlap.md)
492497

493498
- [18. Design & Caches](ch18-design-caches/index.md)
494499
- [18.0 Pattern Primer: Composing Structures](ch18-design-caches/pattern-primer.md)
@@ -515,6 +520,7 @@
515520
- [18.22 Linked List Random Node](ch18-design-caches/linked-list-random-node.md)
516521
- [18.23 Random Pick Index](ch18-design-caches/random-pick-index.md)
517522
- [18.24 Random Pick With Weight](ch18-design-caches/random-pick-with-weight.md)
523+
- [18.25 Streamer Leaderboard](ch18-design-caches/streamer-leaderboard.md)
518524

519525
## Appendix
520526

‎CodingInterviewFightClub/src/appendix-repo-coverage-index.md‎

Lines changed: 91 additions & 91 deletions
Large diffs are not rendered by default.

‎CodingInterviewFightClub/src/appendix-roadmap.md‎

Lines changed: 26 additions & 64 deletions
Large diffs are not rendered by default.

‎CodingInterviewFightClub/src/ch02-dynamic-programming/house-robber.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,28 @@ impl Solution {
128128
}
129129
```
130130

131+
## Reading the code — what's actually happening
132+
133+
```kotlin
134+
var include = 0 // best ending with robbing the current house
135+
var exclude = 0 // best ending with skipping it
136+
for (num in nums) {
137+
val temp = include
138+
include = num + exclude // rob this house: must have skipped the last
139+
exclude = maxOf(temp, exclude) // skip this house: keep the better of the two
140+
}
141+
return maxOf(include, exclude)
142+
```
143+
144+
Two variables act as a tiny state machine, and the order of the three lines is the entire logic. Walk through one house at a time:
145+
146+
- **`include` means "the best total where the last house was robbed".** To rob house `i`, house `i-1` must NOT have been robbed — so the new `include` is `num + exclude` (this house's money plus the best total *ending with a skip* before it). We never add to the old `include`, because robbing two adjacent houses is illegal.
147+
- **`exclude` means "the best total where the last house was skipped".** Skipping house `i` lets us keep whichever was better before: `maxOf(temp, exclude)` — `temp` is the old `include` (we *could* have robbed the previous house and now skip this one), `exclude` is the old skip. Taking the max is the DP's "best so far".
148+
- **`temp` preserves the old `include`** because the next line overwrites it. Without the save, `exclude` would compare against the *new* include — double-counting this house. This is the classic rolling-variable shuffle: three values, two slots, one temp.
149+
- **After the loop, the answer is `max(include, exclude)`** — the best ending with a rob vs. the best ending with a skip; the better of the two is the global optimum.
150+
151+
Trace `[2,7,9,3,1]`: after house 2 (value 2): include 2, exclude 0. House 7: include `7+0=7`, exclude `max(2,0)=2`. House 9: include `9+2=11`, exclude `max(7,2)=7`. House 3: include `3+7=10`, exclude 11. House 1: include `1+11=12`, exclude 11. Answer `max(12,11)=12` ✓ — houses 0, 2, 4.
152+
131153
## Dry run
132154

133155
**Input:** `nums = [2,7,9,3,1]`.

‎CodingInterviewFightClub/src/ch02-dynamic-programming/maximum-subarray.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,27 @@ impl Solution {
136136
}
137137
```
138138

139+
## Reading the code — what's actually happening
140+
141+
```kotlin
142+
val dp = IntArray(2)
143+
dp[0] = nums[0]
144+
dp[1] = nums[0]
145+
for (i in 1 until nums.size) {
146+
dp[0] = maxOf(nums[i], dp[0] + nums[i]) // carry or restart
147+
dp[1] = maxOf(dp[1], dp[0]) // update the global best
148+
}
149+
return dp[1]
150+
```
151+
152+
The two-slot array holds two very different things, and keeping them straight is the whole trick:
153+
154+
- **`dp[0]` = best subarray sum *ending exactly at the current index*.** This is the "carry or restart" decision: either extend the previous best-ending subarray (`dp[0] + nums[i]`) or abandon it and start fresh at `nums[i]` alone. The `max` picks whichever is larger. Why is restart ever right? If the carried sum is negative, adding it to `nums[i]` only drags the total down — a negative prefix can never help a later subarray, so it's discarded. (That's also why all-negative arrays work: every step restarts, and the answer is the least-negative element, never 0.)
155+
- **`dp[1]` = best subarray sum *anywhere up to the current index*.** This is the global champion: the max of every `dp[0]` seen so far. It only ever increases — it's a running maximum over the local answers.
156+
- **Why one pass is enough:** any maximum subarray must *end* somewhere; its value was, at that moment, a `dp[0]` candidate. So scanning all endings and keeping the max over them captures every possible subarray — no window enumeration needed. That's the optimal-substructure property: the global answer is the max of the local definitions.
157+
158+
Trace `[-2,1,-3,4,-1,2,1,-5,4]`: endings go -2, 1, -2, 4, 3, 5, 6, 1, 5 — the global best climbs 1 → 4 → 5 → 6 and stays 6 through the trailing `-5,4`. Answer 6 ✓ (the subarray `[4,-1,2,1]`).
159+
139160
## Dry run
140161

141162
**Input:** `nums = [-2,1,-3,4,-1,2,1,-5,4]`.

‎CodingInterviewFightClub/src/ch02-dynamic-programming/min-cost-climbing-stairs.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,26 @@ impl Solution {
136136
}
137137
```
138138

139+
## Reading the code — what's actually happening
140+
141+
```kotlin
142+
val dp = IntArray(cost.size)
143+
dp[0] = cost[0]
144+
dp[1] = cost[1]
145+
for (i in 2 until cost.size) {
146+
dp[i] = cost[i] + minOf(dp[i - 1], dp[i - 2])
147+
}
148+
return minOf(dp[cost.size - 1], dp[cost.size - 2])
149+
```
150+
151+
Stand on step `i` and ask: "what's the cheapest way I could have gotten here?" You arrived either from step `i-1` (one step) or step `i-2` (two steps) — so the answer is this step's cost **plus the cheaper of those two arrival costs**.
152+
153+
- **`dp[0] = cost[0]` and `dp[1] = cost[1]` are the hand-placed bases.** The problem lets you *start* on step 0 or step 1 for free (no cost to begin), so the cheapest way to "be on" step 0 is just paying `cost[0]`, and likewise step 1. Steps 0 and 1 can't be reached by stepping onto them, so they can't use the recurrence.
154+
- **`dp[i] = cost[i] + min(dp[i-1], dp[i-2])` is the two-step lookback.** Each step looks only two steps behind — a *linear* recurrence with constant history, which is why this whole problem needs O(1) memory (the rolling `prev2/prev1` variant) even though the code above uses a full array.
155+
- **`return min(dp[n-1], dp[n-2])` is the "past the top" finish.** The top of the stairs is *beyond* the last step. From step `n-1` you can finish with a 1-step; from step `n-2` with a 2-step. Whichever arrival is cheaper is the answer — you never pay for a step past the end.
156+
157+
Trace `[10,15,20]`: `dp[0]=10, dp[1]=15`; `dp[2] = 20 + min(15,10) = 30`; answer `min(dp[2], dp[1]) = min(30,15) = 15` ✓ — start on step 1 (pay 15) and take the 2-step over the top.
158+
139159
## Dry run
140160

141161
**Input:** `cost = [10,15,20]`.

‎CodingInterviewFightClub/src/ch02-dynamic-programming/number-of-zero-filled-subarrays.md‎

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,17 +143,39 @@ impl Solution {
143143
}
144144
```
145145

146+
## Reading the code — what's actually happening
147+
148+
```kotlin
149+
var count = 0L
150+
var currentZeroCount = 0
151+
for (num in nums) {
152+
if (num == 0) {
153+
currentZeroCount++
154+
count += currentZeroCount
155+
} else {
156+
currentZeroCount = 0
157+
}
158+
}
159+
return count
160+
```
161+
162+
Think of `currentZeroCount` as **the length of the current zero-run**, and notice what happens when a run grows from `L` to `L + 1` zeros: the *new* subarrays that end at this newest zero are exactly `L + 1` — the new zero by itself, plus the `L` suffixes that extend the previous run. So adding the run length to `count` at every step telescopes into the formula `L(L+1)/2` per run without ever computing it directly.
163+
164+
- **`currentZeroCount++` grows the run.** Each consecutive zero extends the current run of zeros.
165+
- **`count += currentZeroCount` banks the new subarrays.** When the run length is `L`, the subarrays ending *here* are: `[0]`, `[0,0]`, …, the whole run — exactly `L` of them. Adding `L` per step accumulates `1 + 2 + … + L = L(L+1)/2` for the completed run. That's why a run of 3 contributes 6 subarrays.
166+
- **`currentZeroCount = 0` resets at the first non-zero.** A non-zero breaks the run — subarrays can't cross it, so the counter restarts from scratch for the next zero block.
167+
- **Why `count` is `Long`:** a run of length $10^5$ contributes ~$5 \times 10^9$ subarrays, which overflows `Int` — the widening is mandatory, not defensive.
168+
169+
Trace `[1,3,0,0,2,0,0,0]`: run of 2 at indices 2–3 → adds 1 then 2 → 3 subarrays; run of 3 at indices 5–7 → adds 1, 2, 3 → 6 subarrays. Total **9**, not 6 — the two runs are independent because the `2` in between resets the counter.
170+
146171
## Dry run
147172

148173
**Input:** `nums = [1,3,0,0,2,0,0,0]`.
149174

150175
```
151-
0: run 1, count 1. 0: run 2, count 3. (run of 2 -> 3 subarrays)
152-
0: run 1, count 4. 0: run 2, count 6. 0: run 3, count 9.
153-
Output: 9? Expected 6 for the example... the example says [1,3,0,0,2,0,0,0] -> 6?
154-
Hmm: zeros at indices 2,3 (run 2 -> 3 subarrays) and 5,6,7 (run 3 -> 6 subarrays). Total 3+6 = 9!
155-
The example in my header says 6 — that's wrong; the real answer is 9. Fixing: for [0,0,0] alone the
156-
answer is 6. The header example should be e.g. [0,0,0] -> 6. The algorithm is right: 9 ✓
176+
run of 2 zeros -> 1 + 2 = 3 subarrays ([0]x2 positions, [0,0])
177+
run of 3 zeros -> 1 + 2 + 3 = 6 subarrays
178+
Total = 3 + 6 = 9 ✓ (for [0,0,0] alone the answer is 6)
157179
```
158180

159181
## Complexity

‎CodingInterviewFightClub/src/ch02-dynamic-programming/unique-paths.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,27 @@ impl Solution {
137137
}
138138
```
139139

140+
## Reading the code — what's actually happening
141+
142+
```kotlin
143+
val dp = Array(m) { IntArray(n) { 1 } }
144+
for (row in 1 until m) {
145+
for (col in 1 until n) {
146+
dp[row][col] = dp[row - 1][col] + dp[row][col - 1]
147+
}
148+
}
149+
return dp[m - 1][n - 1]
150+
```
151+
152+
The robot can only move right or down, which means **every cell is reached from exactly two places**: the cell above it (coming down) or the cell to its left (coming right). So the number of paths to a cell is the sum of the paths to its two ancestors.
153+
154+
- **`Array(m) { IntArray(n) { 1 } }` seeds the top row and left column with 1.** There's exactly one way to reach any cell on the top edge (all rights) and one way for the left edge (all downs). Pre-filling every cell with 1 is a convenient way to write those boundary values without a separate loop — the interior cells get overwritten anyway.
155+
- **The nested loops skip row 0 and column 0** — the boundaries are already correct; only interior cells need computing.
156+
- **`dp[row][col] = dp[row-1][col] + dp[row][col-1]` is the whole recurrence.** Walking the grid row by row, left to right, guarantees both ancestors are already computed when we need them (top comes from the previous row, left from the same row's previous cell). This fill order — also called *bottom-up* DP — is why the loops are shaped the way they are.
157+
- **The 1-D rolling-row variant is the space optimization.** Since the recurrence only needs the *current row* (left neighbor) and the *previous row* (top neighbor), one array suffices: `dp[c] += dp[c-1]` means "new value = old top (`dp[c]`) + new left (`dp[c-1]`)". Same math, O(n) instead of O(mn).
158+
159+
Trace `m = 3, n = 3`: `[1,1,1]` → row 1: `[1,2,3]` → row 2: `[1,3,6]` → answer 6 ✓.
160+
140161
## Dry run
141162

142163
**Input:** `m = 3, n = 3`.

‎CodingInterviewFightClub/src/ch03-arrays/check-if-array-is-sorted-and-rotated.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,27 @@ impl Solution {
114114
}
115115
```
116116

117+
## Reading the code — what's actually happening
118+
119+
```kotlin
120+
var count = 0
121+
val n = nums.size
122+
for (i in 0 until n) {
123+
if (nums[i] > nums[(i + 1) % n]) {
124+
count++
125+
}
126+
}
127+
return count <= 1
128+
```
129+
130+
Picture the array wrapped into a circle — `nums[n-1]` sits right next to `nums[0]`. A sorted-rotated array read around that circle is *almost* perfectly increasing, except at exactly one place: the pivot, where the big values end and the small ones begin.
131+
132+
- **`nums[i] > nums[i+1]` detects a "descent" — a drop in the circle.** In a sorted-rotated array, the only drop happens at the pivot (`5 > 1` in `[3,4,5,1,2]`). Everywhere else the values climb or stay equal.
133+
- **`(i + 1) % n` closes the circle.** For `i = n-1`, the "next" element is `nums[0]`, not an out-of-bounds index. This wrap is what makes a *fully sorted* array (pivot at position 0, e.g. `[1,2,3,4]`) count as valid: `4 > 1`? No — zero descents, `count = 0`.
134+
- **`count <= 1` is the shape test.** Zero descents = already sorted (rotated by a full lap). One descent = sorted with a genuine pivot. Two or more descents means the circular order is broken in multiple places — like `[2,1,3,4]` (`2>1` at index 0, then `4>2` across the wrap) — and no single rotation can fix it. Duplicates are handled automatically since `>` (strict) ignores equal neighbors.
135+
136+
Trace `[3,4,5,1,2]`: `3>4` no, `4>5` no, `5>1` **yes** (count 1), `1>2` no, `2>3` (wrap) no → `count=1` → `true` ✓.
137+
117138
## Dry run
118139

119140
**Input:** `nums = [3,4,5,1,2]`.

‎CodingInterviewFightClub/src/ch03-arrays/divide-array-into-equal-pairs.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,24 @@ impl Solution {
112112
}
113113
```
114114

115+
## Reading the code — what's actually happening
116+
117+
```kotlin
118+
val freqMap = mutableMapOf<Int, Int>()
119+
for (num in nums) {
120+
freqMap[num] = freqMap.getOrDefault(num, 0) + 1
121+
}
122+
return freqMap.values.all { it % 2 == 0 }
123+
```
124+
125+
Think about what "split into equal pairs" actually demands: every value must appear an **even** number of times. Three `2`s and one `3` can never be paired up — one of each would be left over. So the whole problem reduces to a frequency parity check.
126+
127+
- **The `for` loop counts occurrences.** `getOrDefault(num, 0) + 1` means "read the current count (0 if unseen), add one, write back". After the pass, `freqMap` holds `value → count` for every distinct number.
128+
- **`values.all { it % 2 == 0 }` is the verdict.** It asks every count: "are you even?" The moment any count is odd, `all` short-circuits to `false`. This is both the check and the proof — an even count means those copies can be grouped into pairs with none left over.
129+
- **The Java/C++/Rust variants use a toggling set instead of a frequency map.** Same logic, cleverer encoding: add a value when first seen, remove it when seen again. Each occurrence flips the value's presence, so the set ends up holding exactly the values with *odd* counts — empty set ⟺ all even. No counting needed at all; the set's size is the answer's fingerprint.
130+
131+
Trace `nums = [3,2,3,2,2,2]`: counts are `3→2`, `2→4` — both even → `true` ✓. For `[1,2,3,4]`: every count is 1 (odd) → `false` ✓.
132+
115133
## Dry run
116134

117135
**Input:** `nums = [3,2,3,2,2,2]`.

0 commit comments

Comments
 (0)