docs(docs): add and expand algorithm documentation

This commit is contained in:
Prad Nukala
2026-08-20 16:04:24 -04:00
parent 34bb305a96
commit 32a41bdcb0
10 changed files with 1061 additions and 50 deletions
+107 -4
View File
@@ -3,13 +3,116 @@ title: 'Frequency Map'
description: 'Converging/parallel index walk that prunes the O(n^2) pair space using order.' description: 'Converging/parallel index walk that prunes the O(n^2) pair space using order.'
--- ---
:::warning[Under construction] ## The idea
This page is a scaffold. Notes, canonical problems, and template code are not written yet.
:::
## Concepts A frequency map is a hash table with one job: **count things**.
Key = the thing. Value = how many times you saw it.
One pass to build. O(1) to ask "how many?"
Most string and array problems fall to this question:
"Do these two collections contain the same stuff?"
Count both. Compare the counts.
## The picture
```mermaid
flowchart LR
S["'banana'"] --> C["Counter"]
C --> B["b → 1"]
C --> A["a → 3"]
C --> N["n → 2"]
style C fill:#1565c0,color:#fff
```
## The Python tool
`Counter` builds the map in one line. `defaultdict(int)` when you need to count by hand.
```python
from collections import Counter, defaultdict
count = Counter("banana") # {'a': 3, 'n': 2, 'b': 1}
count.most_common(2) # [('a', 3), ('n', 2)]
freq = defaultdict(int)
for c in "banana":
freq[c] += 1 # no KeyError, starts at 0
```
## Ransom Note (LC 383) — do I have enough letters?
Count the magazine. Spend letters as the note needs them.
Counter subtraction does this in two lines.
```python
from collections import Counter
def can_construct(ransom_note: str, magazine: str) -> bool:
need = Counter(ransom_note)
have = Counter(magazine)
return all(have[c] >= n for c, n in need.items())
```
## Top K Frequent (LC 347) — count, then rank
Two steps: count everything, then pick the k biggest counts.
```mermaid
flowchart LR
A["nums:<br/>1 1 1 2 2 3"] --> B["Count:<br/>1→3, 2→2, 3→1"]
B --> C["Buckets by count:<br/>slot 3: [1]<br/>slot 2: [2]<br/>slot 1: [3]"]
C --> D["Walk from top:<br/>[1, 2] ✓"]
style D fill:#2e7d32,color:#fff
```
Bucket sort trick: a number can appear at most n times.
So make n+1 buckets. Put each number in the bucket of its count.
Walk buckets from the top. O(n) — no sort, no heap.
```python
from collections import Counter
def top_k_frequent(nums: list[int], k: int) -> list[int]:
count = Counter(nums)
buckets = [[] for _ in range(len(nums) + 1)]
for num, freq in count.items():
buckets[freq].append(num)
result = []
for freq in range(len(buckets) - 1, 0, -1):
for num in buckets[freq]:
result.append(num)
if len(result) == k:
return result
```
## Say the trade-off out loud
Three ways to rank counts. Name them in the interview:
| Method | Time | When |
|---|---|---|
| Sort the counts | O(n log n) | Fine, simple |
| Heap of size k | O(n log k) | k small, streaming |
| Bucket sort | O(n) | Best — counts are bounded by n |
## Complexity
| | Time | Space |
|---|---|---|
| Build the map | O(n) | O(u) — unique items |
| Lookup | O(1) | — |
## Close-out ritual
Before you submit, say out loud:
1. Time and space complexity.
2. One edge case trace: empty string, k = number of unique items, or one element repeated n times.
## Plan problems
**A-set:** LC 217 · 242 · 1 · 49 · 347
**B-set:** LC 383 · 205 · 128 · 290
+105 -4
View File
@@ -3,13 +3,114 @@ title: 'Two Pointers'
description: 'Converging/parallel index walk that prunes the O(n^2) pair space using order.' description: 'Converging/parallel index walk that prunes the O(n^2) pair space using order.'
--- ---
:::warning[Under construction] ## The idea
This page is a scaffold. Notes, canonical problems, and template code are not written yet.
:::
## Concepts Checking every pair costs O(n²). That is n² boxes to open.
But if the array is **ordered**, you do not need every pair.
Put one pointer at each end. Look at the sum:
- Too small? Only a bigger left value can help. Move left in.
- Too big? Only a smaller right value can help. Move right in.
Each move kills a whole row of the pair space. O(n²) → O(n).
## The picture
```mermaid
flowchart TD
A["2 7 11 15 target 18<br/>L R"] --> B["2 + 15 = 17 < 18<br/>too small → move L right"]
B --> C["2 7 11 15<br/> L R"]
C --> D["7 + 15 = 22 > 18<br/>too big → move R left"]
D --> E["2 7 11 15<br/> L R"]
E --> F["7 + 11 = 18 ✓"]
style F fill:#2e7d32,color:#fff
```
## Not the same as binary search
Binary search **jumps** to the middle and discards half unseen — O(log n).
Two pointers **walks** — every element gets inspected once — O(n).
Both need order. Different moves.
## Two Sum II (LC 167) — the pure template
```python
def two_sum(numbers: list[int], target: int) -> list[int]:
lo, hi = 0, len(numbers) - 1
while lo < hi:
total = numbers[lo] + numbers[hi]
if total == target:
return [lo + 1, hi + 1]
if total < target:
lo += 1 # only a bigger value helps
else:
hi -= 1 # only a smaller value helps
```
## Container With Most Water (LC 11) — move the shorter wall
Water = width × shorter wall.
Moving the taller wall can never help — width shrinks, height cannot grow.
So always move the shorter one. Say this proof in the interview.
```python
def max_area(height: list[int]) -> int:
lo, hi = 0, len(height) - 1
best = 0
while lo < hi:
best = max(best, (hi - lo) * min(height[lo], height[hi]))
if height[lo] < height[hi]:
lo += 1
else:
hi -= 1
return best
```
## 3Sum (LC 15) — fix one, converge two
Sort first. Fix the smallest number. Now it is Two Sum II on the rest.
Skip duplicates at every level or you get repeat triplets.
```python
def three_sum(nums: list[int]) -> list[list[int]]:
nums.sort()
result = []
for i in range(len(nums) - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue # skip duplicate anchor
lo, hi = i + 1, len(nums) - 1
while lo < hi:
total = nums[i] + nums[lo] + nums[hi]
if total < 0:
lo += 1
elif total > 0:
hi -= 1
else:
result.append([nums[i], nums[lo], nums[hi]])
lo += 1
while lo < hi and nums[lo] == nums[lo - 1]:
lo += 1 # skip duplicate pair
return result
```
## Complexity
| | Time | Space |
|---|---|---|
| Converging pair | O(n) | O(1) |
| 3Sum | O(n²) — sort + n passes | O(1) extra |
## Close-out ritual
Before you submit, say out loud:
1. Time and space complexity.
2. One edge case trace: two elements, all duplicates, or no valid answer.
## Plan problems
**A-set:** LC 125 · 167 · 15 · 11 · 42
**B-set:** LC 392 · 977 · 189 · 80
+113 -3
View File
@@ -3,7 +3,117 @@ title: 'Prefix Sum'
description: 'Precomputed cumulative state for O(1) range queries.' description: 'Precomputed cumulative state for O(1) range queries.'
--- ---
:::warning[Under construction] ## The idea
This page is a scaffold. Notes, canonical problems, and template code are not written yet.
:::
Precompute running totals once. Then any range sum costs O(1).
Like mile markers on a highway.
Distance from mile 30 to mile 80? Subtract: 80 30 = 50.
You do not re-drive the road.
**The formula:** `sum(i..j) = prefix[j + 1] - prefix[i]`
## The picture
```mermaid
flowchart TD
A["nums: 3 1 4 1 5"] --> B["prefix: 0 3 4 8 9 14"]
B --> C["sum(1..3) = prefix[4] prefix[1]<br/>= 9 3 = 6"]
C --> D["Check: 1 + 4 + 1 = 6 ✓"]
style D fill:#2e7d32,color:#fff
```
The leading 0 matters. It makes ranges that start at index 0 work with no special case.
## Range Sum Query (LC 303)
Pay O(n) once at build time. Answer every query in O(1).
```python
from itertools import accumulate
class NumArray:
def __init__(self, nums: list[int]):
self.prefix = [0] + list(accumulate(nums))
def sum_range(self, left: int, right: int) -> int:
return self.prefix[right + 1] - self.prefix[left]
```
## Prefix + Hashmap (LC 560 — Subarray Sum Equals K)
The signature trick of this topic. The question flips:
"Which subarrays sum to k?" becomes
"At each point, how many *earlier* prefixes equal `current k`?"
Because: if `prefix[j] prefix[i] = k`, the slice between them sums to k.
```mermaid
flowchart LR
A["Walk the array,<br/>carry running sum"] --> B["Ask the map:<br/>seen sum k before?"]
B --> C["Yes, m times →<br/>add m to answer"]
B --> D["Record current sum<br/>in the map"]
D --> A
style C fill:#2e7d32,color:#fff
```
```python
from collections import defaultdict
def subarray_sum(nums: list[int], k: int) -> int:
count = 0
current = 0
seen = defaultdict(int)
seen[0] = 1 # empty prefix — subarrays that start at 0
for n in nums:
current += n
count += seen[current - k] # ask first
seen[current] += 1 # record after
return count
```
**Order matters.** Ask before you record, or a subarray of length 0 counts itself when k = 0.
## Product variant (LC 238 — Product of Array Except Self)
Same idea, two directions. Prefix products from the left, suffix products from the right.
`answer[i] = left[i] × right[i]` — everything except i.
```python
def product_except_self(nums: list[int]) -> list[int]:
n = len(nums)
result = [1] * n
left = 1
for i in range(n):
result[i] = left
left *= nums[i]
right = 1
for i in range(n - 1, -1, -1):
result[i] *= right
right *= nums[i]
return result
```
## Complexity
| | Time | Space |
|---|---|---|
| Build | O(n) | O(n) |
| Each range query | O(1) | — |
| Prefix + hashmap | O(n) one pass | O(n) |
## Close-out ritual
Before you submit, say out loud:
1. Time and space complexity.
2. One edge case trace: range starting at 0, k = 0 with zeros in the array, or negative numbers (this is why sliding window fails and prefix + hashmap wins).
## Plan problems
**A-set:** LC 303 · 238 · 560
**B-set:** LC 525 · 974
+108 -3
View File
@@ -3,10 +3,115 @@ title: 'Sliding Window'
description: 'Two pointers plus incremental state between them.' description: 'Two pointers plus incremental state between them.'
--- ---
:::warning[Under construction] ## The idea
This page is a scaffold. Notes, canonical problems, and template code are not written yet.
:::
Two pointers that move the **same direction**, carrying state between them.
Think of a caterpillar:
- The head crawls forward and eats (expand right).
- When the rule breaks, the tail pulls in until the rule holds again (contract left).
The window is always a **contiguous** slice. You never rebuild it — you update the carried state as edges move. That is what makes it O(n).
**The recipe:**
1. Expand right. Add the new element to your state.
2. Rule broken? Contract left until it holds.
3. Record the best window. Repeat.
## The picture
```mermaid
flowchart TD
A["a b c a b c<br/>[a] window = a"] --> B["[a b] expand → ab"]
B --> C["[a b c] expand → abc, best = 3"]
C --> D["a [b c a] 'a' repeats → contract, then expand"]
D --> E["a b [c a b] keep sliding, best stays 3"]
style C fill:#2e7d32,color:#fff
```
## Longest Substring Without Repeating (LC 3)
State = a set of chars in the window. Repeat found? Shrink from the left until it is gone.
```python
def length_of_longest_substring(s: str) -> int:
window = set()
left = 0
best = 0
for right, c in enumerate(s):
while c in window: # rule broken
window.remove(s[left]) # contract left
left += 1
window.add(c) # expand right
best = max(best, right - left + 1)
return best
```
## Best Time to Buy and Sell (LC 121) — the hidden window
Looks like a stock problem. It is a window problem.
Left = cheapest buy so far. Right = today. Carry one number: the min price.
```python
def max_profit(prices: list[int]) -> int:
min_price = prices[0]
best = 0
for p in prices[1:]:
best = max(best, p - min_price)
min_price = min(min_price, p)
return best
```
## Character Replacement (LC 424) — window with a budget
Rule: window is valid if `window size count of top letter ≤ k`.
That many replacements fix the window. Carry a frequency map.
```python
from collections import defaultdict
def character_replacement(s: str, k: int) -> int:
count = defaultdict(int)
left = 0
best = 0
top = 0 # highest letter count seen
for right, c in enumerate(s):
count[c] += 1
top = max(top, count[c])
if (right - left + 1) - top > k: # over budget
count[s[left]] -= 1 # contract exactly one step
left += 1
best = max(best, right - left + 1)
return best
```
## When the window fails
Sliding window needs a one-way rule: **growing can only hurt, shrinking can only help.**
Negative numbers break this — a bigger window can flip from bad to good.
That is when you reach for prefix sum + hashmap (LC 560) instead.
Say this decision out loud in the interview.
## Complexity
| | Time | Space |
|---|---|---|
| All patterns above | O(n) — each index enters and leaves once | O(1) or O(alphabet) |
## Close-out ritual
Before you submit, say out loud:
1. Time and space complexity — note each element enters and leaves the window at most once.
2. One edge case trace: empty string, all same char, or k larger than the string.
## Plan problems
**A-set:** LC 121 · 3 · 424 · 567 · 76
**B-set:** LC 209 · 1004 · 643
<YouTube id="QGNAVBn1_bc" title="Sliding Window" /> <YouTube id="QGNAVBn1_bc" title="Sliding Window" />
+86 -18
View File
@@ -3,30 +3,98 @@ title: 'Binary Search'
description: 'In algorithms, arrays and strings are very similar. They are both ordered collections of elements. The difference is that arrays are mutable, while strings are immutable.' description: 'In algorithms, arrays and strings are very similar. They are both ordered collections of elements. The difference is that arrays are mutable, while strings are immutable.'
--- ---
## The idea
Technically, an array can't be resized. A dynamic array, or list, can be. In the context of algorithm problems, usually when people talk about arrays, they are referring to dynamic arrays. In this entire course, we will be talking about dynamic arrays/lists, but we will just use the word "array". You look for a name in a phone book. You do not start at page 1.
You open the middle. Too far? Go left. Not far enough? Go right.
Each step throws away half the book.
## Time Complexity 100 items → 7 steps. 4 billion items → 32 steps.
Arrays and strings are both ordered collections of elements. The difference is that arrays are mutable, while strings are immutable. **The core rule:** ask a yes/no question that flips exactly once.
Everything left of the flip is "no". Everything right is "yes".
Binary search finds the flip point.
| Operation | Array/List | String(Immutable) | ## The picture
| --------- | ---------- | ----------------- |
| Appending to end | *O(1) | O(n) |
| Popping from end | O(1) | O(n) |
| Insertion, not from end | O(n) | O(n) |
| Deletion, not from end | O(n) | O(n) |
| Modifying an element | O(1) | O(n) |
| Random access | *O(1) | O(n) |
| Checking if an element exists | O(1) | O(n) |
:::note[Clarification 1] ```mermaid
Appending to the end of a list is amortized O(1) time complexity. This means that the time complexity of appending to the end of a list is the same as the time complexity of appending to the end of an array. flowchart TD
::: A["Array: 1 3 5 7 9 11 13<br/>Target: 9"] --> B["mid = 7<br/>7 < 9 → discard left half"]
B --> C["Array: 9 11 13<br/>mid = 11<br/>11 > 9 → discard right half"]
C --> D["Array: 9<br/>mid = 9<br/>Found ✓"]
:::note[Clarification 2] style D fill:#2e7d32,color:#fff
Random access in this context means that you can access an element at any index in constant time. ```
:::
## The template
```python
def binary_search(nums: list[int], target: int) -> int:
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = (lo + hi) // 2
if nums[mid] == target:
return mid
if nums[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1
```
## Search the answer space (LC 875 — Koko Eating Bananas)
The array does not need to exist. You can binary search over *possible answers*.
Ask: "does speed k work?" Slow speeds fail. Fast speeds work.
The answer flips once. Find the flip.
```mermaid
flowchart LR
A["k=1<br/>NO"] --> B["k=2<br/>NO"] --> C["k=3<br/>NO"] --> D["k=4<br/>YES ← answer"] --> E["k=5<br/>YES"] --> F["k=6<br/>YES"]
style D fill:#2e7d32,color:#fff
```
```python
import math
def min_eating_speed(piles: list[int], h: int) -> int:
def k_works(k: int) -> bool:
return sum(math.ceil(p / k) for p in piles) <= h
lo, hi = 1, max(piles)
while lo < hi:
mid = (lo + hi) // 2
if k_works(mid):
hi = mid # mid works — keep it, look left
else:
lo = mid + 1 # mid fails — discard it
return lo
```
## Not the same as two pointers
Two pointers **walks** — it inspects every element it passes. O(n).
Binary search **jumps** — it inspects one midpoint and discards half unseen. O(log n).
## Complexity
| | Time | Space |
|---|---|---|
| Sorted array | O(log n) | O(1) |
| Answer space | O(n log m) | O(1) |
n = array size, m = answer range.
## Close-out ritual
Before you submit, say out loud:
1. Time and space complexity.
2. One edge case trace: empty array, one element, or target not present.
## Plan problems
**A-set:** LC 704 · 74 · 875 · 33 · 153
**B-set:** LC 35 · 162 · 34 · 4 ⭐
+121
View File
@@ -0,0 +1,121 @@
---
title: Breadth-First Search
---
# Breadth-First Search (BFS)
## The idea
You want a mango seller. You ask your friends first.
No luck? You ask friends-of-friends. Then their friends.
You search in **rings**, closest first.
That is BFS. It answers two questions:
1. **Is there a path** from A to B?
2. **What is the shortest path?** (unweighted — fewest hops)
BFS is *always* shortest path on unweighted graphs. First time you reach a node = fewest possible steps.
## The picture
```mermaid
flowchart TD
YOU((You)) --> A((Alice))
YOU --> B((Bob))
A --> C((Claire))
A --> D((Dan))
B --> E((Eve))
E --> M((Mango seller ✓))
style YOU fill:#1565c0,color:#fff
style A fill:#6a1b9a,color:#fff
style B fill:#6a1b9a,color:#fff
style C fill:#ef6c00,color:#fff
style D fill:#ef6c00,color:#fff
style E fill:#ef6c00,color:#fff
style M fill:#2e7d32,color:#fff
```
Blue = start. Purple = ring 1. Orange = ring 2. Green = found at ring 3.
## The two tools
1. **Queue** — first in, first out. This keeps the rings in order.
2. **Visited set** — never check the same node twice. Without it, cycles loop forever.
## The template
```python
from collections import deque
def bfs(start, graph: dict) -> None:
queue = deque([start])
visited = {start}
while queue:
level_size = len(queue) # snapshot = one full ring
for _ in range(level_size):
node = queue.popleft()
# process node here
for nxt in graph.get(node, []):
if nxt not in visited:
visited.add(nxt) # mark WHEN queued, not when popped
queue.append(nxt)
```
## Multi-source BFS (LC 994 — Rotting Oranges)
Start with *all* rotten oranges in the queue at once.
Each ring = one minute of rot spreading.
```python
from collections import deque
def oranges_rotting(grid: list[list[int]]) -> int:
rows, cols = len(grid), len(grid[0])
queue = deque()
fresh = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == 2:
queue.append((r, c))
elif grid[r][c] == 1:
fresh += 1
minutes = 0
dirs = [(1, 0), (-1, 0), (0, 1), (0, -1)]
while queue and fresh > 0:
for _ in range(len(queue)):
r, c = queue.popleft()
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
grid[nr][nc] = 2
fresh -= 1
queue.append((nr, nc))
minutes += 1
return minutes if fresh == 0 else -1
```
## Complexity
| | Time | Space |
|---|---|---|
| BFS | O(V + E) | O(V) |
V = nodes, E = edges. On a grid: O(rows × cols).
## Close-out ritual
Before you submit, say out loud:
1. Time and space complexity.
2. One edge case trace: empty graph, start = target, or unreachable target.
## Plan problems
**Tree BFS A-set:** LC 102 · 199 · 1448
**Graph BFS A-set:** LC 994 · 127
**B-set:** LC 103 · 637 · 909 · 433 · 286
+110
View File
@@ -0,0 +1,110 @@
---
title: Depth-First Search
---
# Depth-First Search (DFS)
## The idea
BFS searches in rings. DFS picks one path and follows it to the end.
Dead end? Back up one step. Try the next branch.
Like exploring a maze with one hand on the wall.
Use DFS when:
- The answer **composes from subtrees** (depth, path sums, subtree checks).
- You need **all** of a region (islands, flood fill, connected components).
Use BFS when you need **shortest path**. DFS does not give shortest path.
## The picture
```mermaid
flowchart TD
A((1)) --> B((2))
A --> C((5))
B --> D((3))
B --> E((4))
C --> F((6))
style A fill:#1565c0,color:#fff
style B fill:#1565c0,color:#fff
style D fill:#1565c0,color:#fff
```
Visit order: 1 → 2 → 3 (bottom!) → back up → 4 → back up → 5 → 6.
Numbers show the order. DFS goes **deep before wide**.
## Tree DFS — answers flow up (LC 104 — Max Depth)
The pattern: ask each child a question. Combine the answers. Return up.
```python
def max_depth(root) -> int:
if root is None:
return 0 # base case
left = max_depth(root.left)
right = max_depth(root.right)
return 1 + max(left, right) # combine + return up
```
```mermaid
flowchart BT
D["leaf returns 1"] --> B["returns 1 + max(1,1) = 2"]
E["leaf returns 1"] --> B
B --> A["root returns 1 + max(2,1) = 3"]
C["leaf returns 1"] --> A
style A fill:#2e7d32,color:#fff
```
## Graph DFS — add a visited set (LC 200 — Number of Islands)
Graphs have cycles. Trees do not.
So graph DFS needs one new thing: **mark where you have been.**
On a grid, sink the land as you visit it — the grid *is* the visited set.
```python
def num_islands(grid: list[list[str]]) -> int:
rows, cols = len(grid), len(grid[0])
count = 0
def sink(r: int, c: int) -> None:
if r < 0 or r >= rows or c < 0 or c >= cols:
return
if grid[r][c] != "1":
return
grid[r][c] = "0" # mark visited
sink(r + 1, c)
sink(r - 1, c)
sink(r, c + 1)
sink(r, c - 1)
for r in range(rows):
for c in range(cols):
if grid[r][c] == "1":
count += 1
sink(r, c) # eat the whole island
return count
```
## Complexity
| | Time | Space |
|---|---|---|
| Tree DFS | O(n) | O(h) — call stack, h = height |
| Graph DFS | O(V + E) | O(V) |
Worst case h = n (a stick-shaped tree).
## Close-out ritual
Before you submit, say out loud:
1. Time and space complexity — name the call stack.
2. One edge case trace: null root, single node, or all-water grid.
## Plan problems
**Tree DFS A-set:** LC 226 · 104 · 100 · 572 · 236 · 124 ⭐
**Graph DFS A-set:** LC 200 · 695 · 133 · 417 · 130
**B-set:** LC 101 · 112 · 129 · 543 · 399 · 261
+127
View File
@@ -0,0 +1,127 @@
---
title: Dynamic Programming (1-D)
---
# Dynamic Programming (1-D)
## The idea
DP is backtracking with a memory.
Backtracking tries every path. Many paths repeat the same subproblem.
DP solves each subproblem **once**, saves the answer, and reuses it.
Grokking's rule: break the big problem into small problems.
Solve the small ones first. Build up.
**The two things you must find:**
1. **The state** — what does `dp[i]` mean, in one sentence?
2. **The recurrence** — how does `dp[i]` come from earlier answers?
## The picture — why memo matters (Climbing Stairs)
Without memo, `f(5)` computes `f(3)` twice and `f(2)` three times:
```mermaid
flowchart TD
A["f(5)"] --> B["f(4)"]
A --> C["f(3)"]
B --> D["f(3)"]
B --> E["f(2)"]
C --> F["f(2)"]
C --> G["f(1)"]
D --> H["f(2)"]
D --> I["f(1)"]
style C fill:#c62828,color:#fff
style D fill:#c62828,color:#fff
style E fill:#ef6c00,color:#fff
style F fill:#ef6c00,color:#fff
style H fill:#ef6c00,color:#fff
```
Red and orange = repeated work. Memo turns the tree into a straight line: O(2ⁿ) → O(n).
## Climbing Stairs (LC 70)
State: `dp[i]` = ways to reach step i.
Recurrence: you arrive from one step below or two below.
```python
def climb_stairs(n: int) -> int:
if n <= 2:
return n
prev2, prev1 = 1, 2
for _ in range(3, n + 1):
prev2, prev1 = prev1, prev1 + prev2
return prev1
```
## House Robber (LC 198)
State: `dp[i]` = max loot using houses 0..i.
Recurrence at each house: **rob it** (skip the neighbor) or **skip it**.
```mermaid
flowchart LR
A["House i"] --> B["Rob:<br/>nums[i] + dp[i-2]"]
A --> C["Skip:<br/>dp[i-1]"]
B --> D["dp[i] = max of both"]
C --> D
style D fill:#2e7d32,color:#fff
```
```python
def rob(nums: list[int]) -> int:
skip = take = 0
for n in nums:
skip, take = max(skip, take), skip + n # skip it / rob it
return max(skip, take)
```
## Coin Change (LC 322)
State: `dp[a]` = fewest coins to make amount a.
Recurrence: try each coin, take the best.
```python
import math
def coin_change(coins: list[int], amount: int) -> int:
dp = [math.inf] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a:
dp[a] = min(dp[a], dp[a - c] + 1)
return dp[amount] if dp[amount] != math.inf else -1
```
## The interview script
Say these four lines out loud, in order:
1. "The brute force is backtracking — try everything."
2. "Subproblems overlap, so I will memoize."
3. "State: dp[i] means ___." (one sentence)
4. "Recurrence: dp[i] = ___."
## Complexity
| | Time | Space |
|---|---|---|
| Climbing Stairs / House Robber | O(n) | O(1) with rolling vars |
| Coin Change | O(amount × coins) | O(amount) |
## Close-out ritual
Before you submit, say out loud:
1. Time and space complexity.
2. One edge case trace: n = 0 or 1, empty array, or unreachable amount (return -1).
## Plan problems
**A-set:** LC 70 · 746 · 198 · 213 · 322 · 300
**B-set:** LC 139 · 91 · 647
Note: DP matters only if Google or Databricks advance to later rounds. Your fintech targets skew toward simulation, hashmap, and heap.
+95
View File
@@ -0,0 +1,95 @@
---
title: Greedy Algorithms
---
# Greedy Algorithms
## The idea
At each step, take the best move *right now*. Never look back.
Grokking's example: the classroom problem.
You want the most classes in one room. Which do you pick?
**Always pick the class that ends first.** It leaves the most room for the rest.
Greedy works only when the local best is *provably* the global best.
When it works, it beats DP — no memo table, no lookback, often O(n).
## The picture
```mermaid
gantt
dateFormat HH:mm
axisFormat %H:%M
section Pick ✓
Art (ends first) :done, 09:00, 45m
Math (ends next) :done, 10:00, 60m
Music :done, 11:00, 60m
section Skip ✗
English (overlaps Art) :crit, 09:30, 60m
CS (overlaps Math) :crit, 10:30, 60m
```
Pick by earliest end time. Skip anything that overlaps a pick.
## Kadane's — DP squeezed to one variable (LC 53 — Maximum Subarray)
The greedy question at each element:
"Do I extend the running sum, or start fresh here?"
If the running sum is negative, it only hurts. Drop it.
```python
def max_sub_array(nums: list[int]) -> int:
best = current = nums[0]
for n in nums[1:]:
current = max(n, current + n) # extend or restart
best = max(best, current)
return best
```
```mermaid
flowchart LR
A["-2"] --> B["1<br/>restart"] --> C["-2<br/>extend"] --> D["4<br/>restart"] --> E["3<br/>extend"] --> F["5<br/>extend"] --> G["6<br/>extend ← best"]
style G fill:#2e7d32,color:#fff
```
## Jump Game (LC 55) — track the farthest reach
One pass. Keep the farthest index you can touch.
If your position ever passes the reach, you are stuck.
```python
def can_jump(nums: list[int]) -> bool:
reach = 0
for i, n in enumerate(nums):
if i > reach:
return False # stuck
reach = max(reach, i + n)
return True
```
## How to justify greedy in an interview
Use an **exchange argument**: "If an optimal answer made a different choice here, I could swap in my greedy choice without making it worse."
Say this out loud. It is the difference between guessing and proving.
## Complexity
| | Time | Space |
|---|---|---|
| One-pass greedy (Kadane, Jump) | O(n) | O(1) |
| Sort-then-commit (intervals) | O(n log n) | O(1) |
## Close-out ritual
Before you submit, say out loud:
1. Time and space complexity.
2. One edge case trace: all-negative array, single element, or zero at index 0.
## Plan problems
**A-set:** LC 53 · 55 · 45 · 134
**B-set:** LC 122 · 918 · 763
**Feeds into topic 24 (Intervals):** LC 57 · 56 · 435 · 253
+89 -18
View File
@@ -3,28 +3,99 @@ title: 'Hash Tables'
description: 'In algorithms, arrays and strings are very similar. They are both ordered collections of elements. The difference is that arrays are mutable, while strings are immutable.' description: 'In algorithms, arrays and strings are very similar. They are both ordered collections of elements. The difference is that arrays are mutable, while strings are immutable.'
--- ---
## The idea
Technically, an array can't be resized. A dynamic array, or list, can be. In the context of algorithm problems, usually when people talk about arrays, they are referring to dynamic arrays. In this entire course, we will be talking about dynamic arrays/lists, but we will just use the word "array". A hash table is like a helpful grocery clerk.
You say "avocado". She tells you the price at once.
She does not walk the aisles. She just *knows*.
## Time Complexity How? A hash function turns your key into a slot number.
Same key → same slot, every time. Lookup is O(1).
Arrays and strings are both ordered collections of elements. The difference is that arrays are mutable, while strings are immutable. ## The picture
| Operation | Array/List | String(Immutable) | ```mermaid
| --------- | ---------- | ----------------- | flowchart LR
| Appending to end | *O(1) | O(n) | K1["'apple'"] --> H["Hash<br/>function"]
| Popping from end | O(1) | O(n) | K2["'milk'"] --> H
| Insertion, not from end | O(n) | O(n) | K3["'avocado'"] --> H
| Deletion, not from end | O(n) | O(n) | H --> S0["Slot 0: milk → 1.49"]
| Modifying an element | O(1) | O(n) | H --> S1["Slot 1: apple → 0.67"]
| Random access | *O(1) | O(n) | H --> S2["Slot 2: avocado → 1.99"]
| Checking if an element exists | O(1) | O(n) |
:::note[Clarification 1] style H fill:#1565c0,color:#fff
Appending to the end of a list is amortized O(1) time complexity. This means that the time complexity of appending to the end of a list is the same as the time complexity of appending to the end of an array. ```
:::
:::note[Clarification 2] ## Three jobs it does
Random access in this context means that you can access an element at any index in constant time.
:::
1. **Membership** — "have I seen this before?"
2. **Frequency** — "how many times?"
3. **Mapping** — "what goes with this?"
## Membership (LC 217 — Contains Duplicate)
```python
def contains_duplicate(nums: list[int]) -> bool:
seen = set()
for n in nums:
if n in seen:
return True
seen.add(n)
return False
```
## Frequency (LC 242 — Valid Anagram)
```python
from collections import Counter
def is_anagram(s: str, t: str) -> bool:
if len(s) != len(t):
return False
return Counter(s) == Counter(t)
```
## Key design (LC 49 — Group Anagrams)
Sometimes the trick is *what you use as the key*.
"eat", "tea", "ate" all sort to "aet". Sorted string = group key.
```mermaid
flowchart LR
A["eat"] --> K["key: 'aet'"]
B["tea"] --> K
C["ate"] --> K
K --> G["group: [eat, tea, ate]"]
style K fill:#1565c0,color:#fff
```
```python
from collections import defaultdict
def group_anagrams(strs: list[str]) -> list[list[str]]:
groups = defaultdict(list)
for s in strs:
key = "".join(sorted(s))
groups[key].append(s)
return list(groups.values())
```
## Complexity
| | Average | Worst |
|---|---|---|
| Lookup / insert / delete | O(1) | O(n) |
Worst case comes from collisions. Interviews treat it as O(1).
## Close-out ritual
Before you submit, say out loud:
1. Time and space complexity.
2. One edge case trace: empty input, all same element, or key collision on your key design.
## Plan problems
**A-set:** LC 217 · 242 · 1 · 49 · 347
**B-set:** LC 383 · 205 · 128 · 290