diff --git a/islands/StudyDeck.tsx b/islands/StudyDeck.tsx new file mode 100644 index 0000000..c3068f9 --- /dev/null +++ b/islands/StudyDeck.tsx @@ -0,0 +1,46 @@ +// reveal.js touches window/document at init — render on the client only. +export const client = "only"; + +import { Deck, Markdown } from "@revealjs/react"; +import RevealHighlight from "reveal.js/plugin/highlight"; +import "reveal.js/reveal.css"; +import "reveal.js/theme/black.css"; +import "reveal.js/plugin/highlight/monokai.css"; + +import { decks, type DeckName } from "./decks"; + +/** + * An embedded reveal.js slide deck for drilling a DS/algorithms topic. + * Usage in any MDX page: ``. + */ +export default function StudyDeck({ deck }: { deck: DeckName }) { + const markdown = decks[deck]; + if (!markdown) { + return ( +

+ Unknown study deck "{deck}". Available: {Object.keys(decks).join(", ")}. +

+ ); + } + return ( +
+ + {markdown} + +
+ ); +} diff --git a/islands/decks/arrays-and-strings.md b/islands/decks/arrays-and-strings.md new file mode 100644 index 0000000..a4184a7 --- /dev/null +++ b/islands/decks/arrays-and-strings.md @@ -0,0 +1,56 @@ +## Arrays & Strings + +Ordered collections. Arrays are **mutable**, strings are **immutable** — every "edit" to a string builds a new one. + +--- + +## Cost model + +| Operation | Array | String | +| --- | --- | --- | +| Append / pop at end | O(1)* | O(n) | +| Insert / delete in middle | O(n) | O(n) | +| Random access | O(1) | O(1) | +| Membership check | O(n) | O(n) | + +*amortized — occasional resize, constant on average. + +--- + +## Recognition + +- Contiguous data + index arithmetic → array patterns apply +- Building a string piece by piece → collect parts, join once +- "In-place" requirement → think swap / overwrite, not rebuild + +--- + +## String building + +```js +// O(n^2): each += copies the whole string +let s = ""; +for (const ch of parts) s += ch; + +// O(n): buffer then join +const buf = []; +for (const ch of parts) buf.push(ch); +const out = buf.join(""); +``` + +--- + +## Recall + +- Why is append amortized O(1) and not plain O(1)? +- What does immutability cost when you "modify" one character? +- Which patterns sit on top of arrays? (two pointers, sliding window, prefix sum) + +--- + +## Drill + +- 1480 Running Sum of 1d Array +- 217 Contains Duplicate +- 189 Rotate Array +- 977 Squares of a Sorted Array diff --git a/islands/decks/binary-search.md b/islands/decks/binary-search.md new file mode 100644 index 0000000..22d0d81 --- /dev/null +++ b/islands/decks/binary-search.md @@ -0,0 +1,63 @@ +## Binary Search + +Halve a **monotonic** search space each step → O(log n). + +--- + +## Recognition + +- Sorted input + "find / insert position" +- "Minimize the maximum" / "first value where condition flips" +- Answer itself is numeric and checkable → binary search **on the answer** + +--- + +## Template + +```js +function search(nums, target) { + let lo = 0; + let hi = nums.length - 1; + while (lo <= hi) { + const mid = lo + ((hi - lo) >> 1); + if (nums[mid] === target) return mid; + if (nums[mid] < target) lo = mid + 1; + else hi = mid - 1; + } + return -1; // lo is the insert position +} +``` + +--- + +## First-true boundary + +```js +// smallest index where ok(i) is true; ok is false...false true...true +let lo = 0; +let hi = n; // exclusive; n means "never true" +while (lo < hi) { + const mid = lo + ((hi - lo) >> 1); + if (ok(mid)) hi = mid; + else lo = mid + 1; +} +return lo; +``` + +Most "hard" binary searches are this shape in disguise. + +--- + +## Pitfalls + +- Off-by-one: pick `lo <= hi` **or** `lo < hi` and stay consistent +- `mid = lo + ((hi - lo) >> 1)` avoids overflow and floats +- No progress → infinite loop: every branch must shrink the range + +--- + +## Recall + +- What property must the search space have? (not "sorted" — *monotonic*) +- Where does `lo` land when the target is absent? +- When do you search the answer space instead of the array? diff --git a/islands/decks/hashing.md b/islands/decks/hashing.md new file mode 100644 index 0000000..92f4c68 --- /dev/null +++ b/islands/decks/hashing.md @@ -0,0 +1,72 @@ +## Hashing + +Trade O(n) memory for **O(1) average lookup**. The workhorse behind "have I seen this before?". + +--- + +## Recognition + +- "Does X exist?" / "How many times?" → `Set` / `Map` +- Pair matching (complement lookup) → map value → index +- Grouping by a derived key → map key → bucket +- Counting characters → frequency map + +--- + +## Template — complement lookup (LC 1) + +```js +function twoSum(nums, target) { + const seen = new Map(); // value -> index + for (let i = 0; i < nums.length; i++) { + const need = target - nums[i]; + if (seen.has(need)) return [seen.get(need), i]; + seen.set(nums[i], i); + } +} +``` + +One pass: check for the partner **before** inserting yourself. + +--- + +## Template — grouping (LC 49) + +```js +function groupAnagrams(strs) { + const groups = new Map(); + for (const s of strs) { + const key = [...s].sort().join(""); + if (!groups.has(key)) groups.set(key, []); + groups.get(key).push(s); + } + return [...groups.values()]; +} +``` + +The whole trick is **choosing the canonical key**. + +--- + +## Pitfalls + +- Frequency compare needs both directions (surplus *and* deficit) +- Object keys coerce to strings — use `Map` for numbers +- Hashing is O(1) *average*; keys must be cheap to compute + +--- + +## Recall + +- Two Sum: why insert after the lookup, not before? +- Group Anagrams: name two valid canonical keys +- LC 128 Longest Consecutive: why check `!set.has(x - 1)` first? + +--- + +## Drill + +- 217 Contains Duplicate · 242 Valid Anagram +- 1 Two Sum · 49 Group Anagrams +- 347 Top K Frequent · 383 Ransom Note +- 205 Isomorphic Strings · 128 Longest Consecutive · 290 Word Pattern diff --git a/islands/decks/index.ts b/islands/decks/index.ts new file mode 100644 index 0000000..fecbee6 --- /dev/null +++ b/islands/decks/index.ts @@ -0,0 +1,26 @@ +/** + * Markdown study decks rendered by the StudyDeck island (reveal.js). + * + * Each deck is a plain markdown file in this directory — edit the `.md` + * file to change its slides. Slides split on `---` lines; lists animate + * in one item at a time (see `animateLists` in StudyDeck.tsx). Problem + * numbers reference the curriculum in README.md and the solutions under + * `work/`. + */ +import arraysAndStrings from "./arrays-and-strings.md?raw"; +import binarySearch from "./binary-search.md?raw"; +import hashing from "./hashing.md?raw"; +import prefixSum from "./prefix-sum.md?raw"; +import slidingWindow from "./sliding-window.md?raw"; +import twoPointers from "./two-pointers.md?raw"; + +export const decks = { + "arrays-and-strings": arraysAndStrings, + "binary-search": binarySearch, + hashing, + "prefix-sum": prefixSum, + "sliding-window": slidingWindow, + "two-pointers": twoPointers, +} as const; + +export type DeckName = keyof typeof decks; diff --git a/islands/decks/prefix-sum.md b/islands/decks/prefix-sum.md new file mode 100644 index 0000000..e0ba462 --- /dev/null +++ b/islands/decks/prefix-sum.md @@ -0,0 +1,71 @@ +## Prefix Sum + +Precompute cumulative totals once → any range sum in **O(1)**. + +--- + +## Recognition + +- Many range-sum queries over a static array +- "Count subarrays with sum k" (negatives allowed — window won't work) +- "Product/sum of everything except me" → prefix from both directions + +--- + +## Template — range query (LC 303) + +```js +// prefix[i] = sum of nums[0..i-1], prefix[0] = 0 +const prefix = [0]; +for (const x of nums) prefix.push(prefix[prefix.length - 1] + x); + +// sum of nums[l..r] inclusive: +const rangeSum = (l, r) => prefix[r + 1] - prefix[l]; +``` + +The leading 0 removes every edge case at `l = 0`. + +--- + +## Template — prefix + hashmap (LC 560) + +```js +function subarraySum(nums, k) { + const count = new Map([[0, 1]]); // empty prefix + let sum = 0; + let total = 0; + for (const x of nums) { + sum += x; + total += count.get(sum - k) ?? 0; // earlier prefix to cut off + count.set(sum, (count.get(sum) ?? 0) + 1); + } + return total; +} +``` + +sum(l..r) = prefix[r] − prefix[l−1] — so look up `sum - k`. + +--- + +## Pitfalls + +- Forgetting the seed `(0, 1)` misses subarrays starting at index 0 +- Count **before** inserting the current prefix (a subarray has length ≥ 1) +- Sliding window fails here when values can be negative + +--- + +## Recall + +- Why does LC 560 need a hashmap instead of a window? +- LC 238 Product Except Self: how do prefix and suffix passes combine? +- LC 525 Contiguous Array: what do you turn 0s into, and why? + +--- + +## Drill + +- 303 Range Sum Query · 1480 Running Sum +- 238 Product of Array Except Self +- 560 Subarray Sum Equals K +- 525 Contiguous Array · 974 Subarray Sums Divisible by K diff --git a/islands/decks/raw.d.ts b/islands/decks/raw.d.ts new file mode 100644 index 0000000..1fa5881 --- /dev/null +++ b/islands/decks/raw.d.ts @@ -0,0 +1,5 @@ +// Vite `?raw` imports resolve to the file's text content. +declare module "*.md?raw" { + const content: string; + export default content; +} diff --git a/islands/decks/sliding-window.md b/islands/decks/sliding-window.md new file mode 100644 index 0000000..669748b --- /dev/null +++ b/islands/decks/sliding-window.md @@ -0,0 +1,68 @@ +## Sliding Window + +Two pointers **plus incremental state** between them. Never recompute the window from scratch. + +--- + +## Recognition + +- "Longest / shortest **contiguous** subarray or substring such that…" +- Constraint is monotone: growing the window can only make it worse +- Fixed window size k → same idea, both ends move together + +--- + +## Template — variable window (LC 3) + +```js +function lengthOfLongestSubstring(s) { + const seen = new Set(); + let best = 0; + let l = 0; + for (let r = 0; r < s.length; r++) { + while (seen.has(s[r])) { // shrink until valid + seen.delete(s[l]); + l++; + } + seen.add(s[r]); + best = Math.max(best, r - l + 1); + } + return best; +} +``` + +Grow with `r`, shrink with `l` only while invalid. + +--- + +## Why it's O(n) + +- `r` moves n times, `l` moves at most n times +- Each element enters and leaves the window **once** +- The inner `while` is amortized, not nested work + +--- + +## Pitfalls + +- State must update in O(1) on both add and remove +- "Shortest window" flips the loop: shrink while **valid**, record before breaking +- Doesn't apply when the constraint isn't monotone (mixed signs → prefix sum) + +--- + +## Recall + +- LC 121 Best Time to Buy/Sell: what is the "window state"? (min so far) +- LC 76 Minimum Window: what makes a window "valid"? +- When must you fall back to prefix sums instead? + +--- + +## Drill + +- 121 Best Time to Buy and Sell Stock +- 3 Longest Substring Without Repeating Characters +- 424 Longest Repeating Character Replacement +- 567 Permutation in String · 76 Minimum Window Substring +- 209 Minimum Size Subarray Sum diff --git a/islands/decks/two-pointers.md b/islands/decks/two-pointers.md new file mode 100644 index 0000000..7308a88 --- /dev/null +++ b/islands/decks/two-pointers.md @@ -0,0 +1,77 @@ +## Two Pointers + +Walk two indices instead of trying all pairs — **order lets you prune O(n²) → O(n)**. + +--- + +## Recognition + +- Sorted array + pair/target condition → converge from both ends +- Palindrome / symmetric check → ends inward +- In-place partition or dedupe → slow writer + fast reader +- Two sequences compared → one pointer each + +--- + +## Template — converging (LC 167) + +```js +function twoSumSorted(nums, target) { + let l = 0; + let r = nums.length - 1; + while (l < r) { + const sum = nums[l] + nums[r]; + if (sum === target) return [l + 1, r + 1]; + if (sum < target) l++; // need a bigger sum + else r--; // need a smaller sum + } +} +``` + +Each step discards every pair using the abandoned index — that's the proof. + +--- + +## Template — fast/slow writer (LC 977 idea) + +```js +function sortedSquares(nums) { + const out = new Array(nums.length); + let l = 0; + let r = nums.length - 1; + for (let i = nums.length - 1; i >= 0; i--) { + const a = nums[l] * nums[l]; + const b = nums[r] * nums[r]; + if (a > b) { out[i] = a; l++; } + else { out[i] = b; r--; } + } + return out; +} +``` + +Largest squares live at the **edges** — fill the output backwards. + +--- + +## Pitfalls + +- Moving the wrong pointer breaks the pruning argument +- Duplicates in 3Sum: skip repeats after each fixed element +- `l < r` vs `l <= r`: do the pointers meet or cross? + +--- + +## Recall + +- Container With Most Water: why move the *shorter* wall? +- 3Sum = sort + fix one + which pattern inside? +- Valid Palindrome: what do you do with non-alphanumerics? + +--- + +## Drill + +- 125 Valid Palindrome · 167 Two Sum II +- 15 3Sum · 11 Container With Most Water +- 42 Trapping Rain Water · 392 Is Subsequence +- 977 Squares of a Sorted Array · 80 Remove Duplicates II