mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-16 23:16:26 +00:00
docs(docs): add and expand algorithm documentation
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|
||||||
|
|||||||
@@ -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 ⭐
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user