mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 21:22:43 +00:00
Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c18301aa6 | ||
|
|
02f6a20cc1 | ||
|
|
9288aaa152 | ||
|
|
d5ea71dbc1 | ||
|
|
3e1a66eb37 | ||
|
|
9a2b80949b | ||
|
|
883dc69dba | ||
|
|
e3f0b5420a | ||
|
|
987eb7ab5b | ||
|
|
e4118fccd3 | ||
|
|
95db4ca46c | ||
|
|
0cc7ca607e | ||
|
|
71ec39fe34 | ||
|
|
51decd23bd | ||
|
|
90082518a6 | ||
|
|
e10a913546 | ||
|
|
fec94ed636 | ||
|
|
30273d0030 | ||
|
|
50299defef | ||
|
|
c843be855e | ||
|
|
1e0e03daaa | ||
|
|
7b182206dd | ||
|
|
faaaf52f85 | ||
|
|
090f3d6719 | ||
|
|
a3ae16ad96 | ||
|
|
9faa0b0337 | ||
|
|
cc29304903 | ||
|
|
c50b600103 | ||
|
|
a86d72c7db | ||
|
|
e59d78e571 | ||
|
|
a93bb068b6 | ||
|
|
53b2d5ec8c | ||
|
|
5dda64e41e | ||
|
|
d798871fbc | ||
|
|
bb7324fe8c | ||
|
|
d13859d31e | ||
|
|
664ea534ed | ||
|
|
b86f36daf1 | ||
|
|
7bae7b2920 | ||
|
|
64173857e7 | ||
|
|
002c2c9c97 | ||
|
|
fa8deac963 | ||
|
|
ef8d2b41e1 |
No files matched your search
@@ -23,6 +23,7 @@ flowchart LR
|
|||||||
D1 -->|Sat midnight ET| Review[Review — Week N issue]
|
D1 -->|Sat midnight ET| Review[Review — Week N issue]
|
||||||
PI -->|"spaced-repetition.yml (6 AM ET cron, closed +3d/+7d)"| SR[Spaced Repetition — Month D issue]
|
PI -->|"spaced-repetition.yml (6 AM ET cron, closed +3d/+7d)"| SR[Spaced Repetition — Month D issue]
|
||||||
D1 --> Charts[SVG charts + /api/stats]
|
D1 --> Charts[SVG charts + /api/stats]
|
||||||
|
Charts -->|bun run stats| TUI[terminal dashboard]
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Reconcile, don't react.** `close-solved.ts`, `close-topics.ts`, and the Worker's catalog sync recompute desired state from scratch each run: re-runs are no-ops, backfills need no special casing, closing is one-directional. `close-solved` posts its *whole* implemented set to `/admin/solved`, not just the issues it closed this run.
|
- **Reconcile, don't react.** `close-solved.ts`, `close-topics.ts`, and the Worker's catalog sync recompute desired state from scratch each run: re-runs are no-ops, backfills need no special casing, closing is one-directional. `close-solved` posts its *whole* implemented set to `/admin/solved`, not just the issues it closed this run.
|
||||||
@@ -56,6 +57,10 @@ bun run pick # scaffold a solution via leetcode-cli (fuzzy picker; Ta
|
|||||||
# re-solves → work/3, work/7. Each section hides what its own bucket
|
# re-solves → work/3, work/7. Each section hides what its own bucket
|
||||||
# already holds; index+descriptions cached in
|
# already holds; index+descriptions cached in
|
||||||
# ~/.local/share/leetcode/data.db — see apps/cli/db.ts)
|
# ~/.local/share/leetcode/data.db — see apps/cli/db.ts)
|
||||||
|
bun run stats # terminal dashboard over GET /api/stats — four panes (Overview,
|
||||||
|
# Ladder, Concepts, Activity); Tab/1-4 switches, ↑↓ scrolls,
|
||||||
|
# r refetches, q quits. Outside a TTY it prints every pane once.
|
||||||
|
# SRS_API points it at a local Worker like close-solved does.
|
||||||
bun run test # run ONE solution against LeetCode's judge (not a test suite;
|
bun run test # run ONE solution against LeetCode's judge (not a test suite;
|
||||||
# most recently tested first, stamped in the db on exit 0)
|
# most recently tested first, stamped in the db on exit 0)
|
||||||
bun run submit # submit ONE solution to LeetCode (newest scaffold first, nothing
|
bun run submit # submit ONE solution to LeetCode (newest scaffold first, nothing
|
||||||
@@ -92,12 +97,13 @@ curl -X POST -H "Authorization: Bearer $LINK_KEY" \
|
|||||||
|
|
||||||
## Important Files
|
## Important Files
|
||||||
|
|
||||||
- `apps/api/src/srs.ts` — SRS domain: the ladder (`new → +3 → +7 → retired`; fail resets to `+3`; first-ever log enters at `+3`), `WINDOWS`/`STAGES` (the one vocabulary shared by the charts, the `work/<n>` buckets and the review issues), ET date math, `workingDay()` (no scheduled date is ever a Sunday), seeded sampling, `logAttempt()` (the ONE write path — email taps, webhook, gate scoring all converge here), and overload leveling: at most `REVIEW_CAP` (3) reviews surface per day — `levelReviews()` (run by `sendDigest` on real sends only, never dry) gives everything past the cap a concrete future WORKING day, ≤ 3 per day, oldest first, instead of letting the due pile grow.
|
- `apps/api/src/srs.ts` — SRS domain: the ladder (`new → +3 → +7 → retired`; fail resets to `+3`; first-ever log enters at `+3`), `WINDOWS`/`STAGES`/`TEMPERATURES`/`DIFFICULTIES`/`SETS` (the one vocabulary shared by the charts, the `work/<n>` buckets, the review issues and the stats dashboard), ET date math, `workingDay()` (no scheduled date is ever a Sunday), `temperatureOf()` (days since a concept was last exercised → `hot`/`fresh`/`fading`/`cold`, bands derived from `WINDOWS` so they cannot drift; read-side only, nothing is scheduled off it), seeded sampling, `logAttempt()` (the ONE write path — email taps, webhook, gate scoring all converge here), and overload leveling: at most `REVIEW_CAP` (3) reviews surface per day — `levelReviews()` (run by `sendDigest` on real sends only, never dry) gives everything past the cap a concrete future WORKING day, ≤ 3 per day, oldest first, instead of letting the due pile grow.
|
||||||
- `apps/api/src/index.ts` — router + cron dispatch; `apps/api/wrangler.jsonc` — bindings (`DB`, `EMAIL`), crons, vars; secrets `GH_PAT`/`WEBHOOK_SECRET`/`LINK_KEY` via `wrangler secret put`.
|
- `apps/api/src/index.ts` — router + cron dispatch; `apps/api/wrangler.jsonc` — bindings (`DB`, `EMAIL`), crons, vars; secrets `GH_PAT`/`WEBHOOK_SECRET`/`LINK_KEY` via `wrangler secret put`.
|
||||||
- `apps/api/src/digest.ts` / `apps/api/src/email.tsx` — the daily digest, split data/presentation. `digest.ts` reads D1 into a `DigestData`; `email.tsx` owns every colour and every sentence, and renders both the HTML and (via its own `plainDigest`, not React Email's `plainText` mode, which flattens the tables) the text alternative. The retrieval rules hold by construction: `DigestRow` has no title and no issue field, so a review or drill line *cannot* leak the topic or a solution link. Subject is `(Day N/56) LeetCode Daily Digest`.
|
- `apps/api/src/digest.ts` / `apps/api/src/email.tsx` — the daily digest, split data/presentation. `digest.ts` reads D1 into a `DigestData`; `email.tsx` owns every colour and every sentence, and renders both the HTML and (via its own `plainDigest`, not React Email's `plainText` mode, which flattens the tables) the text alternative. The retrieval rules hold by construction: `DigestRow` has no title and no issue field, so a review or drill line *cannot* leak the topic or a solution link. Subject is `(Day N/56) LeetCode Daily Digest`.
|
||||||
- `apps/api/src/png.ts` — hand-rolled PNG encoder (RGB8, one IDAT, zlib via `CompressionStream("deflate")`, CRC32, 5×7 bitmap font). It exists because every major email client refuses remote SVG, so `/chart/heatmap.png` rasters the heatmap for the digest while `/chart/heatmap.svg` keeps serving the README byte-for-byte. Both come from one `heatmapCells()` so the two pictures cannot drift.
|
- `apps/api/src/png.ts` — hand-rolled PNG encoder (RGB8, one IDAT, zlib via `CompressionStream("deflate")`, CRC32, 5×7 bitmap font). It exists because every major email client refuses remote SVG, so `/chart/heatmap.png` rasters the heatmap for the digest while `/chart/heatmap.svg` keeps serving the README byte-for-byte. Both come from one `heatmapCells()` so the two pictures cannot drift.
|
||||||
- `apps/cli/sync.ts` — work→docs contract: only `## Solution` onward is script-owned on existing pages; human prose is never touched; never hand-write solution pages or edit inside `## Solution`.
|
- `apps/cli/sync.ts` — work→docs contract: on existing pages only the `## Solution` section itself is script-owned, bounded by `solutionEnd()` at the next unfenced top-level heading; human prose before it and any sections after it (`## Approach`, `## Explanation`) are never touched. Never hand-write solution pages or edit inside `## Solution`.
|
||||||
- `apps/cli/work.ts` — the `work/` layout: bucket paths, `firstSolved()` (one `git log`, ET dates, keyed by LC number so past layout moves don't matter), and `reviewQueues()` (what each window still owes). Everything that needs to know where a solution file lives, or when it was solved, goes through here.
|
- `apps/cli/work.ts` — the `work/` layout: bucket paths, `isSolution()`/`solvedInBucket()` (the stub test, shared with `close-solved.ts`), `firstSolved()` (one `git log`, ET dates, keyed by LC number so past layout moves don't matter), and `reviewQueues()` (what each window still owes). Everything that needs to know where a solution file lives, or when it was solved, goes through here.
|
||||||
|
- `apps/api/src/stats.ts` / `apps/cli/stats.ts` / `apps/cli/tui.ts` / `apps/cli/api.ts` — the read side. `buildStats()` is the ONE aggregation: phases, ladder, difficulty, per-topic temperature, gates, the 14-day due queue, and `heat` (every campaign day, zeros and future days included — the docs page windows the tail off it rather than asking a second question). Two readers consume it, the docs island and `bun run stats`, so its shape is both their contract: change all three together. `tui.ts` is hand-rolled ANSI (bars, sparklines, heat cells, alternate screen) on charts.ts's palette, and every chart differs by GLYPH as well as by colour so a piped or `NO_COLOR` dashboard still reads. The TUI recomputes nothing: the only local fact it shows is the `work/` solution count, which is inventory, not schedule.
|
||||||
- `README.md` live charts are Worker endpoints (`/chart/*.svg`, `/badge/gate.svg`, 5-min Camo cache); the docs `/progress` page fetches `/api/stats` client-side (`apps/docs/islands/ProgressDashboard.tsx`).
|
- `README.md` live charts are Worker endpoints (`/chart/*.svg`, `/badge/gate.svg`, 5-min Camo cache); the docs `/progress` page fetches `/api/stats` client-side (`apps/docs/islands/ProgressDashboard.tsx`).
|
||||||
- `apps/docs/blume.config.ts` — site base `/leetcode`; `README.md` campaign table doubles as topic-map input to `sync.ts` (`readmeTopics()`, `TOPIC_ALIASES`).
|
- `apps/docs/blume.config.ts` — site base `/leetcode`; `README.md` campaign table doubles as topic-map input to `sync.ts` (`readmeTopics()`, `TOPIC_ALIASES`).
|
||||||
- Old `.github/srs/` JSON state, its `srs-*` Actions, and the one-shot `import-srs.ts` importer are RETIRED and deleted — do not resurrect; git history is the record.
|
- Old `.github/srs/` JSON state, its `srs-*` Actions, and the one-shot `import-srs.ts` importer are RETIRED and deleted — do not resurrect; git history is the record.
|
||||||
@@ -105,8 +111,7 @@ curl -X POST -H "Authorization: Bearer $LINK_KEY" \
|
|||||||
## Runtime/Tooling Preferences
|
## Runtime/Tooling Preferences
|
||||||
|
|
||||||
- **Bun only**: run scripts with `bun apps/cli/<name>.ts` (or the root `bun run` aliases), install with `bun install --frozen-lockfile`. One workspace (`apps/*`), one root `bun.lock`. Node 22 appears only in `deploy.yml` because Blume requires it.
|
- **Bun only**: run scripts with `bun apps/cli/<name>.ts` (or the root `bun run` aliases), install with `bun install --frozen-lockfile`. One workspace (`apps/*`), one root `bun.lock`. Node 22 appears only in `deploy.yml` because Blume requires it.
|
||||||
- **No root tsconfig, no ESLint, no Prettier** — match surrounding style by hand. `apps/api/` has a strict `tsconfig.json`; `Env` types are generated (`bunx wrangler types`), never hand-written.
|
- **Near-zero runtime npm dependencies** — SVG, PNG, terminal ANSI, HMAC (WebCrypto), GraphQL are hand-rolled; Actions scripts use Bun builtins + `fetch` only. The **one** exception is the digest email: `apps/api/src/email.tsx` renders React Email (`react`, `react-dom`, `@react-email/components`, `@react-email/render`) inside the Worker, because hand-rolling table-based email HTML that survives Gmail *and* Outlook is not worth owning. It costs ~235 KB gzipped of a 3 MB budget, needs no `nodejs_compat` (it resolves to `renderToReadableStream`), and requires `"jsx": "react-jsx"` in `apps/api/tsconfig.json`. Do not extend this exception to any other module.
|
||||||
- **Near-zero runtime npm dependencies** — SVG, PNG, HMAC (WebCrypto), GraphQL are hand-rolled; Actions scripts use Bun builtins + `fetch` only. The **one** exception is the digest email: `apps/api/src/email.tsx` renders React Email (`react`, `react-dom`, `@react-email/components`, `@react-email/render`) inside the Worker, because hand-rolling table-based email HTML that survives Gmail *and* Outlook is not worth owning. It costs ~235 KB gzipped of a 3 MB budget, needs no `nodejs_compat` (it resolves to `renderToReadableStream`), and requires `"jsx": "react-jsx"` in `apps/api/tsconfig.json`. Do not extend this exception to any other module.
|
|
||||||
- Local GitHub auth falls back to `gh auth token`; scripts run fine outside Actions.
|
- Local GitHub auth falls back to `gh auth token`; scripts run fine outside Actions.
|
||||||
|
|
||||||
## Testing & QA
|
## Testing & QA
|
||||||
|
|||||||
@@ -10,14 +10,11 @@
|
|||||||
* so relabelling a problem is enough to move it and titles stay readable.
|
* so relabelling a problem is enough to move it and titles stay readable.
|
||||||
* The `problem` label is what marks a sub-issue as curriculum.
|
* The `problem` label is what marks a sub-issue as curriculum.
|
||||||
*/
|
*/
|
||||||
import { workingDay } from "./srs.ts";
|
import { DIFFICULTIES, SETS, workingDay } from "./srs.ts";
|
||||||
import type { GitHub } from "./github.ts";
|
import type { GitHub } from "./github.ts";
|
||||||
|
|
||||||
const TITLE_RE = /^LC (\d+) · (.+)$/;
|
const TITLE_RE = /^LC (\d+) · (.+)$/;
|
||||||
|
|
||||||
const DIFFICULTIES = ["easy", "medium", "hard"] as const;
|
|
||||||
const SETS = ["core", "optional", "deferred"] as const;
|
|
||||||
|
|
||||||
/** Deferred Hards enter the queue from this date, two per working day. */
|
/** Deferred Hards enter the queue from this date, two per working day. */
|
||||||
const DEFER_FROM = "2026-09-28";
|
const DEFER_FROM = "2026-09-28";
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import {
|
|||||||
isoWeek,
|
isoWeek,
|
||||||
rng,
|
rng,
|
||||||
sample,
|
sample,
|
||||||
|
TEMPERATURES,
|
||||||
|
temperatureOf,
|
||||||
weekdayOf,
|
weekdayOf,
|
||||||
workingDay,
|
workingDay,
|
||||||
} from "./srs.ts";
|
} from "./srs.ts";
|
||||||
@@ -132,6 +134,31 @@ describe("the ladder", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("concept temperature", () => {
|
||||||
|
test("the bands are the ladder's own windows", () => {
|
||||||
|
// Boundaries are inclusive, and the bands tile the whole age axis with no
|
||||||
|
// gap: whatever the windows become, every age still lands in exactly one.
|
||||||
|
expect(TEMPERATURES).toEqual(["hot", "fresh", "fading", "cold"]);
|
||||||
|
expect(temperatureOf(0)).toBe("hot");
|
||||||
|
expect(temperatureOf(WINDOWS[0])).toBe("hot");
|
||||||
|
expect(temperatureOf(WINDOWS[0] + 1)).toBe("fresh");
|
||||||
|
expect(temperatureOf(WINDOWS[1])).toBe("fresh");
|
||||||
|
expect(temperatureOf(WINDOWS[1] + 1)).toBe("fading");
|
||||||
|
expect(temperatureOf(WINDOWS[1] * 2)).toBe("fading");
|
||||||
|
expect(temperatureOf(WINDOWS[1] * 2 + 1)).toBe("cold");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("never attempted is cold, not unknown", () => {
|
||||||
|
expect(temperatureOf(null)).toBe("cold");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("every age of the campaign has exactly one band", () => {
|
||||||
|
for (let age = 0; age <= CALENDAR.length; age++) {
|
||||||
|
expect(TEMPERATURES).toContain(temperatureOf(age));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("deterministic sampling", () => {
|
describe("deterministic sampling", () => {
|
||||||
test("same seed, same picks", () => {
|
test("same seed, same picks", () => {
|
||||||
const pool = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
const pool = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||||
|
|||||||
+50
-5
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* SRS domain: ET dates, the work week, the interval ladder, deterministic
|
* SRS domain: ET dates, the work week, the interval ladder, concept
|
||||||
* sampling, and the one write path for attempts.
|
* temperature, deterministic sampling, and the one write path for attempts.
|
||||||
*
|
*
|
||||||
* D1 is the single source of truth. The stage names the review a problem must
|
* D1 is the single source of truth. The stage names the review a problem must
|
||||||
* pass NEXT (`+3` = due 3 working days after the last clean solve). Passing
|
* pass NEXT (`+3` = due 3 working days after the last clean solve). Passing
|
||||||
@@ -128,14 +128,20 @@ export function prettyDate(date: string): string {
|
|||||||
|
|
||||||
export const SCHEDULE: Record<string, number> = scheduleJson;
|
export const SCHEDULE: Record<string, number> = scheduleJson;
|
||||||
|
|
||||||
/** Week in which a topic was (or will be) taught. */
|
/** Date the schedule teaches a topic on, if it teaches it at all. */
|
||||||
export function topicWeek(topic: number): number | undefined {
|
export function topicDate(topic: number): string | undefined {
|
||||||
for (const [date, t] of Object.entries(SCHEDULE)) {
|
for (const [date, t] of Object.entries(SCHEDULE)) {
|
||||||
if (t === topic) return campaignWeek(date);
|
if (t === topic) return date;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Week in which a topic was (or will be) taught. */
|
||||||
|
export function topicWeek(topic: number): number | undefined {
|
||||||
|
const date = topicDate(topic);
|
||||||
|
return date === undefined ? undefined : campaignWeek(date);
|
||||||
|
}
|
||||||
|
|
||||||
// ── deterministic sampling ───────────────────────────────────────
|
// ── deterministic sampling ───────────────────────────────────────
|
||||||
|
|
||||||
/** FNV-1a → mulberry32: seeded PRNG so re-runs pick identical problems. */
|
/** FNV-1a → mulberry32: seeded PRNG so re-runs pick identical problems. */
|
||||||
@@ -182,6 +188,45 @@ export const STAGES: Stage[] = ["new", "+3", "+7", "retired"];
|
|||||||
export type Result = "pass" | "fail";
|
export type Result = "pass" | "fail";
|
||||||
export type Kind = "first" | "review" | "drill" | "gate";
|
export type Kind = "first" | "review" | "drill" | "gate";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catalog vocabulary, carried by the `diff:*` / `set:*` issue labels and
|
||||||
|
* stored verbatim in `problems`. Listed low to high / most to least required,
|
||||||
|
* which is the order every chart, table and digest section prints them in.
|
||||||
|
*/
|
||||||
|
export const DIFFICULTIES = ["easy", "medium", "hard"] as const;
|
||||||
|
export type Difficulty = (typeof DIFFICULTIES)[number];
|
||||||
|
export const SETS = ["core", "optional", "deferred"] as const;
|
||||||
|
export type SetLabel = (typeof SETS)[number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How warm a concept is: the age of a topic's most recent attempt, bucketed by
|
||||||
|
* the ladder's own windows so the two cannot drift. Touched inside the first
|
||||||
|
* window it is `hot`, inside the second `fresh`, inside one more full second
|
||||||
|
* window `fading`; older than that — or never attempted — it is `cold`.
|
||||||
|
*
|
||||||
|
* A read-side label only: nothing is scheduled off it. It answers the question
|
||||||
|
* the ladder cannot, because the ladder tracks problems and a topic can go
|
||||||
|
* quiet for a fortnight while not one of its problems comes due.
|
||||||
|
*/
|
||||||
|
export const TEMPERATURES = ["hot", "fresh", "fading", "cold"] as const;
|
||||||
|
export type Temperature = (typeof TEMPERATURES)[number];
|
||||||
|
|
||||||
|
/** Warmest band first, with the age it tolerates. Past the last one is cold. */
|
||||||
|
const TEMPERATURE_MAX_AGE: [Temperature, number][] = [
|
||||||
|
["hot", WINDOWS[0]],
|
||||||
|
["fresh", WINDOWS[1]],
|
||||||
|
["fading", WINDOWS[1] * 2],
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Days since a topic's last attempt → its band; null (never) is cold. */
|
||||||
|
export function temperatureOf(age: number | null): Temperature {
|
||||||
|
if (age === null) return "cold";
|
||||||
|
for (const [temperature, max] of TEMPERATURE_MAX_AGE) {
|
||||||
|
if (age <= max) return temperature;
|
||||||
|
}
|
||||||
|
return "cold";
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProblemRow {
|
export interface ProblemRow {
|
||||||
lc_number: number;
|
lc_number: number;
|
||||||
issue: number;
|
issue: number;
|
||||||
|
|||||||
+146
-28
@@ -1,19 +1,29 @@
|
|||||||
/**
|
/**
|
||||||
* GET /api/stats — the one JSON document behind the docs progress page.
|
* GET /api/stats — the one JSON document behind the docs progress page and the
|
||||||
* Read-only aggregation over D1; shape is the page's contract, change both
|
* `bun run stats` terminal dashboard. Read-only aggregation over D1; the shape
|
||||||
* together. A problem counts as "done" once it entered the ladder
|
* is both readers' contract, so change all three together. A problem counts as
|
||||||
* (stage != 'new').
|
* "done" once it entered the ladder (stage != 'new').
|
||||||
|
*
|
||||||
|
* The temperature block is the one figure here that is not a count of rows:
|
||||||
|
* srs.ts turns a topic's days-since-last-attempt into a band, so "which
|
||||||
|
* concepts have gone cold" has exactly one definition and the dashboard is
|
||||||
|
* free to colour it however it likes.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
CAMPAIGN_DAYS,
|
CAMPAIGN_DAYS,
|
||||||
CAMPAIGN_START,
|
CAMPAIGN_START,
|
||||||
|
DIFFICULTIES,
|
||||||
DUE_WHERE,
|
DUE_WHERE,
|
||||||
addDays,
|
addDays,
|
||||||
campaignDay,
|
campaignDay,
|
||||||
campaignWeek,
|
campaignWeek,
|
||||||
|
daysBetween,
|
||||||
isoWeek,
|
isoWeek,
|
||||||
STAGES,
|
STAGES,
|
||||||
streak,
|
streak,
|
||||||
|
temperatureOf,
|
||||||
|
topicDate,
|
||||||
|
type Temperature,
|
||||||
} from "./srs.ts";
|
} from "./srs.ts";
|
||||||
|
|
||||||
const PHASE_NAMES: Record<number, string> = {
|
const PHASE_NAMES: Record<number, string> = {
|
||||||
@@ -24,6 +34,91 @@ const PHASE_NAMES: Record<number, string> = {
|
|||||||
5: "V — Decision Space",
|
5: "V — Decision Space",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** One concept: catalog coverage plus how long since it was last exercised. */
|
||||||
|
interface TopicStat {
|
||||||
|
issue: number;
|
||||||
|
name: string;
|
||||||
|
milestone: number | null;
|
||||||
|
/** Campaign week the schedule teaches it in; null when unscheduled. */
|
||||||
|
week: number | null;
|
||||||
|
/** False while the schedule has not reached it — temperature is moot. */
|
||||||
|
taught: boolean;
|
||||||
|
total: number;
|
||||||
|
done: number;
|
||||||
|
retired: number;
|
||||||
|
due: number;
|
||||||
|
/** ET date of the most recent attempt on any of its problems. */
|
||||||
|
last: string | null;
|
||||||
|
/** Days since `last`; null when never attempted. */
|
||||||
|
age: number | null;
|
||||||
|
temperature: Temperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildTopics(db: D1Database, today: string): Promise<TopicStat[]> {
|
||||||
|
// Three shapes, three statements: batch() carries one row type, and casting
|
||||||
|
// its results apart would cost more than the round trips it saves.
|
||||||
|
const { results: coverage } = await db
|
||||||
|
.prepare(
|
||||||
|
`SELECT t.issue AS issue, t.name AS name, t.milestone AS milestone,
|
||||||
|
COUNT(p.lc_number) AS total,
|
||||||
|
SUM(CASE WHEN p.stage != 'new' THEN 1 ELSE 0 END) AS done,
|
||||||
|
SUM(CASE WHEN p.stage = 'retired' THEN 1 ELSE 0 END) AS retired
|
||||||
|
FROM topics t LEFT JOIN problems p ON p.topic_issue = t.issue
|
||||||
|
GROUP BY t.issue
|
||||||
|
ORDER BY t.issue`,
|
||||||
|
)
|
||||||
|
.all<{
|
||||||
|
issue: number;
|
||||||
|
name: string;
|
||||||
|
milestone: number | null;
|
||||||
|
total: number;
|
||||||
|
done: number | null;
|
||||||
|
retired: number | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { results: activity } = await db
|
||||||
|
.prepare(
|
||||||
|
`SELECT p.topic_issue AS issue, MAX(a.date) AS last
|
||||||
|
FROM attempts a JOIN problems p ON p.lc_number = a.lc_number
|
||||||
|
GROUP BY p.topic_issue`,
|
||||||
|
)
|
||||||
|
.all<{ issue: number; last: string | null }>();
|
||||||
|
const lastByTopic = new Map<number, string>();
|
||||||
|
for (const row of activity) {
|
||||||
|
if (row.last) lastByTopic.set(row.issue, row.last);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { results: dueRows } = await db
|
||||||
|
.prepare(
|
||||||
|
`SELECT topic_issue AS issue, COUNT(*) AS n
|
||||||
|
FROM problems WHERE ${DUE_WHERE} GROUP BY topic_issue`,
|
||||||
|
)
|
||||||
|
.bind(today)
|
||||||
|
.all<{ issue: number; n: number }>();
|
||||||
|
const dueByTopic = new Map<number, number>();
|
||||||
|
for (const row of dueRows) dueByTopic.set(row.issue, row.n);
|
||||||
|
|
||||||
|
return coverage.map((row) => {
|
||||||
|
const taughtOn = topicDate(row.issue);
|
||||||
|
const last = lastByTopic.get(row.issue) ?? null;
|
||||||
|
const age = last === null ? null : daysBetween(last, today);
|
||||||
|
return {
|
||||||
|
issue: row.issue,
|
||||||
|
name: row.name,
|
||||||
|
milestone: row.milestone,
|
||||||
|
week: taughtOn === undefined ? null : campaignWeek(taughtOn),
|
||||||
|
taught: taughtOn !== undefined && taughtOn <= today,
|
||||||
|
total: row.total,
|
||||||
|
done: row.done ?? 0,
|
||||||
|
retired: row.retired ?? 0,
|
||||||
|
due: dueByTopic.get(row.issue) ?? 0,
|
||||||
|
last,
|
||||||
|
age,
|
||||||
|
temperature: temperatureOf(age),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function buildStats(db: D1Database, today: string): Promise<object> {
|
export async function buildStats(db: D1Database, today: string): Promise<object> {
|
||||||
const { results: phaseRows } = await db
|
const { results: phaseRows } = await db
|
||||||
.prepare(
|
.prepare(
|
||||||
@@ -65,6 +160,21 @@ export async function buildStats(db: D1Database, today: string): Promise<object>
|
|||||||
const ladder: Record<string, number> = Object.fromEntries(STAGES.map((s) => [s, 0]));
|
const ladder: Record<string, number> = Object.fromEntries(STAGES.map((s) => [s, 0]));
|
||||||
for (const row of ladderRows) ladder[row.stage] = row.n;
|
for (const row of ladderRows) ladder[row.stage] = row.n;
|
||||||
|
|
||||||
|
// Same shape as `ladder`: every band present, zeros included, in DIFFICULTIES
|
||||||
|
// order so a reader can index it without sorting.
|
||||||
|
const { results: diffRows } = await db
|
||||||
|
.prepare(
|
||||||
|
`SELECT difficulty,
|
||||||
|
COUNT(*) AS total,
|
||||||
|
SUM(CASE WHEN stage != 'new' THEN 1 ELSE 0 END) AS done
|
||||||
|
FROM problems GROUP BY difficulty`,
|
||||||
|
)
|
||||||
|
.all<{ difficulty: string; total: number; done: number }>();
|
||||||
|
const difficulty = DIFFICULTIES.map((name) => {
|
||||||
|
const row = diffRows.find((r) => r.difficulty === name);
|
||||||
|
return { difficulty: name, total: row?.total ?? 0, done: row?.done ?? 0 };
|
||||||
|
});
|
||||||
|
|
||||||
// gates rows are keyed by ISO week (that's the digest/gate contract), but
|
// gates rows are keyed by ISO week (that's the digest/gate contract), but
|
||||||
// the page speaks campaign weeks. The campaign never crosses a year
|
// the page speaks campaign weeks. The campaign never crosses a year
|
||||||
// boundary, so a plain offset converts safely.
|
// boundary, so a plain offset converts safely.
|
||||||
@@ -74,39 +184,47 @@ export async function buildStats(db: D1Database, today: string): Promise<object>
|
|||||||
.all<{ week: number; issue: number | null; pass_rate: number | null; closed_on: string | null }>();
|
.all<{ week: number; issue: number | null; pass_rate: number | null; closed_on: string | null }>();
|
||||||
const gates = gateRows.map((g) => ({ ...g, week: g.week - isoWeekOffset }));
|
const gates = gateRows.map((g) => ({ ...g, week: g.week - isoWeekOffset }));
|
||||||
|
|
||||||
// Review-queue depth for the next 14 days: everything due by that day.
|
// Review-queue depth for the next 14 days: everything due by that day. One
|
||||||
const queue: { date: string; due: number }[] = [];
|
// statement per day, but DUE_WHERE stays the only definition of "due" — the
|
||||||
for (let i = 0; i < 14; i++) {
|
// batch is a single round trip, not fourteen.
|
||||||
const date = addDays(today, i);
|
const dates = Array.from({ length: 14 }, (_, i) => addDays(today, i));
|
||||||
const row = await db
|
const queueRows = await db.batch<{ n: number }>(
|
||||||
.prepare(`SELECT COUNT(*) AS n FROM problems WHERE ${DUE_WHERE}`)
|
dates.map((date) =>
|
||||||
.bind(date)
|
db.prepare(`SELECT COUNT(*) AS n FROM problems WHERE ${DUE_WHERE}`).bind(date),
|
||||||
.first<{ n: number }>();
|
),
|
||||||
queue.push({ date, due: row?.n ?? 0 });
|
);
|
||||||
}
|
const queue = dates.map((date, i) => ({ date, due: queueRows[i]?.results[0]?.n ?? 0 }));
|
||||||
|
|
||||||
const recent: { date: string; attempts: number; passes: number }[] = [];
|
// Attempts per campaign day, every day present. This is the heatmap's data
|
||||||
for (let i = 6; i >= 0; i--) {
|
// model — the docs page reads the tail of it for "last 7 days" rather than
|
||||||
const date = addDays(today, -i);
|
// asking a second question that could answer differently.
|
||||||
const row = await db
|
const end = addDays(CAMPAIGN_START, CAMPAIGN_DAYS - 1);
|
||||||
.prepare(
|
const { results: heatRows } = await db
|
||||||
`SELECT COUNT(*) AS attempts,
|
.prepare(
|
||||||
SUM(CASE WHEN result = 'pass' THEN 1 ELSE 0 END) AS passes
|
`SELECT date,
|
||||||
FROM attempts WHERE date = ?`,
|
COUNT(*) AS attempts,
|
||||||
)
|
SUM(CASE WHEN result = 'pass' THEN 1 ELSE 0 END) AS passes
|
||||||
.bind(date)
|
FROM attempts WHERE date BETWEEN ?1 AND ?2 GROUP BY date`,
|
||||||
.first<{ attempts: number; passes: number | null }>();
|
)
|
||||||
recent.push({ date, attempts: row?.attempts ?? 0, passes: row?.passes ?? 0 });
|
.bind(CAMPAIGN_START, end)
|
||||||
}
|
.all<{ date: string; attempts: number; passes: number | null }>();
|
||||||
|
const byDate = new Map(heatRows.map((r) => [r.date, r]));
|
||||||
|
const heat = Array.from({ length: CAMPAIGN_DAYS }, (_, i) => {
|
||||||
|
const date = addDays(CAMPAIGN_START, i);
|
||||||
|
const row = byDate.get(date);
|
||||||
|
return { date, attempts: row?.attempts ?? 0, passes: row?.passes ?? 0 };
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generated: new Date().toISOString(),
|
generated: new Date().toISOString(),
|
||||||
campaign: { day: campaignDay(today), week: campaignWeek(today), start: CAMPAIGN_START, days: CAMPAIGN_DAYS },
|
campaign: { day: campaignDay(today), week: campaignWeek(today), start: CAMPAIGN_START, days: CAMPAIGN_DAYS },
|
||||||
phases: [...phases.values()],
|
phases: [...phases.values()],
|
||||||
ladder,
|
ladder,
|
||||||
|
difficulty,
|
||||||
|
topics: await buildTopics(db, today),
|
||||||
gates,
|
gates,
|
||||||
streak: await streak(db, today),
|
streak: await streak(db, today),
|
||||||
queue,
|
queue,
|
||||||
recent,
|
heat,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* The SRS Worker, from the CLI side: one base URL and one read-side payload.
|
||||||
|
*
|
||||||
|
* `SRS_API` points a script at a `wrangler dev` instance instead of production
|
||||||
|
* — the same switch close-solved.ts uses for its `/admin/solved` push, which is
|
||||||
|
* why the default lives here rather than in either script.
|
||||||
|
*
|
||||||
|
* The payload is narrowed with a guard rather than cast: D1 is the only source
|
||||||
|
* of SRS truth, so if the Worker answers something else the caller must say so
|
||||||
|
* out loud, not render half a dashboard off undefined fields.
|
||||||
|
*/
|
||||||
|
import { STAGES, TEMPERATURES, type Stage, type Temperature } from "../api/src/srs.ts";
|
||||||
|
|
||||||
|
export const SRS_API = process.env.SRS_API ?? "https://srs-api.prdlk.workers.dev";
|
||||||
|
|
||||||
|
// The rungs and the bands come straight from the Worker's domain module — the
|
||||||
|
// same import work.ts makes for WINDOWS — so a change to either vocabulary
|
||||||
|
// reaches this dashboard without a second list to remember.
|
||||||
|
export { STAGES, TEMPERATURES, type Stage, type Temperature };
|
||||||
|
|
||||||
|
export interface Phase {
|
||||||
|
milestone: number;
|
||||||
|
name: string;
|
||||||
|
core_done: number;
|
||||||
|
core_total: number;
|
||||||
|
optional_done: number;
|
||||||
|
optional_total: number;
|
||||||
|
deferred_done: number;
|
||||||
|
deferred_total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Topic {
|
||||||
|
issue: number;
|
||||||
|
name: string;
|
||||||
|
milestone: number | null;
|
||||||
|
week: number | null;
|
||||||
|
taught: boolean;
|
||||||
|
total: number;
|
||||||
|
done: number;
|
||||||
|
retired: number;
|
||||||
|
due: number;
|
||||||
|
last: string | null;
|
||||||
|
age: number | null;
|
||||||
|
temperature: Temperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Stats {
|
||||||
|
generated: string;
|
||||||
|
campaign: { day: number; week: number; start: string; days: number };
|
||||||
|
phases: Phase[];
|
||||||
|
ladder: Record<Stage, number>;
|
||||||
|
difficulty: { difficulty: string; total: number; done: number }[];
|
||||||
|
topics: Topic[];
|
||||||
|
/** week is the 1-based campaign week — the Worker converts from ISO. */
|
||||||
|
gates: { week: number; issue: number | null; pass_rate: number | null; closed_on: string | null }[];
|
||||||
|
streak: number;
|
||||||
|
queue: { date: string; due: number }[];
|
||||||
|
/** Every campaign day, zeros and future days included. */
|
||||||
|
heat: { date: string; attempts: number; passes: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStats(value: unknown): value is Stats {
|
||||||
|
if (typeof value !== "object" || value === null) return false;
|
||||||
|
const v = value as Record<string, unknown>;
|
||||||
|
return (
|
||||||
|
typeof v.campaign === "object" &&
|
||||||
|
v.campaign !== null &&
|
||||||
|
typeof v.ladder === "object" &&
|
||||||
|
v.ladder !== null &&
|
||||||
|
typeof v.streak === "number" &&
|
||||||
|
Array.isArray(v.phases) &&
|
||||||
|
Array.isArray(v.difficulty) &&
|
||||||
|
Array.isArray(v.topics) &&
|
||||||
|
Array.isArray(v.gates) &&
|
||||||
|
Array.isArray(v.queue) &&
|
||||||
|
Array.isArray(v.heat)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /api/stats. Throws with the reason; there is no local substitute. */
|
||||||
|
export async function fetchStats(signal?: AbortSignal): Promise<Stats> {
|
||||||
|
const res = await fetch(`${SRS_API}/api/stats`, { signal });
|
||||||
|
if (!res.ok) throw new Error(`GET /api/stats -> ${res.status} ${await res.text()}`);
|
||||||
|
const body: unknown = await res.json();
|
||||||
|
if (!isStats(body)) throw new Error("GET /api/stats -> unrecognised payload shape");
|
||||||
|
return body;
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
*/
|
*/
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
import { SRS_API } from "./api.ts";
|
||||||
import { github } from "./github.ts";
|
import { github } from "./github.ts";
|
||||||
import { markdownTable, printTable, writeStepSummary } from "./report.ts";
|
import { markdownTable, printTable, writeStepSummary } from "./report.ts";
|
||||||
import { isImplemented } from "./source.ts";
|
import { isImplemented } from "./source.ts";
|
||||||
@@ -162,7 +163,8 @@ for (const num of [...work.keys()].sort((a, b) => a - b)) {
|
|||||||
// rung named, so the whole implemented set can go over on every push (a total
|
// rung named, so the whole implemented set can go over on every push (a total
|
||||||
// recompute that backfills whatever earlier runs missed) and re-runs write
|
// recompute that backfills whatever earlier runs missed) and re-runs write
|
||||||
// nothing.
|
// nothing.
|
||||||
const SRS_API = process.env.SRS_API ?? "https://srs-api.prdlk.workers.dev";
|
// SRS_API (apps/cli/api.ts) is the switch that points this at a wrangler dev
|
||||||
|
// instance instead of production.
|
||||||
const solved = [...work.entries()]
|
const solved = [...work.entries()]
|
||||||
.flatMap(([lc, entry]) => [...entry.solved].sort().map((bucket) => ({ lc, bucket })))
|
.flatMap(([lc, entry]) => [...entry.solved].sort().map((bucket) => ({ lc, bucket })))
|
||||||
.sort((a, b) => a.lc - b.lc || a.bucket - b.bucket);
|
.sort((a, b) => a.lc - b.lc || a.bucket - b.bucket);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"pick": "bun ./pick.ts",
|
"pick": "bun ./pick.ts",
|
||||||
|
"stats": "bun ./stats.ts",
|
||||||
"test": "bun ./test.ts",
|
"test": "bun ./test.ts",
|
||||||
"submit": "bun ./submit.ts",
|
"submit": "bun ./submit.ts",
|
||||||
"sync": "bun ./sync.ts",
|
"sync": "bun ./sync.ts",
|
||||||
|
|||||||
Executable
+458
@@ -0,0 +1,458 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* `bun run stats` — the campaign dashboard, in the terminal.
|
||||||
|
*
|
||||||
|
* Four panes over ONE source: GET /api/stats. D1 owns SRS state, so this
|
||||||
|
* fetches the Worker's aggregate rather than recomputing anything from work/
|
||||||
|
* or from git — a second answer to "how far along am I" is exactly the drift
|
||||||
|
* the campaign is built to avoid. The one local fact shown is the work/ file
|
||||||
|
* inventory, which is inventory, not schedule. Unreachable Worker = an error,
|
||||||
|
* never a reconstructed guess.
|
||||||
|
*
|
||||||
|
* The panes:
|
||||||
|
* Overview campaign clock, solved counts, phase bars, recent activity
|
||||||
|
* Ladder the +3/+7 rungs and the 14-day due queue
|
||||||
|
* Concepts every topic by temperature — what has gone cold
|
||||||
|
* Activity the campaign heatmap and the weekly gates
|
||||||
|
*
|
||||||
|
* Outside a TTY (a pipe, CI) it prints every pane once and exits, so the same
|
||||||
|
* command works in a scrollback or a file.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
fetchStats,
|
||||||
|
SRS_API,
|
||||||
|
STAGES,
|
||||||
|
TEMPERATURES,
|
||||||
|
type Stats,
|
||||||
|
type Temperature,
|
||||||
|
type Topic,
|
||||||
|
} from "./api.ts";
|
||||||
|
import {
|
||||||
|
BLUE,
|
||||||
|
FG,
|
||||||
|
GREEN,
|
||||||
|
GREENS,
|
||||||
|
LINE,
|
||||||
|
MUTED,
|
||||||
|
RED,
|
||||||
|
YELLOW,
|
||||||
|
bar,
|
||||||
|
bold,
|
||||||
|
cell,
|
||||||
|
fg,
|
||||||
|
heatCell,
|
||||||
|
openScreen,
|
||||||
|
padTo,
|
||||||
|
rule,
|
||||||
|
sparkline,
|
||||||
|
stackedBar,
|
||||||
|
width,
|
||||||
|
} from "./tui.ts";
|
||||||
|
import { BUCKETS, WINDOWS, solvedInBucket } from "./work.ts";
|
||||||
|
|
||||||
|
// ── shared vocabulary → colour ───────────────────────────────────
|
||||||
|
|
||||||
|
// Temperature is an action ramp, not a thermometer: green is warm enough to
|
||||||
|
// leave alone, red is the concept that wants revisiting. The band names carry
|
||||||
|
// the heat, the colours carry the urgency — and the glyphs carry both again,
|
||||||
|
// so a piped or NO_COLOR dashboard still separates the bands.
|
||||||
|
const TEMP_COLOR: Record<Temperature, string> = {
|
||||||
|
hot: GREENS[4]!,
|
||||||
|
fresh: GREEN,
|
||||||
|
fading: YELLOW,
|
||||||
|
cold: RED,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TEMP_GLYPH: Record<Temperature, string> = {
|
||||||
|
hot: "█",
|
||||||
|
fresh: "▓",
|
||||||
|
fading: "▒",
|
||||||
|
cold: "░",
|
||||||
|
};
|
||||||
|
|
||||||
|
const DIFF_COLOR: Record<string, string> = { easy: GREEN, medium: YELLOW, hard: RED };
|
||||||
|
|
||||||
|
const LABEL = 15; // left-hand label column, shared by every pane
|
||||||
|
|
||||||
|
/** "2026-08-31" → "Mon 31", read as a plain calendar date, never localised. */
|
||||||
|
function dayLabel(iso: string): string {
|
||||||
|
const [y, m, d] = iso.split("-").map(Number);
|
||||||
|
const weekday = new Date(Date.UTC(y!, m! - 1, d!)).getUTCDay();
|
||||||
|
return `${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][weekday]} ${String(d).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── overview ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function overview(s: Stats, cols: number): string[] {
|
||||||
|
const bars = Math.max(10, Math.min(48, cols - LABEL - 18));
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
const { day, days, week } = s.campaign;
|
||||||
|
lines.push(rule("campaign", cols));
|
||||||
|
lines.push(
|
||||||
|
`${cell("Day", LABEL)}${bar(day, days, bars)} ${fg(FG, `${day}/${days}`)}` +
|
||||||
|
fg(MUTED, ` week ${week}`),
|
||||||
|
);
|
||||||
|
const streak = s.streak > 0 ? fg(GREENS[4]!, `${s.streak} day${s.streak === 1 ? "" : "s"}`) : fg(MUTED, "none");
|
||||||
|
lines.push(`${cell("Streak", LABEL)}${streak}`);
|
||||||
|
|
||||||
|
const total = s.difficulty.reduce((n, d) => n + d.total, 0);
|
||||||
|
const done = s.difficulty.reduce((n, d) => n + d.done, 0);
|
||||||
|
lines.push("");
|
||||||
|
lines.push(rule("solved", cols));
|
||||||
|
lines.push(
|
||||||
|
`${cell("All", LABEL)}${bar(done, total, bars)} ${fg(FG, `${done}/${total}`)}` +
|
||||||
|
fg(MUTED, ` ${total > 0 ? Math.round((done / total) * 100) : 0}%`),
|
||||||
|
);
|
||||||
|
for (const d of s.difficulty) {
|
||||||
|
lines.push(
|
||||||
|
`${cell(d.difficulty, LABEL)}${bar(d.done, d.total, bars, DIFF_COLOR[d.difficulty] ?? GREEN)}` +
|
||||||
|
` ${fg(MUTED, `${d.done}/${d.total}`)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("");
|
||||||
|
lines.push(rule("phases", cols));
|
||||||
|
lines.push(
|
||||||
|
`${" ".repeat(LABEL)}${fg(GREEN, "█")} ${fg(MUTED, "core")} ${fg(BLUE, "█")} ${fg(MUTED, "optional")} ` +
|
||||||
|
`${fg(RED, "█")} ${fg(MUTED, "deferred")} ${fg(LINE, "░")} ${fg(MUTED, "remaining")}`,
|
||||||
|
);
|
||||||
|
// Every phase bar is scaled to the LARGEST phase, not to itself: phases are
|
||||||
|
// not the same size, and equal-length bars would hide that.
|
||||||
|
const scale = Math.max(
|
||||||
|
1,
|
||||||
|
...s.phases.map((p) => p.core_total + p.optional_total + p.deferred_total),
|
||||||
|
);
|
||||||
|
for (const p of s.phases) {
|
||||||
|
const phaseDone = p.core_done + p.optional_done + p.deferred_done;
|
||||||
|
const phaseTotal = p.core_total + p.optional_total + p.deferred_total;
|
||||||
|
const segments = [
|
||||||
|
{ value: p.core_done, color: GREEN },
|
||||||
|
{ value: p.optional_done, color: BLUE },
|
||||||
|
{ value: p.deferred_done, color: RED },
|
||||||
|
{ value: phaseTotal - phaseDone, color: LINE, glyph: "░" },
|
||||||
|
];
|
||||||
|
lines.push(
|
||||||
|
`${cell(p.name, LABEL - 1)} ${stackedBar(segments, scale, bars)} ${fg(MUTED, `${phaseDone}/${phaseTotal}`)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("");
|
||||||
|
lines.push(rule("last 14 days", cols));
|
||||||
|
const window = s.heat.slice(Math.max(0, day - 14), day);
|
||||||
|
const attempts = window.reduce((n, h) => n + h.attempts, 0);
|
||||||
|
const passes = window.reduce((n, h) => n + h.passes, 0);
|
||||||
|
lines.push(
|
||||||
|
`${cell("Attempts", LABEL)}${sparkline(window.map((h) => h.attempts))} ` +
|
||||||
|
fg(MUTED, `${attempts} attempts · ${passes} passed`),
|
||||||
|
);
|
||||||
|
|
||||||
|
// The one local fact on the dashboard: solutions on disk, per
|
||||||
|
// spaced-repetition bucket. Inventory, not schedule — D1's ladder is the
|
||||||
|
// schedule, and the Ladder pane is where it is shown. Empty scaffolds do not
|
||||||
|
// count, the same rule close-solved.ts closes issues by.
|
||||||
|
lines.push("");
|
||||||
|
lines.push(rule("work/ on disk", cols));
|
||||||
|
lines.push(
|
||||||
|
cell("Solutions", LABEL) +
|
||||||
|
BUCKETS.map((b) => `${fg(FG, String(solvedInBucket(b)))} ${fg(MUTED, `in work/${b}`)}`).join(
|
||||||
|
fg(MUTED, " · "),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ladder ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function ladder(s: Stats, cols: number): string[] {
|
||||||
|
const bars = Math.max(10, Math.min(48, cols - LABEL - 18));
|
||||||
|
const lines: string[] = [rule("ladder", cols)];
|
||||||
|
lines.push(fg(MUTED, `${" ".repeat(LABEL)}the review each problem owes next`));
|
||||||
|
|
||||||
|
const rungs = STAGES.map((stage) => s.ladder[stage] ?? 0);
|
||||||
|
const peak = Math.max(1, ...rungs);
|
||||||
|
STAGES.forEach((stage, i) => {
|
||||||
|
const n = rungs[i]!;
|
||||||
|
// new is the untouched pile and retired is the finished one; only the
|
||||||
|
// middle rungs are live work, so only they get the live colour.
|
||||||
|
const color = stage === "new" ? LINE : stage === "retired" ? GREENS[4]! : GREEN;
|
||||||
|
lines.push(`${cell(stage, LABEL)}${bar(n, peak, bars, color)} ${fg(FG, String(n))}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
lines.push("");
|
||||||
|
lines.push(rule("due queue", cols));
|
||||||
|
const today = s.queue[0];
|
||||||
|
lines.push(
|
||||||
|
fg(MUTED, `${" ".repeat(LABEL)}cumulative: everything the ladder wants by that day`) +
|
||||||
|
(today ? fg(FG, ` · ${today.due} due today`) : ""),
|
||||||
|
);
|
||||||
|
const depth = Math.max(1, ...s.queue.map((q) => q.due));
|
||||||
|
for (const q of s.queue) {
|
||||||
|
const color = q.due > 3 ? RED : q.due > 0 ? GREEN : LINE;
|
||||||
|
lines.push(
|
||||||
|
`${cell(dayLabel(q.date), LABEL)}${bar(q.due, depth, bars, color)} ${fg(MUTED, String(q.due))}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── concepts ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Coldest first, and everything the schedule has not reached yet last. */
|
||||||
|
function byUrgency(a: Topic, b: Topic): number {
|
||||||
|
if (a.taught !== b.taught) return a.taught ? -1 : 1;
|
||||||
|
return (
|
||||||
|
TEMPERATURES.indexOf(b.temperature) - TEMPERATURES.indexOf(a.temperature) ||
|
||||||
|
(b.age ?? Infinity) - (a.age ?? Infinity) ||
|
||||||
|
a.issue - b.issue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row layout: "● " + name + temp + coverage bar + done/total + last + due.
|
||||||
|
// Everything but the name is fixed, so the name absorbs the terminal width.
|
||||||
|
const TEMP_W = 7;
|
||||||
|
const COVER_W = 10;
|
||||||
|
const COUNT_W = 6;
|
||||||
|
const AGE_W = 6;
|
||||||
|
const DUE_W = 3;
|
||||||
|
const CONCEPT_FIXED = 3 + (TEMP_W + 1) + (COVER_W + 1) + (COUNT_W + 1) + (AGE_W + 1) + DUE_W + 1;
|
||||||
|
|
||||||
|
function concepts(s: Stats, cols: number): string[] {
|
||||||
|
const lines: string[] = [rule("concepts by temperature", cols)];
|
||||||
|
// Thresholds are the ladder's windows; print them rather than restate them.
|
||||||
|
const bands: [Temperature, string][] = [
|
||||||
|
["hot", `≤${WINDOWS[0]}d`],
|
||||||
|
["fresh", `≤${WINDOWS[1]}d`],
|
||||||
|
["fading", `≤${WINDOWS[1] * 2}d`],
|
||||||
|
["cold", "older, or never"],
|
||||||
|
];
|
||||||
|
lines.push(
|
||||||
|
fg(MUTED, " last exercised — ") +
|
||||||
|
bands
|
||||||
|
.map(([t, when]) => `${fg(TEMP_COLOR[t], `${TEMP_GLYPH[t]} ${t}`)} ${fg(MUTED, when)}`)
|
||||||
|
.join(fg(MUTED, " · ")),
|
||||||
|
);
|
||||||
|
|
||||||
|
const taught = s.topics.filter((t) => t.taught);
|
||||||
|
const counts = TEMPERATURES.map((t) => ({
|
||||||
|
value: taught.filter((x) => x.temperature === t).length,
|
||||||
|
color: TEMP_COLOR[t],
|
||||||
|
glyph: TEMP_GLYPH[t],
|
||||||
|
}));
|
||||||
|
lines.push("");
|
||||||
|
lines.push(` ${stackedBar(counts, Math.max(1, taught.length), Math.max(10, Math.min(60, cols - 4)))}`);
|
||||||
|
lines.push(
|
||||||
|
` ${TEMPERATURES.map((t, i) => `${fg(TEMP_COLOR[t], TEMP_GLYPH[t])} ${counts[i]!.value} ${fg(MUTED, t)}`).join(" ")}` +
|
||||||
|
fg(MUTED, ` of ${taught.length} taught so far`),
|
||||||
|
);
|
||||||
|
|
||||||
|
const nameW = Math.max(12, Math.min(34, cols - CONCEPT_FIXED));
|
||||||
|
lines.push("");
|
||||||
|
lines.push(
|
||||||
|
fg(
|
||||||
|
MUTED,
|
||||||
|
` ${cell("concept", nameW)} ${cell("temp", TEMP_W)} ` +
|
||||||
|
`${cell("coverage", COVER_W + 1 + COUNT_W)} ${cell("last", AGE_W)} ${cell("due", DUE_W, "right")}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const t of s.topics.slice().sort(byUrgency)) {
|
||||||
|
// An unreached concept has no temperature to report — it shows the week
|
||||||
|
// the schedule gets to it instead, so the list stays one list.
|
||||||
|
const color = t.taught ? TEMP_COLOR[t.temperature] : LINE;
|
||||||
|
const age =
|
||||||
|
!t.taught ? "—" : t.age === null ? "never" : t.age <= 0 ? "today" : `${t.age}d`;
|
||||||
|
lines.push(
|
||||||
|
` ${fg(color, t.taught ? TEMP_GLYPH[t.temperature] : "·")} ${fg(t.taught ? FG : MUTED, cell(t.name, nameW))} ` +
|
||||||
|
`${fg(color, cell(t.taught ? t.temperature : `wk ${t.week ?? "?"}`, TEMP_W))} ` +
|
||||||
|
`${bar(t.done, t.total, COVER_W, t.taught ? GREEN : LINE)} ` +
|
||||||
|
`${fg(MUTED, cell(`${t.done}/${t.total}`, COUNT_W))} ${fg(MUTED, cell(age, AGE_W))} ` +
|
||||||
|
`${t.due > 0 ? fg(GREEN, cell(String(t.due), DUE_W, "right")) : fg(LINE, cell("·", DUE_W, "right"))}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── activity ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||||
|
|
||||||
|
function activity(s: Stats, cols: number): string[] {
|
||||||
|
const lines: string[] = [rule("attempts per campaign day", cols)];
|
||||||
|
lines.push(fg(MUTED, " the README heatmap, same ramp — Sunday is the rest day"));
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
// CAMPAIGN_START is a Monday, so the grid is simply the series in weeks of
|
||||||
|
// seven: column = week, row = weekday, exactly as the SVG lays it out.
|
||||||
|
const weeks = Math.ceil(s.heat.length / 7);
|
||||||
|
lines.push(
|
||||||
|
` ${fg(MUTED, Array.from({ length: weeks }, (_, w) => ` ${String(w + 1).padStart(2)}`).join(""))}`,
|
||||||
|
);
|
||||||
|
WEEKDAYS.forEach((name, row) => {
|
||||||
|
const cells = Array.from({ length: weeks }, (_, w) => {
|
||||||
|
const entry = s.heat[w * 7 + row];
|
||||||
|
if (entry === undefined) return " ";
|
||||||
|
// The marker sits in the gutter, not inside the cell: a foreground
|
||||||
|
// escape within a background run would end the background early.
|
||||||
|
const marker = s.campaign.day - 1 === w * 7 + row ? fg(FG, "▸") : " ";
|
||||||
|
return marker + heatCell(entry.attempts, " ");
|
||||||
|
}).join("");
|
||||||
|
lines.push(` ${fg(MUTED, cell(name, 4))}${cells}`);
|
||||||
|
});
|
||||||
|
lines.push("");
|
||||||
|
lines.push(
|
||||||
|
` ${fg(MUTED, "less ")}${GREENS.map((_, level) => heatCell(level, " ")).join("")}${fg(MUTED, " more")}` +
|
||||||
|
fg(MUTED, " · one cell per day, colour = attempts, ▸ marks today"),
|
||||||
|
);
|
||||||
|
|
||||||
|
lines.push("");
|
||||||
|
lines.push(rule("weekly gates", cols));
|
||||||
|
if (s.gates.length === 0) {
|
||||||
|
lines.push(fg(MUTED, " no gate has been scored yet"));
|
||||||
|
}
|
||||||
|
const gateW = Math.max(10, Math.min(24, cols - 30));
|
||||||
|
for (const g of s.gates) {
|
||||||
|
// Gates were stored as an integer percent before they were stored as a
|
||||||
|
// fraction; both are in the table, so normalise on read.
|
||||||
|
const rate = g.pass_rate === null ? null : g.pass_rate <= 1 ? g.pass_rate : g.pass_rate / 100;
|
||||||
|
const color = rate === null ? LINE : rate >= 0.7 ? GREEN : RED;
|
||||||
|
lines.push(
|
||||||
|
` ${fg(FG, cell(`Week ${g.week}`, 9))}` +
|
||||||
|
`${bar(rate ?? 0, 1, gateW, color)}` +
|
||||||
|
` ${fg(color, cell(rate === null ? "open" : `${Math.round(rate * 100)}%`, 5))}` +
|
||||||
|
` ${fg(MUTED, g.issue === null ? "" : `#${g.issue}`)}${fg(MUTED, g.closed_on ? ` closed ${g.closed_on}` : "")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── panes ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const PANES: { name: string; render: (s: Stats, cols: number) => string[] }[] = [
|
||||||
|
{ name: "Overview", render: overview },
|
||||||
|
{ name: "Ladder", render: ladder },
|
||||||
|
{ name: "Concepts", render: concepts },
|
||||||
|
{ name: "Activity", render: activity },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── non-interactive: every pane, once ────────────────────────────
|
||||||
|
|
||||||
|
let stats: Stats;
|
||||||
|
try {
|
||||||
|
stats = await fetchStats();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`stats: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
console.error(`stats: D1 is the only source of SRS truth — nothing to fall back to (SRS_API=${SRS_API}).`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.stdout.isTTY) {
|
||||||
|
const cols = 80;
|
||||||
|
for (const pane of PANES) {
|
||||||
|
console.log(bold(pane.name.toUpperCase()));
|
||||||
|
for (const line of pane.render(stats, cols)) console.log(line);
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── interactive ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let pane = 0;
|
||||||
|
let offset = 0;
|
||||||
|
let note = "";
|
||||||
|
|
||||||
|
// Quitting resolves this; the finally below is what restores the terminal, so
|
||||||
|
// a thrown render never leaves the shell in raw mode on the alternate screen.
|
||||||
|
const { promise: quit, resolve: leave } = Promise.withResolvers<void>();
|
||||||
|
const screen = openScreen(onKey, draw);
|
||||||
|
|
||||||
|
function frame(): string[] {
|
||||||
|
const cols = screen.columns;
|
||||||
|
const tabs = PANES.map((p, i) =>
|
||||||
|
i === pane ? bold(fg(FG, ` ${p.name} `)) : fg(MUTED, ` ${p.name} `),
|
||||||
|
).join(fg(LINE, "│"));
|
||||||
|
const clock = fg(MUTED, `day ${stats.campaign.day}/${stats.campaign.days} · streak ${stats.streak} `);
|
||||||
|
const header = padTo(tabs, Math.max(0, cols - width(clock))) + clock;
|
||||||
|
|
||||||
|
// header + rule + body + footer fills the terminal exactly.
|
||||||
|
const body = PANES[pane]!.render(stats, cols);
|
||||||
|
const height = Math.max(1, screen.rows - 3);
|
||||||
|
offset = Math.max(0, Math.min(offset, Math.max(0, body.length - height)));
|
||||||
|
const scrolled =
|
||||||
|
body.length > height ? ` · ${offset + 1}-${Math.min(offset + height, body.length)}/${body.length}` : "";
|
||||||
|
|
||||||
|
return [
|
||||||
|
header,
|
||||||
|
fg(LINE, "─".repeat(cols)),
|
||||||
|
...body.slice(offset, offset + height),
|
||||||
|
fg(MUTED, `Tab/←→ pane · ↑↓ scroll · r refresh · q quit${scrolled}`) + (note ? ` ${fg(YELLOW, note)}` : ""),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw(): void {
|
||||||
|
screen.draw(frame());
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKey(chunk: string): void {
|
||||||
|
switch (chunk) {
|
||||||
|
// Raw mode delivers Ctrl-C as a byte, not a signal, so it is handled here.
|
||||||
|
case "q":
|
||||||
|
case "\x03":
|
||||||
|
case "\x1b":
|
||||||
|
leave();
|
||||||
|
return;
|
||||||
|
case "\t":
|
||||||
|
case "\x1b[C":
|
||||||
|
case "l":
|
||||||
|
pane = (pane + 1) % PANES.length;
|
||||||
|
offset = 0;
|
||||||
|
break;
|
||||||
|
case "\x1b[Z":
|
||||||
|
case "\x1b[D":
|
||||||
|
case "h":
|
||||||
|
pane = (pane + PANES.length - 1) % PANES.length;
|
||||||
|
offset = 0;
|
||||||
|
break;
|
||||||
|
case "\x1b[A":
|
||||||
|
case "k":
|
||||||
|
offset = Math.max(0, offset - 1);
|
||||||
|
break;
|
||||||
|
case "\x1b[B":
|
||||||
|
case "j":
|
||||||
|
offset += 1;
|
||||||
|
break;
|
||||||
|
case "r":
|
||||||
|
note = "refreshing…";
|
||||||
|
draw();
|
||||||
|
void fetchStats()
|
||||||
|
.then((fresh) => {
|
||||||
|
stats = fresh;
|
||||||
|
note = "";
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
note = `refresh failed: ${err instanceof Error ? err.message : String(err)}`;
|
||||||
|
})
|
||||||
|
.finally(draw);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
if (chunk >= "1" && chunk <= String(PANES.length)) {
|
||||||
|
pane = Number(chunk) - 1;
|
||||||
|
offset = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
draw();
|
||||||
|
await quit;
|
||||||
|
} finally {
|
||||||
|
screen.close();
|
||||||
|
}
|
||||||
+21
-1
@@ -359,6 +359,24 @@ function ensureLabel(page: string, h: Header): string {
|
|||||||
return page.replace(/^sidebar:$/m, `sidebar:\n${label}`);
|
return page.replace(/^sidebar:$/m, `sidebar:\n${label}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offset just past the script-owned `## Solution` section: the next top-level
|
||||||
|
* heading that is not inside a code fence, or end of file. Sections a human
|
||||||
|
* appends after the solution (`## Explanation`, …) survive regeneration.
|
||||||
|
*/
|
||||||
|
function solutionEnd(page: string, from: number): number {
|
||||||
|
const lines = page.slice(from).split("\n");
|
||||||
|
let offset = from + lines[0]!.length + 1; // past the '## Solution' line itself
|
||||||
|
let fenced = false;
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const line = lines[i]!;
|
||||||
|
if (line.startsWith("```")) fenced = !fenced;
|
||||||
|
else if (!fenced && line.startsWith("## ")) return offset;
|
||||||
|
offset += line.length + 1;
|
||||||
|
}
|
||||||
|
return page.length;
|
||||||
|
}
|
||||||
|
|
||||||
// Existing docs pages, keyed by leetcode number from the frontmatter title.
|
// Existing docs pages, keyed by leetcode number from the frontmatter title.
|
||||||
const pages = new Map<number, string>(); // num -> path relative to DOCS
|
const pages = new Map<number, string>(); // num -> path relative to DOCS
|
||||||
for (const rel of (await Array.fromAsync(new Bun.Glob("**/*.mdx").scan(DOCS))).sort()) {
|
for (const rel of (await Array.fromAsync(new Bun.Glob("**/*.mdx").scan(DOCS))).sort()) {
|
||||||
@@ -386,7 +404,9 @@ for (const sol of ordered) {
|
|||||||
report.push([workRef, existing, "ERROR: no '## Solution' heading"]);
|
report.push([workRef, existing, "ERROR: no '## Solution' heading"]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const next = ensureLabel(page.slice(0, at) + solutionSection(sol), sol.header);
|
const tail = page.slice(solutionEnd(page, at));
|
||||||
|
const body = page.slice(0, at) + solutionSection(sol) + (tail ? `\n${tail}` : "");
|
||||||
|
const next = ensureLabel(body, sol.header);
|
||||||
const actions: string[] = [];
|
const actions: string[] = [];
|
||||||
if (existing !== name) {
|
if (existing !== name) {
|
||||||
await rename(join(DOCS, existing), join(DOCS, name));
|
await rename(join(DOCS, existing), join(DOCS, name));
|
||||||
|
|||||||
+214
@@ -0,0 +1,214 @@
|
|||||||
|
/**
|
||||||
|
* Terminal drawing primitives for the stats dashboard: colour, block-glyph
|
||||||
|
* charts, and the alternate-screen plumbing.
|
||||||
|
*
|
||||||
|
* Hand-rolled for the same reason apps/api/src/charts.ts and png.ts are — the
|
||||||
|
* repo carries near-zero runtime dependencies, and everything a progress
|
||||||
|
* dashboard needs is a bar, a sparkline and a coloured cell. The palette is
|
||||||
|
* charts.ts's, so the terminal, the README SVGs and the digest email are the
|
||||||
|
* same picture in three renderers.
|
||||||
|
*
|
||||||
|
* Side-effect-free on import except for reading the colour environment once;
|
||||||
|
* nothing is written to the terminal until openScreen() is called.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── palette (apps/api/src/charts.ts) ─────────────────────────────
|
||||||
|
|
||||||
|
export const FG = "#fafafa"; // zinc-50 text
|
||||||
|
export const MUTED = "#a1a1aa"; // zinc-400 secondary text
|
||||||
|
export const LINE = "#27272a"; // zinc-800 structure / empty track
|
||||||
|
export const GREEN = "#16a34a"; // green-600 (core / pass)
|
||||||
|
export const BLUE = "#2563eb"; // blue-600 (optional)
|
||||||
|
export const YELLOW = "#eab308"; // yellow-500 (attention)
|
||||||
|
export const RED = "#f87171"; // red-400 (fail, on dark)
|
||||||
|
|
||||||
|
/** GitHub dark-mode contribution hues; level 0 is the empty cell. */
|
||||||
|
export const GREENS = ["#27272a", "#0e4429", "#006d32", "#26a641", "#39d353"];
|
||||||
|
|
||||||
|
// ── colour ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Honour NO_COLOR and pipes, and let FORCE_COLOR override both — the dashboard
|
||||||
|
* is legible without colour, and `bun run stats | less -R` should still be.
|
||||||
|
*/
|
||||||
|
export const COLOR =
|
||||||
|
process.env.FORCE_COLOR !== undefined
|
||||||
|
? process.env.FORCE_COLOR !== "0"
|
||||||
|
: process.env.NO_COLOR === undefined && process.stdout.isTTY === true;
|
||||||
|
|
||||||
|
const RESET = "\x1b[0m";
|
||||||
|
|
||||||
|
function channels(hex: string): string {
|
||||||
|
return `${parseInt(hex.slice(1, 3), 16)};${parseInt(hex.slice(3, 5), 16)};${parseInt(hex.slice(5, 7), 16)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fg(hex: string, text: string): string {
|
||||||
|
return COLOR ? `\x1b[38;2;${channels(hex)}m${text}${RESET}` : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bg(hex: string, text: string): string {
|
||||||
|
return COLOR ? `\x1b[48;2;${channels(hex)}m${text}${RESET}` : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bold(text: string): string {
|
||||||
|
return COLOR ? `\x1b[1m${text}${RESET}` : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ANSI = /\x1b\[[0-9;]*m/g;
|
||||||
|
|
||||||
|
/** Printable width — every glyph used here is one cell wide. */
|
||||||
|
export function width(text: string): number {
|
||||||
|
return text.replace(ANSI, "").length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pad or cut a *plain* string to exactly n cells; colour it afterwards. */
|
||||||
|
export function cell(text: string, n: number, align: "left" | "right" = "left"): string {
|
||||||
|
const clipped = text.length > n ? `${text.slice(0, Math.max(0, n - 1))}…` : text;
|
||||||
|
return align === "left" ? clipped.padEnd(n) : clipped.padStart(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pad an already-coloured string to n cells. */
|
||||||
|
export function padTo(text: string, n: number): string {
|
||||||
|
return text + " ".repeat(Math.max(0, n - width(text)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── charts ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const EIGHTHS = ["", "▏", "▎", "▍", "▌", "▋", "▊", "▉"];
|
||||||
|
const SPARKS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filled and empty. The two differ by GLYPH as well as by colour, so a bar
|
||||||
|
* still reads under NO_COLOR or in a pipe — every chart here has to survive
|
||||||
|
* `bun run stats > notes.txt`.
|
||||||
|
*/
|
||||||
|
const FILL = "█";
|
||||||
|
const TRACK = "░";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One bar over a muted track, `cells` wide. Sub-cell remainders use the
|
||||||
|
* eighth-block glyphs, so one solved problem out of 161 still shows.
|
||||||
|
*/
|
||||||
|
export function bar(value: number, total: number, cells: number, color = GREEN): string {
|
||||||
|
const units = Math.round(Math.max(0, Math.min(1, total > 0 ? value / total : 0)) * cells * 8);
|
||||||
|
const filled = FILL.repeat(Math.floor(units / 8)) + EIGHTHS[units % 8]!;
|
||||||
|
return fg(color, filled) + fg(LINE, TRACK.repeat(Math.max(0, cells - filled.length)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Segment {
|
||||||
|
value: number;
|
||||||
|
color: string;
|
||||||
|
/** Defaults to the solid block; pass TRACK-ish glyphs for "not done yet". */
|
||||||
|
glyph?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Segments laid end to end, `total` mapped to `cells`. Anything short of
|
||||||
|
* `total` is left BLANK, not tracked: bars sharing a scale must show their
|
||||||
|
* different lengths. A caller wanting a visible remainder passes it as a
|
||||||
|
* segment of its own.
|
||||||
|
*
|
||||||
|
* Non-zero segments are floored but never vanish; the overshoot that floor
|
||||||
|
* creates is taken back from the widest segment, which can spare it.
|
||||||
|
*/
|
||||||
|
export function stackedBar(segments: Segment[], total: number, cells: number): string {
|
||||||
|
if (total <= 0) return " ".repeat(cells);
|
||||||
|
const widths = segments.map((s) =>
|
||||||
|
s.value > 0 ? Math.max(1, Math.floor((s.value / total) * cells)) : 0,
|
||||||
|
);
|
||||||
|
let over = widths.reduce((a, b) => a + b, 0) - cells;
|
||||||
|
while (over > 0) {
|
||||||
|
const i = widths.indexOf(Math.max(...widths));
|
||||||
|
if (widths[i]! <= 1) break;
|
||||||
|
widths[i]!--;
|
||||||
|
over--;
|
||||||
|
}
|
||||||
|
const drawn = widths.map((n, i) => fg(segments[i]!.color, (segments[i]!.glyph ?? FILL).repeat(n)));
|
||||||
|
return drawn.join("") + " ".repeat(Math.max(0, cells - widths.reduce((a, b) => a + b, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One cell of the campaign heatmap. With colour it is the GREENS ramp the
|
||||||
|
* README and the digest use; without it, the same five levels as shading.
|
||||||
|
*/
|
||||||
|
export function heatCell(attempts: number, body: string): string {
|
||||||
|
const level = Math.min(attempts, GREENS.length - 1);
|
||||||
|
return COLOR ? bg(GREENS[level]!, body) : [" ", "·", "▒", "▓", "█"][level]!.repeat(body.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One glyph per value, scaled to the largest. All-zero renders as a floor. */
|
||||||
|
export function sparkline(values: number[], color = GREEN): string {
|
||||||
|
const peak = Math.max(...values, 0);
|
||||||
|
const line = values
|
||||||
|
.map((v) => (peak === 0 ? SPARKS[0]! : SPARKS[Math.min(7, Math.round((v / peak) * 7))]!))
|
||||||
|
.join("");
|
||||||
|
return fg(color, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── layout ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** A section rule that fills the width: `── name ────────────`. */
|
||||||
|
export function rule(title: string, cells: number): string {
|
||||||
|
const label = ` ${title} `;
|
||||||
|
return (
|
||||||
|
fg(LINE, "──") + fg(MUTED, label) + fg(LINE, "─".repeat(Math.max(0, cells - 2 - label.length)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── screen ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const ALT_ON = "\x1b[?1049h\x1b[?25l";
|
||||||
|
const ALT_OFF = "\x1b[?25h\x1b[?1049l";
|
||||||
|
|
||||||
|
export interface Screen {
|
||||||
|
readonly rows: number;
|
||||||
|
readonly columns: number;
|
||||||
|
/** Draw a frame, clipped to the terminal height. */
|
||||||
|
draw(lines: string[]): void;
|
||||||
|
close(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take over the terminal: alternate buffer, hidden cursor, raw keys. `onKey`
|
||||||
|
* receives the raw chunk (`"\t"`, `"\x1b[B"`, `"q"`, …) — decoding belongs to
|
||||||
|
* the app, which is the only thing that knows what a key means.
|
||||||
|
*
|
||||||
|
* The caller MUST close() in a finally: raw mode survives a thrown exception.
|
||||||
|
*/
|
||||||
|
export function openScreen(onKey: (chunk: string) => void, onResize: () => void): Screen {
|
||||||
|
const out = process.stdout;
|
||||||
|
const input = process.stdin;
|
||||||
|
|
||||||
|
out.write(ALT_ON);
|
||||||
|
input.setRawMode?.(true);
|
||||||
|
input.setEncoding("utf8");
|
||||||
|
input.resume();
|
||||||
|
input.on("data", onKey);
|
||||||
|
out.on("resize", onResize);
|
||||||
|
|
||||||
|
let closed = false;
|
||||||
|
return {
|
||||||
|
get rows() {
|
||||||
|
return out.rows ?? 24;
|
||||||
|
},
|
||||||
|
get columns() {
|
||||||
|
return out.columns ?? 80;
|
||||||
|
},
|
||||||
|
draw(lines: string[]): void {
|
||||||
|
const visible = lines.slice(0, this.rows);
|
||||||
|
// Home, then erase each line as it is rewritten plus everything below the
|
||||||
|
// last one. No full clear, so a redraw does not flash, and no trailing
|
||||||
|
// newline, which would scroll the alternate screen by a row per frame.
|
||||||
|
out.write(`\x1b[H${visible.map((l) => `${l}\x1b[K`).join("\r\n")}\x1b[J`);
|
||||||
|
},
|
||||||
|
close(): void {
|
||||||
|
if (closed) return;
|
||||||
|
closed = true;
|
||||||
|
input.off("data", onKey);
|
||||||
|
out.off("resize", onResize);
|
||||||
|
input.setRawMode?.(false);
|
||||||
|
input.pause();
|
||||||
|
out.write(ALT_OFF);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
+21
-4
@@ -93,6 +93,25 @@ export function scanBucket(bucket: Bucket): Map<number, WorkFile[]> {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is this file a real solution rather than an empty scaffold? The same call
|
||||||
|
* close-solved.ts makes before it closes an issue — a stub under work/1 is a
|
||||||
|
* problem still unsolved, and owes no review.
|
||||||
|
*/
|
||||||
|
export function isSolution(file: WorkFile): boolean {
|
||||||
|
const source = readFileSync(join(WORK, file.rel), "utf8");
|
||||||
|
return isImplemented(source, file.rel.endsWith(".py") ? "py" : "js");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Problems in a bucket with a solution on disk; scaffolds do not count. */
|
||||||
|
export function solvedInBucket(bucket: Bucket): number {
|
||||||
|
let solved = 0;
|
||||||
|
for (const files of scanBucket(bucket).values()) {
|
||||||
|
if (files.some(isSolution)) solved++;
|
||||||
|
}
|
||||||
|
return solved;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LC number -> ET date of the commit that first added a work/ file for it.
|
* LC number -> ET date of the commit that first added a work/ file for it.
|
||||||
*
|
*
|
||||||
@@ -153,14 +172,12 @@ export function reviewQueues(today: string): Map<Window, Owed[]> {
|
|||||||
const dates = firstSolved();
|
const dates = firstSolved();
|
||||||
|
|
||||||
// An empty scaffold under work/1 is a problem still unsolved, not one owing
|
// An empty scaffold under work/1 is a problem still unsolved, not one owing
|
||||||
// a review — the same call close-solved.ts makes before it closes an issue.
|
// a review.
|
||||||
const solved: { file: WorkFile; date: string }[] = [];
|
const solved: { file: WorkFile; date: string }[] = [];
|
||||||
for (const files of scanBucket(1).values()) {
|
for (const files of scanBucket(1).values()) {
|
||||||
const date = dates.get(files[0]!.lc);
|
const date = dates.get(files[0]!.lc);
|
||||||
if (date === undefined) continue; // uncommitted: you only just solved it
|
if (date === undefined) continue; // uncommitted: you only just solved it
|
||||||
const file = files.find((f) =>
|
const file = files.find(isSolution);
|
||||||
isImplemented(readFileSync(join(WORK, f.rel), "utf8"), f.rel.endsWith(".py") ? "py" : "js"),
|
|
||||||
);
|
|
||||||
if (file) solved.push({ file, date });
|
if (file) solved.push({ file, date });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,20 @@ Can you come up with an algorithm that is less than O(n^2) time complexity?
|
|||||||
- `-10^9 <= target <= 10^9`
|
- `-10^9 <= target <= 10^9`
|
||||||
- Only one valid answer exists.
|
- Only one valid answer exists.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["twoSum(nums, target)"]) --> I["seen = {} — value to index"]
|
||||||
|
I --> L{"more (i, num) in nums?"}
|
||||||
|
L -- no --> E(["return []"])
|
||||||
|
L -- yes --> C["complement = target - num"]
|
||||||
|
C --> H{"complement in seen?"}
|
||||||
|
H -- yes --> R(["return [seen[complement], i]"])
|
||||||
|
H -- no --> W["seen[num] = i"]
|
||||||
|
W --> L
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -45,3 +59,7 @@ class Solution:
|
|||||||
seen[num] = i
|
seen[num] = i
|
||||||
return []
|
return []
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="KLlXCFG5TnA" title="Two Sum - Leetcode 1 - HashMap - Python" />
|
||||||
@@ -27,6 +27,22 @@ Notice that you may not slant the container.
|
|||||||
- `2 <= n <= 10^5`
|
- `2 <= n <= 10^5`
|
||||||
- `0 <= height[i] <= 10^4`
|
- `0 <= height[i] <= 10^4`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["maxArea(height)"]) --> I["res = 0, left = 0, right = len - 1"]
|
||||||
|
I --> W{"left < right?"}
|
||||||
|
W -- no --> E(["return res"])
|
||||||
|
W -- yes --> A["area = (right - left) * min(height[left], height[right])"]
|
||||||
|
A --> M["res = max(res, area)"]
|
||||||
|
M --> C{"height[left] < height[right]?"}
|
||||||
|
C -- yes --> L["left += 1 — drop the shorter wall"]
|
||||||
|
C -- no --> R["right -= 1 — drop the shorter wall"]
|
||||||
|
L --> W
|
||||||
|
R --> W
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -46,3 +62,7 @@ class Solution:
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="UuiTKBwPgAo" title="Container with Most Water - Leetcode 11 - Python" />
|
||||||
@@ -23,6 +23,24 @@ sidebar:
|
|||||||
- `1 <= prices.length <= 10^5`
|
- `1 <= prices.length <= 10^5`
|
||||||
- `0 <= prices[i] <= 10^4`
|
- `0 <= prices[i] <= 10^4`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
::::warning
|
||||||
|
The diagram traces the code below literally. That code does not run: `curr` and `ans` are never initialised, so the first `curr > left` test raises `NameError`, and `left` holds a price rather than an index, so `prices[left]` indexes by value.
|
||||||
|
::::
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["maxProfit(prices)"]) --> I["left = min(prices)"]
|
||||||
|
I --> F{"more right in range(len(prices))?"}
|
||||||
|
F -- no --> E(["return ans"])
|
||||||
|
F -- yes --> W{"curr > left?"}
|
||||||
|
W -- yes --> P["curr -= prices[left]; left += 1"]
|
||||||
|
P --> W
|
||||||
|
W -- no --> M["ans = max(ans, curr)"]
|
||||||
|
M --> F
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -36,3 +54,7 @@ class Solution:
|
|||||||
ans = max(ans, curr)
|
ans = max(ans, curr)
|
||||||
return ans
|
return ans
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="1pkOgXD63yU" title="Sliding Window: Best Time to Buy and Sell Stock - Leetcode 121 - Python" />
|
||||||
+14
@@ -26,6 +26,20 @@ sidebar:
|
|||||||
- `2 <= nums.length <= 500`
|
- `2 <= nums.length <= 500`
|
||||||
- `0 <= nums[i] <= 100`
|
- `0 <= nums[i] <= 100`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["smallerNumbersThanCurrent(nums)"]) --> F["freq: count every value in nums"]
|
||||||
|
F --> O["sorted = keys of freq, ascending"]
|
||||||
|
O --> I["count = 0, smaller = {}"]
|
||||||
|
I --> L{"more num in sorted?"}
|
||||||
|
L -- yes --> A["smaller[num] = count — everything already passed is smaller"]
|
||||||
|
A --> B["count += freq[num]"]
|
||||||
|
B --> L
|
||||||
|
L -- no --> M(["return nums.map(n => smaller[n])"])
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|||||||
@@ -37,6 +37,18 @@ startValue = 4 | startValue = 5 | nums
|
|||||||
- `1 <= nums.length <= 100`
|
- `1 <= nums.length <= 100`
|
||||||
- `-100 <= nums[i] <= 100`
|
- `-100 <= nums[i] <= 100`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["minStartValue(nums)"]) --> I["prefix = [nums[0]]"]
|
||||||
|
I --> L{"more i in 1..n-1?"}
|
||||||
|
L -- yes --> P["prefix.push(prefix[i-1] + nums[i])"]
|
||||||
|
P --> L
|
||||||
|
L -- no --> M["min = smallest running total"]
|
||||||
|
M --> R(["return max(1, 1 - min) — lift the dip to 1"])
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|||||||
@@ -23,6 +23,19 @@ sidebar:
|
|||||||
- `1 <= arr.length <= 1000`
|
- `1 <= arr.length <= 1000`
|
||||||
- `0 <= arr[i] <= 1000`
|
- `0 <= arr[i] <= 1000`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["countElements(arr)"]) --> I["arrSet = Set(arr), count = 0"]
|
||||||
|
I --> L{"more n in arr?"}
|
||||||
|
L -- no --> E(["return count"])
|
||||||
|
L -- yes --> H{"arrSet has n + 1?"}
|
||||||
|
H -- yes --> C["count++"]
|
||||||
|
C --> L
|
||||||
|
H -- no --> L
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|||||||
@@ -27,6 +27,17 @@ sidebar:
|
|||||||
- `1 <= nums.length <= 1000`
|
- `1 <= nums.length <= 1000`
|
||||||
- `-10^6 <= nums[i] <= 10^6`
|
- `-10^6 <= nums[i] <= 10^6`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["runningSum(nums)"]) --> I["prefix = [nums[0]]"]
|
||||||
|
I --> L{"more i in 1..n-1?"}
|
||||||
|
L -- yes --> P["prefix.push(prefix[i-1] + nums[i])"]
|
||||||
|
P --> L
|
||||||
|
L -- no --> E(["return prefix"])
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|||||||
@@ -32,6 +32,28 @@ Notice that the solution set must not contain duplicate triplets.
|
|||||||
- `3 <= nums.length <= 3000`
|
- `3 <= nums.length <= 3000`
|
||||||
- `-10^5 <= nums[i] <= 10^5`
|
- `-10^5 <= nums[i] <= 10^5`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["threeSum(nums)"]) --> O["nums.sort() — duplicates become adjacent"]
|
||||||
|
O --> F{"more i in 0..n-1?"}
|
||||||
|
F -- no --> E(["return result"])
|
||||||
|
F -- yes --> D{"nums[i] == nums[i-1]?"}
|
||||||
|
D -- yes --> F
|
||||||
|
D -- no --> P["left = i+1, right = n-1, target = -nums[i]"]
|
||||||
|
P --> W{"left < right?"}
|
||||||
|
W -- no --> F
|
||||||
|
W -- yes --> C["current = nums[left] + nums[right]"]
|
||||||
|
C --> Q{"current vs target"}
|
||||||
|
Q -- equal --> A["append triplet, skip equal neighbours, left += 1, right -= 1"]
|
||||||
|
A --> W
|
||||||
|
Q -- "current < target" --> L["left += 1 — need a bigger sum"]
|
||||||
|
L --> W
|
||||||
|
Q -- "current > target" --> R["right -= 1 — need a smaller sum"]
|
||||||
|
R --> W
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -76,3 +98,7 @@ class Solution:
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="jzZsG8n2R9A" title="3Sum - Leetcode 15 - Python" />
|
||||||
@@ -28,6 +28,21 @@ sidebar:
|
|||||||
- `1 <= tokens.length <= 10^4`
|
- `1 <= tokens.length <= 10^4`
|
||||||
- `tokens[i]` is either an operator: "+", "-", "*", or "/", or an integer in the range [-200, 200].
|
- `tokens[i]` is either an operator: "+", "-", "*", or "/", or an integer in the range [-200, 200].
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["evalRPN(tokens)"]) --> I["stack = [], operations = + - * /"]
|
||||||
|
I --> L{"more token c?"}
|
||||||
|
L -- no --> E(["return stack[0]"])
|
||||||
|
L -- yes --> Q{"c is an operator?"}
|
||||||
|
Q -- no --> N["stack.append(int(c))"]
|
||||||
|
N --> L
|
||||||
|
Q -- yes --> P["y = stack.pop(), x = stack.pop() — order matters"]
|
||||||
|
P --> C["stack.append(operations[c](x, y))"]
|
||||||
|
C --> L
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -53,3 +68,7 @@ class Solution:
|
|||||||
|
|
||||||
return stack[0]
|
return stack[0]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="iu0082c4HDE" title="Evaluate Reverse Polish Notation - Leetcode 150 - Python" />
|
||||||
@@ -35,6 +35,21 @@ Notice that rotating an array [a[0], a[1], a[2], ..., a[n-1]] 1 time results in
|
|||||||
- All the integers of `nums` are unique.
|
- All the integers of `nums` are unique.
|
||||||
- `nums` is sorted and rotated between 1 and `n` times.
|
- `nums` is sorted and rotated between 1 and `n` times.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["findMin(nums)"]) --> I["l = 0, r = n-1, lowest_index = -1"]
|
||||||
|
I --> W{"l <= r?"}
|
||||||
|
W -- no --> E(["return nums[lowest_index]"])
|
||||||
|
W -- yes --> M["m = (l + r) // 2"]
|
||||||
|
M --> C{"nums[m] <= nums[-1]?"}
|
||||||
|
C -- yes --> A["in the right sorted run: record lowest_index = m, r = m - 1"]
|
||||||
|
A --> W
|
||||||
|
C -- no --> B["still in the left sorted run: l = m + 1"]
|
||||||
|
B --> W
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -53,3 +68,7 @@ class Solution:
|
|||||||
|
|
||||||
return nums[lowest_index]
|
return nums[lowest_index]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="nIVW4P8b1VA" title="Find Minimum in Rotated Sorted Array - Binary Search - Leetcode 153 - Python" />
|
||||||
@@ -27,6 +27,18 @@ sidebar:
|
|||||||
- `1 <= nums.length <= 100`
|
- `1 <= nums.length <= 100`
|
||||||
- `-100 <= nums[i] <= 100`
|
- `-100 <= nums[i] <= 100`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["frequencySort(nums)"]) --> F["freq: count every value in nums"]
|
||||||
|
F --> C["sort nums with a two-key comparator"]
|
||||||
|
C --> K1["primary: freq[a] - freq[b] — rarer first"]
|
||||||
|
C --> K2["tie-break: b - a — larger value first"]
|
||||||
|
K1 --> R(["return nums"])
|
||||||
|
K2 --> R
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -40,3 +52,7 @@ var frequencySort = function (nums) {
|
|||||||
return nums.sort((a, b) => freq[a] - freq[b] || b - a);
|
return nums.sort((a, b) => freq[a] - freq[b] || b - a);
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="Evq1SfUbhBg" title="Sort Array by Increasing Frequency - Leetcode 1636 - Python" />
|
||||||
@@ -35,6 +35,22 @@ Your solution must use only constant extra space.
|
|||||||
- `-1000 <= target <= 1000`
|
- `-1000 <= target <= 1000`
|
||||||
- The tests are generated such that there is exactly one solution.
|
- The tests are generated such that there is exactly one solution.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["twoSum(numbers, target)"]) --> I["i = 0, j = len - 1"]
|
||||||
|
I --> W{"i < j?"}
|
||||||
|
W -- no --> E(["return []"])
|
||||||
|
W -- yes --> C["c = numbers[i] + numbers[j]"]
|
||||||
|
C --> Q{"c vs target"}
|
||||||
|
Q -- equal --> R(["return [i+1, j+1] — 1-indexed"])
|
||||||
|
Q -- "c < target" --> L["i += 1 — grow the sum"]
|
||||||
|
L --> W
|
||||||
|
Q -- "c > target" --> H["j -= 1 — shrink the sum"]
|
||||||
|
H --> W
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -53,3 +69,7 @@ class Solution:
|
|||||||
j-=1
|
j-=1
|
||||||
return []
|
return []
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="cQ1Oz4ckceM" title="TWO SUM II - Amazon Coding Interview Question - Leetcode 167 - Python" />
|
||||||
@@ -27,9 +27,29 @@ sidebar:
|
|||||||
- `1 <= nums.length <= 10^5`
|
- `1 <= nums.length <= 10^5`
|
||||||
- `-10^9 <= nums[i] <= 10^9`
|
- `-10^9 <= nums[i] <= 10^9`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["containsDuplicate(nums)"]) --> I["freq = {}"]
|
||||||
|
I --> L{"more n in nums?"}
|
||||||
|
L -- no --> E(["return false"])
|
||||||
|
L -- yes --> C["freq[n] = (freq[n] or 0) + 1"]
|
||||||
|
C --> Q{"freq[n] >= 2?"}
|
||||||
|
Q -- yes --> R(["return true"])
|
||||||
|
Q -- no --> L
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
<CodeGroup>
|
||||||
|
|
||||||
|
```py Python
|
||||||
|
class Solution:
|
||||||
|
def containsDuplicate(self, nums: List[int]) -> bool:
|
||||||
|
```
|
||||||
|
|
||||||
|
```js JavaScript
|
||||||
/**
|
/**
|
||||||
* @param {number[]} nums
|
* @param {number[]} nums
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
@@ -45,3 +65,9 @@ var containsDuplicate = function(nums) {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="3OamzN90kPg" title="Contains Duplicate - Leetcode 217 - Python" />
|
||||||
@@ -27,6 +27,17 @@ Can you solve the problem in O(1) extra space complexity? (The output array does
|
|||||||
- `-30 <= nums[i] <= 30`
|
- `-30 <= nums[i] <= 30`
|
||||||
- The input is generated such that `answer[i]` is guaranteed to fit in a 32-bit integer.
|
- The input is generated such that `answer[i]` is guaranteed to fit in a 32-bit integer.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["productExceptSelf(nums)"]) --> I["answer = ones(n), rightArr = ones(n)"]
|
||||||
|
I --> P1["Pass 1 — left to right: answer[i] = nums[i-1] * answer[i-1]"]
|
||||||
|
P1 --> P2["Pass 2 — right to left: rightArr[i] = nums[i+1] * rightArr[i+1]"]
|
||||||
|
P2 --> P3["Pass 3: answer[i] *= rightArr[i]"]
|
||||||
|
P3 --> E(["return answer — no division needed"])
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -51,3 +62,7 @@ var productExceptSelf = function(nums) {
|
|||||||
return answer;
|
return answer;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="bNvIQI2wAjk" title="Product of Array Except Self - Leetcode 238 - Python" />
|
||||||
@@ -34,6 +34,18 @@ Could you implement a solution using only O(1) extra space complexity and O(n) r
|
|||||||
- `0 <= nums[i] <= n`
|
- `0 <= nums[i] <= n`
|
||||||
- All the numbers of `nums` are unique.
|
- All the numbers of `nums` are unique.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["missingNumber(nums)"]) --> I["numSet = Set(nums), expectedCount = n + 1"]
|
||||||
|
I --> L{"more i in 0..expectedCount-1?"}
|
||||||
|
L -- yes --> Q{"numSet has i?"}
|
||||||
|
Q -- no --> R(["return i"])
|
||||||
|
Q -- yes --> L
|
||||||
|
L -- no --> E(["return -1"])
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -53,3 +65,7 @@ var missingNumber = function(nums) {
|
|||||||
return -1;
|
return -1;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="WnPLSRLSANE" title="Missing Number - Blind 75 - Leetcode 268 - Python" />
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
title: '271. Encode and Decode Strings'
|
||||||
|
description: Design an algorithm to encode a list of strings to a string. The encoded string is then sent over the network and is decoded back to the original list of strings
|
||||||
|
sidebar:
|
||||||
|
label: 'Encode and Decode Strings'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Array</Badge>
|
||||||
|
|
||||||
|
::::warning
|
||||||
|
Could you write a generalized algorithm to work on any possible set of characters?
|
||||||
|
::::
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `dummy_input = ["Hello","World"]`
|
||||||
|
- Output: `["Hello","World"]`
|
||||||
|
- Explanation: Machine `1`: Codec encoder = new Codec(); String msg = encoder.encode(strs); Machine `1` ---msg---> Machine `2` Machine `2`: Codec decoder = new Codec(); `String[] strs` = decoder.decode(msg);
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `dummy_input = [""]`
|
||||||
|
- Output: `[""]`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- `1 <= strs.length <= 200`
|
||||||
|
- `0 <= strs[i].length <= 200`
|
||||||
|
- `strs[i]` contains any possible characters out of 256 valid ASCII characters.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Codec:
|
||||||
|
def encode(self, strs: List[str]) -> str:
|
||||||
|
"""Encodes a list of strings to a single string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def decode(self, s: str) -> List[str]:
|
||||||
|
"""Decodes a single string to a list of strings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Your Codec object will be instantiated and called as such:
|
||||||
|
# codec = Codec()
|
||||||
|
# codec.decode(codec.encode(strs))
|
||||||
|
```
|
||||||
@@ -20,6 +20,21 @@ sidebar:
|
|||||||
- `0 <= left <= right < nums.length`
|
- `0 <= left <= right < nums.length`
|
||||||
- At most `10^4` calls will be made to `sumRange`.
|
- At most `10^4` calls will be made to `sumRange`.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph build["constructor(nums) — O(n) once"]
|
||||||
|
A["prefix = [0]"] --> B{"more n in nums?"}
|
||||||
|
B -- yes --> C["prefix.push(last + n)"]
|
||||||
|
C --> B
|
||||||
|
end
|
||||||
|
build --> Q
|
||||||
|
subgraph Q["sumRange(left, right) — O(1) per call"]
|
||||||
|
D(["return prefix[right + 1] - prefix[left]"])
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -50,3 +65,7 @@ class NumArray {
|
|||||||
* var param_1 = obj.sumRange(left,right)
|
* var param_1 = obj.sumRange(left,right)
|
||||||
*/
|
*/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="2pndAmo_sMA" title="Range Sum Query Immutable - Leetcode 303 - Python" />
|
||||||
@@ -32,6 +32,29 @@ You must write an algorithm with O(log n) runtime complexity.
|
|||||||
- `nums` is an ascending array that is possibly rotated.
|
- `nums` is an ascending array that is possibly rotated.
|
||||||
- `-10^4 <= target <= 10^4`
|
- `-10^4 <= target <= 10^4`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["search(nums, target)"]) --> I["l = 0, r = n - 1"]
|
||||||
|
I --> W{"l <= r?"}
|
||||||
|
W -- no --> E(["return -1"])
|
||||||
|
W -- yes --> M["mid = floor((l + r) / 2)"]
|
||||||
|
M --> F{"nums[mid] == target?"}
|
||||||
|
F -- yes --> R(["return mid"])
|
||||||
|
F -- no --> H{"nums[l] <= nums[mid]? — left half sorted"}
|
||||||
|
H -- yes --> A{"target outside [nums[l], nums[mid]]?"}
|
||||||
|
A -- yes --> A1["l = mid + 1 — search the right half"]
|
||||||
|
A -- no --> A2["r = mid - 1"]
|
||||||
|
H -- no --> B{"target outside [nums[mid], nums[r]]?"}
|
||||||
|
B -- yes --> B1["r = mid - 1 — search the left half"]
|
||||||
|
B -- no --> B2["l = mid + 1"]
|
||||||
|
A1 --> W
|
||||||
|
A2 --> W
|
||||||
|
B1 --> W
|
||||||
|
B2 --> W
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -69,3 +92,7 @@ var search = function(nums, target) {
|
|||||||
return -1;
|
return -1;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="U8XENwh8Oy8" title="Search in rotated sorted array - Leetcode 33 - Python" />
|
||||||
@@ -31,6 +31,16 @@ Your algorithm's time complexity must be better than O(n log n), where n is the
|
|||||||
- `k` is in the range `[1, the number of unique elements in the array]`.
|
- `k` is in the range `[1, the number of unique elements in the array]`.
|
||||||
- It is guaranteed that the answer is unique.
|
- It is guaranteed that the answer is unique.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["topKFrequent(nums, k)"]) --> F["freq: count every value in nums"]
|
||||||
|
F --> K["keys of freq, cast to Number"]
|
||||||
|
K --> O["sort by freq[b] - freq[a] — most frequent first"]
|
||||||
|
O --> R(["return slice(0, k)"])
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -49,3 +59,7 @@ var topKFrequent = function (nums, k) {
|
|||||||
.slice(0, k);
|
.slice(0, k);
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="YPTqKIgVk-k" title="Top K Frequent Elements - Bucket Sort - Leetcode 347 - Python" />
|
||||||
@@ -31,6 +31,22 @@ You must write an algorithm with O(log n) runtime complexity.
|
|||||||
- `nums` contains distinct values sorted in ascending order.
|
- `nums` contains distinct values sorted in ascending order.
|
||||||
- `-10^4 <= target <= 10^4`
|
- `-10^4 <= target <= 10^4`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["searchInsert(nums, target)"]) --> I["l = 0, r = n - 1"]
|
||||||
|
I --> W{"l <= r?"}
|
||||||
|
W -- yes --> M["m = (l + r) // 2"]
|
||||||
|
M --> Q{"nums[m] vs target"}
|
||||||
|
Q -- equal --> R(["return m"])
|
||||||
|
Q -- "nums[m] < target" --> L["l = m + 1"]
|
||||||
|
L --> W
|
||||||
|
Q -- "nums[m] > target" --> H["r = m - 1"]
|
||||||
|
H --> W
|
||||||
|
W -- no --> E(["return (l + r) // 2 + 1 — the insert slot"])
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -49,3 +65,7 @@ class Solution:
|
|||||||
|
|
||||||
return (l + r) // 2 + 1
|
return (l + r) // 2 + 1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="K-RYzDZkzCI" title="Search Insert Position - Binary Search - Leetcode 35 - Python" />
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
title: '48. Rotate Image'
|
||||||
|
description: You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise)
|
||||||
|
sidebar:
|
||||||
|
label: 'Rotate Image'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Matrix Index Math</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `matrix = [[1,2,3],[4,5,6],[7,8,9]]`
|
||||||
|
- Output: `[[7,4,1],[8,5,2],[9,6,3]]`
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]`
|
||||||
|
- Output: `[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- `n == matrix.length == matrix[i].length`
|
||||||
|
- `1 <= n <= 20`
|
||||||
|
- `-1000 <= matrix[i][j] <= 1000`
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Solution:
|
||||||
|
def rotate(self, matrix: List[List[int]]) -> None:
|
||||||
|
n = len(matrix)
|
||||||
|
for r in range(n):
|
||||||
|
for c in range(r + 1, n):
|
||||||
|
matrix[r][c], matrix[c][r] = matrix[c][r], matrix[r][c]
|
||||||
|
|
||||||
|
for r in range(n):
|
||||||
|
matrix[r].reverse()
|
||||||
|
```
|
||||||
@@ -27,6 +27,21 @@ sidebar:
|
|||||||
- `0 <= strs[i].length <= 100`
|
- `0 <= strs[i].length <= 100`
|
||||||
- `strs[i]` consists of lowercase English letters.
|
- `strs[i]` consists of lowercase English letters.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["groupAnagrams(strs)"]) --> K["sorted[i] = letters of strs[i], sorted — the anagram key"]
|
||||||
|
K --> I["anagrams = {}"]
|
||||||
|
I --> L{"more i in 0..n-1?"}
|
||||||
|
L -- no --> E(["return Object.values(anagrams)"])
|
||||||
|
L -- yes --> Q{"anagrams has sorted[i]?"}
|
||||||
|
Q -- no --> N["anagrams[sorted[i]] = [strs[i]]"]
|
||||||
|
Q -- yes --> P["anagrams[sorted[i]].push(strs[i])"]
|
||||||
|
N --> L
|
||||||
|
P --> L
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -49,3 +64,7 @@ var groupAnagrams = function(strs) {
|
|||||||
return Object.values(anagrams);
|
return Object.values(anagrams);
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="vzdNOK2oB2E" title="Group Anagrams - Categorize Strings by Count - Leetcode 49" />
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
title: '54. Spiral Matrix'
|
||||||
|
description: Given an m x n matrix, return all elements of the matrix in spiral order
|
||||||
|
sidebar:
|
||||||
|
label: 'Spiral Matrix'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Matrix Index Math</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `matrix = [[1,2,3],[4,5,6],[7,8,9]]`
|
||||||
|
- Output: `[1,2,3,6,9,8,7,4,5]`
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]`
|
||||||
|
- Output: `[1,2,3,4,8,12,11,10,9,5,6,7]`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- `m == matrix.length`
|
||||||
|
- `n == matrix[i].length`
|
||||||
|
- `1 <= m, n <= 10`
|
||||||
|
- `-100 <= matrix[i][j] <= 100`
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Solution:
|
||||||
|
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
|
||||||
|
res = []
|
||||||
|
top, bottom = 0, len(matrix) - 1
|
||||||
|
left, right = 0, len(matrix[0]) - 1
|
||||||
|
|
||||||
|
while top <= bottom and left <= right:
|
||||||
|
# 1. Top Row
|
||||||
|
for c in range(left, right + 1):
|
||||||
|
res.append(matrix[top][c])
|
||||||
|
top += 1
|
||||||
|
|
||||||
|
# 2. Right Column
|
||||||
|
for r in range(top, bottom + 1):
|
||||||
|
res.append(matrix[r][right])
|
||||||
|
right -= 1
|
||||||
|
|
||||||
|
# 3. Bottom Row
|
||||||
|
if top <= bottom:
|
||||||
|
for c in range(right, left - 1, -1):
|
||||||
|
res.append(matrix[bottom][c])
|
||||||
|
bottom -= 1
|
||||||
|
|
||||||
|
# 4. Left Column
|
||||||
|
if left <= right:
|
||||||
|
for r in range(bottom, top - 1, -1):
|
||||||
|
res.append(matrix[r][left])
|
||||||
|
left += 1
|
||||||
|
|
||||||
|
return res
|
||||||
|
```
|
||||||
@@ -22,6 +22,21 @@ sidebar:
|
|||||||
- `-1000 <= nums[i] <= 1000`
|
- `-1000 <= nums[i] <= 1000`
|
||||||
- `-10^7 <= k <= 10^7`
|
- `-10^7 <= k <= 10^7`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["subarraySum(nums, k)"]) --> I["map = {0: 1} — the empty prefix; sum = 0, count = 0"]
|
||||||
|
I --> L{"more n in nums?"}
|
||||||
|
L -- no --> E(["return count"])
|
||||||
|
L -- yes --> A["sum += n"]
|
||||||
|
A --> Q{"map has sum - k?"}
|
||||||
|
Q -- yes --> C["count += map.get(sum - k) — every earlier prefix that closes a window"]
|
||||||
|
Q -- no --> W["map.set(sum, count of sum + 1)"]
|
||||||
|
C --> W
|
||||||
|
W --> L
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -49,3 +64,7 @@ var subarraySum = function(nums, k) {
|
|||||||
return count
|
return count
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="fFVZt-6sgyo" title="Subarray Sum Equals K - Prefix Sums - Leetcode 560 - Python" />
|
||||||
@@ -29,6 +29,22 @@ You must write an algorithm with O(log n) runtime complexity.
|
|||||||
- All the integers in `nums` are unique.
|
- All the integers in `nums` are unique.
|
||||||
- `nums` is sorted in ascending order.
|
- `nums` is sorted in ascending order.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["search(nums, target)"]) --> I["left = 0, right = n - 1"]
|
||||||
|
I --> W{"left <= right?"}
|
||||||
|
W -- no --> E(["return -1"])
|
||||||
|
W -- yes --> M["mid = left + floor((right - left) / 2)"]
|
||||||
|
M --> Q{"nums[mid] vs target"}
|
||||||
|
Q -- equal --> R(["return mid"])
|
||||||
|
Q -- "nums[mid] < target" --> L["left = mid + 1"]
|
||||||
|
L --> W
|
||||||
|
Q -- "nums[mid] > target" --> H["right = mid - 1"]
|
||||||
|
H --> W
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -54,3 +70,7 @@ var search = function(nums, target) {
|
|||||||
return -1;
|
return -1;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="s4DPM8ct1pI" title="Binary Search - Leetcode 704 - Python" />
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
title: '73. Set Matrix Zeroes'
|
||||||
|
description: Given an m x n integer matrix matrix, if an element is 0, set its entire row and column to 0's
|
||||||
|
sidebar:
|
||||||
|
label: 'Set Matrix Zeroes'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Matrix Index Math</Badge>
|
||||||
|
|
||||||
|
::::warning
|
||||||
|
You must do it in place.
|
||||||
|
::::
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `matrix = [[1,1,1],[1,0,1],[1,1,1]]`
|
||||||
|
- Output: `[[1,0,1],[0,0,0],[1,0,1]]`
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]`
|
||||||
|
- Output: `[[0,0,0,0],[0,4,5,0],[0,3,1,0]]`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- `m == matrix.length`
|
||||||
|
- `n == matrix[0].length`
|
||||||
|
- `1 <= m, n <= 200`
|
||||||
|
- `-2^31 <= matrix[i][j] <= 2^31 - 1`
|
||||||
|
- A straightforward solution using O(mn) space is probably a bad idea.
|
||||||
|
- A simple improvement uses O(m + n) space, but still not the best solution.
|
||||||
|
- Could you devise a constant space solution?
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Solution:
|
||||||
|
def setZeroes(self, matrix: List[List[int]]) -> None:
|
||||||
|
"""
|
||||||
|
Do not return anything, modify matrix in-place instead.
|
||||||
|
"""
|
||||||
|
m, n = len(matrix), len(matrix[0])
|
||||||
|
|
||||||
|
# 1. remember if row 0 or column 0 have their own zeros
|
||||||
|
row0_zero = any(matrix[0][c] == 0 for c in range(n))
|
||||||
|
col0_zero = any(matrix[r][0] == 0 for r in range(m))
|
||||||
|
|
||||||
|
# 2. use row 0 and column 0 as marker lists
|
||||||
|
for r in range(1, m):
|
||||||
|
for c in range(1, n):
|
||||||
|
if matrix[r][c] == 0:
|
||||||
|
matrix[r][0] = 0
|
||||||
|
matrix[0][c] = 0
|
||||||
|
|
||||||
|
# 3. zero the inner cells from the markers
|
||||||
|
for r in range(1, m):
|
||||||
|
for c in range(1, n):
|
||||||
|
if matrix[r][0] == 0 or matrix[0][c] == 0:
|
||||||
|
matrix[r][c] = 0
|
||||||
|
|
||||||
|
# 4. handle row 0 and column 0 last
|
||||||
|
if row0_zero:
|
||||||
|
for c in range(n):
|
||||||
|
matrix[0][c] = 0
|
||||||
|
if col0_zero:
|
||||||
|
for r in range(m):
|
||||||
|
matrix[r][0] = 0
|
||||||
|
```
|
||||||
@@ -25,6 +25,20 @@ sidebar:
|
|||||||
- `1 <= temperatures.length <= 10^5`
|
- `1 <= temperatures.length <= 10^5`
|
||||||
- `30 <= temperatures[i] <= 100`
|
- `30 <= temperatures[i] <= 100`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["dailyTemperatures(temperatures)"]) --> I["ans = zeros(n), stack = [] — indices of days still waiting"]
|
||||||
|
I --> L{"more (i, temp)?"}
|
||||||
|
L -- no --> E(["return ans"])
|
||||||
|
L -- yes --> W{"stack non-empty and temperatures[stack[-1]] < temp?"}
|
||||||
|
W -- yes --> P["prev = stack.pop(); ans[prev] = i - prev — today resolves that day"]
|
||||||
|
P --> W
|
||||||
|
W -- no --> A["stack.append(i)"]
|
||||||
|
A --> L
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -39,3 +53,7 @@ class Solution:
|
|||||||
stack.append(i)
|
stack.append(i)
|
||||||
return ans
|
return ans
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="cTBiBSnjO3c" title="Daily Temperatures - Monotonic Stack - Leetcode 739 - Python" />
|
||||||
@@ -27,6 +27,32 @@ You must write a solution in O(log(m * n)) time complexity.
|
|||||||
- `1 <= m, n <= 100`
|
- `1 <= m, n <= 100`
|
||||||
- `-10^4 <= matrix[i][j], target <= 10^4`
|
- `-10^4 <= matrix[i][j], target <= 10^4`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["searchMatrix(matrix, target)"]) --> I["top = 0, bot = ROWS - 1"]
|
||||||
|
I --> W1{"top <= bot?"}
|
||||||
|
W1 -- yes --> R1["row = (top + bot) // 2"]
|
||||||
|
R1 --> C1{"target vs that row's range"}
|
||||||
|
C1 -- "above matrix[row][-1]" --> U["top = row + 1"]
|
||||||
|
C1 -- "below matrix[row][0]" --> V["bot = row - 1"]
|
||||||
|
C1 -- inside --> B(["break — row found"])
|
||||||
|
U --> W1
|
||||||
|
V --> W1
|
||||||
|
W1 -- no --> F(["return False — no row can hold it"])
|
||||||
|
B --> P["row = (top + bot) // 2; l = 0, r = COLS - 1"]
|
||||||
|
P --> W2{"l <= r?"}
|
||||||
|
W2 -- no --> G(["return False"])
|
||||||
|
W2 -- yes --> M["m = (l + r) // 2"]
|
||||||
|
M --> C2{"target vs matrix[row][m]"}
|
||||||
|
C2 -- greater --> X["l = m + 1"]
|
||||||
|
C2 -- smaller --> Y["r = m - 1"]
|
||||||
|
C2 -- equal --> T(["return True"])
|
||||||
|
X --> W2
|
||||||
|
Y --> W2
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -59,3 +85,7 @@ class Solution:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="Ber2pi2C0j0" title="Search a 2D Matrix - Leetcode 74 - Python" />
|
||||||
@@ -32,6 +32,22 @@ sidebar:
|
|||||||
- All the values of `position` are unique.
|
- All the values of `position` are unique.
|
||||||
- `0 < speed[i] <= 10^6`
|
- `0 < speed[i] <= 10^6`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["carFleet(target, position, speed)"]) --> O["zip and sort by position, descending — nearest the target first"]
|
||||||
|
O --> I["stack = [] — arrival time of each fleet's leader"]
|
||||||
|
I --> L{"more (pos, spd)?"}
|
||||||
|
L -- no --> E(["return len(stack) — one entry per fleet"])
|
||||||
|
L -- yes --> T["time = (target - pos) / spd"]
|
||||||
|
T --> Q{"stack empty or time > stack[-1]?"}
|
||||||
|
Q -- yes --> A["stack.append(time) — slower, so it starts a new fleet"]
|
||||||
|
Q -- no --> B["discard — it catches the car ahead and joins that fleet"]
|
||||||
|
A --> L
|
||||||
|
B --> L
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -45,3 +61,7 @@ class Solution:
|
|||||||
|
|
||||||
return len(stack)
|
return len(stack)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="Pr6T-3yB9RM" title="Car Fleet - Leetcode 853 - Python" />
|
||||||
@@ -26,6 +26,22 @@ sidebar:
|
|||||||
- `piles.length <= h <= 10^9`
|
- `piles.length <= h <= 10^9`
|
||||||
- `1 <= piles[i] <= 10^9`
|
- `1 <= piles[i] <= 10^9`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["minEatingSpeed(piles, h)"]) --> I["l = 1, r = max(piles)"]
|
||||||
|
I --> W{"l < r?"}
|
||||||
|
W -- no --> E(["return l — the smallest workable speed"])
|
||||||
|
W -- yes --> M["mid = floor((l + r) / 2)"]
|
||||||
|
M --> K["kWorks(mid): hours = sum of ceil(p / mid)"]
|
||||||
|
K --> Q{"hours <= h?"}
|
||||||
|
Q -- yes --> A["r = mid — keep mid, try slower"]
|
||||||
|
Q -- no --> B["l = mid + 1 — too slow, speed up"]
|
||||||
|
A --> W
|
||||||
|
B --> W
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -60,3 +76,7 @@ var minEatingSpeed = function(piles, h) {
|
|||||||
return l;
|
return l;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="U2SozAs9RzA" title="Koko Eating Bananas - Binary Search - Leetcode 875 - Python" />
|
||||||
@@ -27,6 +27,21 @@ Squaring each element and sorting the new array is very trivial, could you find
|
|||||||
- `-10^4 <= nums[i] <= 10^4`
|
- `-10^4 <= nums[i] <= 10^4`
|
||||||
- `nums` is sorted in non-decreasing order.
|
- `nums` is sorted in non-decreasing order.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["sortedSquares(nums)"]) --> I["ans = Array(n), left = 0, right = n - 1"]
|
||||||
|
I --> L{"more i from n-1 down to 0?"}
|
||||||
|
L -- no --> E(["return ans"])
|
||||||
|
L -- yes --> C{"abs(nums[left]) < abs(nums[right])?"}
|
||||||
|
C -- yes --> R["square = nums[right]; right--"]
|
||||||
|
C -- no --> F["square = nums[left]; left++"]
|
||||||
|
R --> W["ans[i] = square * square — fill from the back, largest first"]
|
||||||
|
F --> W
|
||||||
|
W --> L
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -53,3 +68,7 @@ var sortedSquares = function(nums) {
|
|||||||
return ans;
|
return ans;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="FPCZsG_AkUg" title="Squares of a Sorted Array - Leetcode 977 - Python" />
|
||||||
@@ -33,6 +33,20 @@ Can you solve it using O(1) (i.e. constant) memory?
|
|||||||
- `-10^5 <= Node.val <= 10^5`
|
- `-10^5 <= Node.val <= 10^5`
|
||||||
- pos is -1 or a valid index in the linked-list.
|
- pos is -1 or a valid index in the linked-list.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["hasCycle(head)"]) --> I["fast = slow = head"]
|
||||||
|
I --> W{"fast and fast.next?"}
|
||||||
|
W -- no --> E(["return False — ran off the end, no cycle"])
|
||||||
|
W -- yes --> A["fast = fast.next.next — two steps"]
|
||||||
|
A --> B["slow = slow.next — one step"]
|
||||||
|
B --> Q{"fast is slow?"}
|
||||||
|
Q -- yes --> R(["return True — the gap closed, so it loops"])
|
||||||
|
Q -- no --> W
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -54,3 +68,7 @@ class Solution:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="gBTe7lFR3vc" title="Linked List Cycle - Floyd's Tortoise and Hare - Leetcode 141 - Python" />
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
---
|
||||||
|
title: '146. LRU Cache'
|
||||||
|
description: Design a data structure that follows the constraints of a Least Recently Used (LRU) cache
|
||||||
|
sidebar:
|
||||||
|
label: 'LRU Cache'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Hybrid Structures</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: ``
|
||||||
|
- Output: ``
|
||||||
|
- Explanation: Input ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"] `[[2]`, [1, 1], [2, 2], `[1]`, [3, 3], `[2]`, [4, 4], `[1]`, `[3]`, [4]] Output [null, null, null, `1`, null, `-1`, null, `-1`, `3`, 4] Explanation LRUCache lRUCache = new LRUCache(2); lRUCache.put(1, `1`); // cache is {1=1} lRUCache.put(2, `2`); // cache is {1=1, 2=2} lRUCache.get(1); // return `1` lRUCache.put(3, `3`); // LRU `key` was `2`, evicts `key 2`, cache is {1=1, 3=3} lRUCache.get(2); // returns `-1` (not found) lRUCache.put(4, `4`); // LRU `key` was `1`, evicts `key 1`, cache is {4=4, 3=3} lRUCache.get(1); // return `-1` (not found) lRUCache.get(3); // return `3` lRUCache.get(4); // return `4`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- `1 <= capacity <= 3000`
|
||||||
|
- `0 <= key <= 10^4`
|
||||||
|
- `0 <= value <= 10^5`
|
||||||
|
- At most 2 * 10^5 calls will be made to get and put.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph state["state — dict for O(1) lookup, doubly linked list for O(1) reorder"]
|
||||||
|
H["head — least recent"] --- N["... nodes ..."] --- T["tail — most recent"]
|
||||||
|
end
|
||||||
|
subgraph get["get(key)"]
|
||||||
|
G1{"key in cache?"} -- no --> G2(["return -1"])
|
||||||
|
G1 -- yes --> G3["_remove_node then _add_node — move it beside the tail"]
|
||||||
|
G3 --> G4(["return node.value"])
|
||||||
|
end
|
||||||
|
subgraph put["put(key, value)"]
|
||||||
|
P1{"key in cache?"} -- yes --> P2["update value, _remove_node then _add_node"]
|
||||||
|
P1 -- no --> P3["new Node, cache[key] = node, _add_node"]
|
||||||
|
P3 --> P4{"len(cache) > capacity?"}
|
||||||
|
P4 -- yes --> P5["evict head.next — the least recent — and del cache[lru.key]"]
|
||||||
|
P4 -- no --> P6(["done"])
|
||||||
|
P2 --> P6
|
||||||
|
P5 --> P6
|
||||||
|
end
|
||||||
|
state --> get
|
||||||
|
state --> put
|
||||||
|
```
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Node:
|
||||||
|
def __init__(self, key=0, value=0):
|
||||||
|
self.key = key
|
||||||
|
self.value = value
|
||||||
|
self.prev = None
|
||||||
|
self.next = None
|
||||||
|
|
||||||
|
|
||||||
|
class LRUCache:
|
||||||
|
def __init__(self, capacity: int):
|
||||||
|
self.capacity = capacity
|
||||||
|
self.cache = {}
|
||||||
|
self.head = Node()
|
||||||
|
self.tail = Node()
|
||||||
|
self.head.next = self.tail # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
self.tail.prev = self.head # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
|
||||||
|
def _remove_node(self, node):
|
||||||
|
prev_node = node.prev
|
||||||
|
next_node = node.next
|
||||||
|
prev_node.next = next_node
|
||||||
|
next_node.prev = prev_node
|
||||||
|
|
||||||
|
def _add_node(self, node):
|
||||||
|
# Insert just before the tail (most recent at end)
|
||||||
|
node.next = self.tail
|
||||||
|
node.prev = self.tail.prev
|
||||||
|
self.tail.prev.next = node # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
self.tail.prev = node
|
||||||
|
|
||||||
|
def get(self, key: int) -> int:
|
||||||
|
if key not in self.cache:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
node = self.cache[key]
|
||||||
|
self._remove_node(node)
|
||||||
|
self._add_node(node)
|
||||||
|
return node.value
|
||||||
|
|
||||||
|
def put(self, key: int, value: int) -> None:
|
||||||
|
if key in self.cache:
|
||||||
|
node = self.cache[key]
|
||||||
|
node.value = value
|
||||||
|
self._remove_node(node)
|
||||||
|
self._add_node(node)
|
||||||
|
else:
|
||||||
|
new_node = Node(key, value)
|
||||||
|
self.cache[key] = new_node
|
||||||
|
self._add_node(new_node)
|
||||||
|
|
||||||
|
if len(self.cache) > self.capacity:
|
||||||
|
lru = self.head.next # Oldest node is next to the head
|
||||||
|
self._remove_node(lru)
|
||||||
|
del self.cache[lru.key] # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
|
||||||
|
|
||||||
|
# Your LRUCache object will be instantiated and called as such:
|
||||||
|
# obj = LRUCache(capacity)
|
||||||
|
# param_1 = obj.get(key)
|
||||||
|
# obj.put(key,value)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="7ABFKPK2hD4" title="LRU Cache - Twitch Interview Question - Leetcode 146" />
|
||||||
@@ -22,6 +22,19 @@ sidebar:
|
|||||||
- `1 <= sentence.length <= 1000`
|
- `1 <= sentence.length <= 1000`
|
||||||
- `sentence` consists of lowercase English letters.
|
- `sentence` consists of lowercase English letters.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["checkIfPangram(sentence)"]) --> I["freq = {}"]
|
||||||
|
I --> L{"more char c?"}
|
||||||
|
L -- yes --> C["freq[c] = (freq[c] or 0) + 1"]
|
||||||
|
C --> L
|
||||||
|
L -- no --> Q{"distinct keys in freq == 26?"}
|
||||||
|
Q -- yes --> R(["return true"])
|
||||||
|
Q -- no --> F(["return false"])
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|||||||
@@ -25,6 +25,19 @@ What if the inputs contain Unicode characters? How would you adapt your solution
|
|||||||
- `1 <= s.length, t.length <= 5 * 10^4`
|
- `1 <= s.length, t.length <= 5 * 10^4`
|
||||||
- `s` and `t` consist of lowercase English letters.
|
- `s` and `t` consist of lowercase English letters.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["isAnagram(s, t)"]) --> I["freq: count every char of s"]
|
||||||
|
I --> L{"more char c in t?"}
|
||||||
|
L -- no --> E(["return true"])
|
||||||
|
L -- yes --> Q{"freq[c] missing or 0?"}
|
||||||
|
Q -- yes --> R(["return false — t has a char s cannot cover"])
|
||||||
|
Q -- no --> D["freq[c] -= 1"]
|
||||||
|
D --> L
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -46,3 +59,7 @@ var isAnagram = function(s, t) {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="9UtInBqnCgA" title="Valid Anagram - Leetcode 242 - Python" />
|
||||||
@@ -28,6 +28,21 @@ sidebar:
|
|||||||
- `0 <= s.length <= 10^5`
|
- `0 <= s.length <= 10^5`
|
||||||
- `s` consists of English letters, digits, symbols and spaces.
|
- `s` consists of English letters, digits, symbols and spaces.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["lengthOfLongestSubstring(s)"]) --> I["left = 0, ans = 0, window = set()"]
|
||||||
|
I --> L{"more (right, c) in s?"}
|
||||||
|
L -- no --> E(["return ans"])
|
||||||
|
L -- yes --> W{"c already in window?"}
|
||||||
|
W -- yes --> P["window.remove(s[left]); left += 1 — shrink until c is free"]
|
||||||
|
P --> W
|
||||||
|
W -- no --> A["window.add(c)"]
|
||||||
|
A --> M["ans = max(ans, right - left + 1)"]
|
||||||
|
M --> L
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -45,3 +60,7 @@ class Solution:
|
|||||||
|
|
||||||
return ans
|
return ans
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="wiGpQwVHdE0" title="Longest Substring Without Repeating Characters - Leetcode 3 - Python" />
|
||||||
@@ -26,6 +26,18 @@ sidebar:
|
|||||||
- `1 <= s.length <= 10^5`
|
- `1 <= s.length <= 10^5`
|
||||||
- `s` consists of only lowercase English letters.
|
- `s` consists of only lowercase English letters.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["firstUniqChar(s)"]) --> F["Pass 1 — freq: count every char of s"]
|
||||||
|
F --> L{"Pass 2 — more i in 0..len-1?"}
|
||||||
|
L -- no --> E(["return -1"])
|
||||||
|
L -- yes --> Q{"freq[s[i]] == 1?"}
|
||||||
|
Q -- yes --> R(["return i — leftmost wins because the scan is in order"])
|
||||||
|
Q -- no --> L
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -45,3 +57,7 @@ var firstUniqChar = function (s) {
|
|||||||
return -1;
|
return -1;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="rBENYgWy3xU" title="First Unique Character in a String - Leetcode 387 - Python" />
|
||||||
@@ -24,6 +24,21 @@ sidebar:
|
|||||||
- `s` consists of only uppercase English letters.
|
- `s` consists of only uppercase English letters.
|
||||||
- `0 <= k <= s.length`
|
- `0 <= k <= s.length`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["characterReplacement(s, k)"]) --> I["count = {}, res = 0, l = 0, maxF = 0"]
|
||||||
|
I --> L{"more (r, c) in s?"}
|
||||||
|
L -- no --> E(["return res"])
|
||||||
|
L -- yes --> A["count[c] += 1; maxF = max(maxF, count[c])"]
|
||||||
|
A --> W{"(r - l + 1) - maxF > k? — too many chars to replace"}
|
||||||
|
W -- yes --> P["count[s[l]] -= 1; l += 1 — shrink from the left"]
|
||||||
|
P --> W
|
||||||
|
W -- no --> M["res = max(res, r - l + 1)"]
|
||||||
|
M --> L
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -45,3 +60,7 @@ class Solution:
|
|||||||
res = max(res, r - l + 1)
|
res = max(res, r - l + 1)
|
||||||
return res
|
return res
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="gqXU1UyA8pk" title="Longest Repeating Character Replacement - Leetcode 424 - Python" />
|
||||||
@@ -28,6 +28,18 @@ sidebar:
|
|||||||
- `1 <= s.length <= 5 * 10^5`
|
- `1 <= s.length <= 5 * 10^5`
|
||||||
- `s` consists of uppercase and lowercase English letters and digits.
|
- `s` consists of uppercase and lowercase English letters and digits.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["frequencySort(s)"]) --> F["freq: count every char of s"]
|
||||||
|
F --> C["split s and sort with a two-key comparator"]
|
||||||
|
C --> K1["primary: freq[b] - freq[a] — most frequent first"]
|
||||||
|
C --> K2["tie-break: a.localeCompare(b) — alphabetical"]
|
||||||
|
K1 --> R(["join and return"])
|
||||||
|
K2 --> R
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -47,3 +59,7 @@ var frequencySort = function (s) {
|
|||||||
.join("");
|
.join("");
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="OXdXc9HTrIg" title="Sort Characters By Frequency - Leetcode 451 - Python" />
|
||||||
@@ -22,6 +22,24 @@ sidebar:
|
|||||||
- `1 <= s1.length, s2.length <= 10^4`
|
- `1 <= s1.length, s2.length <= 10^4`
|
||||||
- `s1` and `s2` consist of lowercase English letters.
|
- `s1` and `s2` consist of lowercase English letters.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["checkInclusion(s1, s2)"]) --> G{"k = len(s1) > len(s2)?"}
|
||||||
|
G -- yes --> X(["return False"])
|
||||||
|
G -- no --> I["need = Counter(s1), window = Counter()"]
|
||||||
|
I --> L{"more (right, c) in s2?"}
|
||||||
|
L -- no --> E(["return False"])
|
||||||
|
L -- yes --> A["window[c] += 1"]
|
||||||
|
A --> B{"right >= k? — window is now longer than k"}
|
||||||
|
B -- yes --> C["drop s2[right - k] from window, deleting the key at 0"]
|
||||||
|
B -- no --> Q
|
||||||
|
C --> Q{"window == need?"}
|
||||||
|
Q -- yes --> R(["return True"])
|
||||||
|
Q -- no --> L
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -49,3 +67,7 @@ class Solution:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="UbyhOgBN834" title="Permutation in String - Leetcode 567 - Python" />
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
title: '2. Add Two Numbers'
|
||||||
|
description: You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each of their nodes contains a single digit. Add the two numbers and return the sum as a linked list
|
||||||
|
sidebar:
|
||||||
|
label: 'Add Two Numbers'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Hybrid Structures</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `l1 = [2,4,3], l2 = [5,6,4]`
|
||||||
|
- Output: `[7,0,8]`
|
||||||
|
- Explanation: `342 + 465 = 807`.
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `l1 = [0], l2 = [0]`
|
||||||
|
- Output: `[0]`
|
||||||
|
|
||||||
|
### Example 3:
|
||||||
|
- Input: `l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]`
|
||||||
|
- Output: `[8,9,9,9,0,0,0,1]`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- The number of nodes in each linked list is in the range [1, 100].
|
||||||
|
- `0 <= Node.val <= 9`
|
||||||
|
- It is guaranteed that the list represents a number that does not have leading zeros.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
# Definition for singly-linked list.
|
||||||
|
# class ListNode:
|
||||||
|
# def __init__(self, val=0, next=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.next = next
|
||||||
|
class Solution:
|
||||||
|
def addTwoNumbers(
|
||||||
|
self, l1: Optional[ListNode], l2: Optional[ListNode]
|
||||||
|
) -> Optional[ListNode]:
|
||||||
|
|
||||||
|
dummy = ListNode()
|
||||||
|
carry = 0
|
||||||
|
curr = dummy
|
||||||
|
while l1 or l2 or carry:
|
||||||
|
s = (l1.val if l1 else 0) + (l2.val if l2 else 0) + carry
|
||||||
|
carry, val = divmod(s, 10)
|
||||||
|
|
||||||
|
curr.next = ListNode(val)
|
||||||
|
curr = curr.next
|
||||||
|
|
||||||
|
l1 = l1.next if l1 else None
|
||||||
|
l2 = l2.next if l2 else None
|
||||||
|
|
||||||
|
return dummy.next
|
||||||
|
```
|
||||||
@@ -29,6 +29,19 @@ A linked list can be reversed either iteratively or recursively. Could you imple
|
|||||||
- The number of nodes in the list is the range [0, 5000].
|
- The number of nodes in the list is the range [0, 5000].
|
||||||
- `-5000 <= Node.val <= 5000`
|
- `-5000 <= Node.val <= 5000`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["reverseList(head)"]) --> I["prev = None, curr = head"]
|
||||||
|
I --> W{"curr?"}
|
||||||
|
W -- no --> E(["return prev — the old tail is the new head"])
|
||||||
|
W -- yes --> A["next_ = curr.next — save it before overwriting"]
|
||||||
|
A --> B["curr.next = prev — flip the link"]
|
||||||
|
B --> C["prev = curr; curr = next_ — step forward"]
|
||||||
|
C --> W
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -48,3 +61,7 @@ class Solution:
|
|||||||
curr = next_
|
curr = next_
|
||||||
return prev
|
return prev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="G0_I-ZF0S38" title="Reverse Linked List - Iterative AND Recursive - Leetcode 206 - Python" />
|
||||||
@@ -26,6 +26,23 @@ sidebar:
|
|||||||
- `-100 <= Node.val <= 100`
|
- `-100 <= Node.val <= 100`
|
||||||
- Both list1 and list2 are sorted in non-decreasing order.
|
- Both list1 and list2 are sorted in non-decreasing order.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["mergeTwoLists(list1, list2)"]) --> G1{"list1 empty?"}
|
||||||
|
G1 -- yes --> X1(["return list2"])
|
||||||
|
G1 -- no --> G2{"list2 empty?"}
|
||||||
|
G2 -- yes --> X2(["return list1"])
|
||||||
|
G2 -- no --> H["head = the smaller first node; advance that list"]
|
||||||
|
H --> C["current = head"]
|
||||||
|
C --> W{"list1 and list2 both non-empty?"}
|
||||||
|
W -- yes --> P["current.next = the smaller node; advance that list; current = current.next"]
|
||||||
|
P --> W
|
||||||
|
W -- no --> T["current.next = list1 or list2 — append the leftover tail"]
|
||||||
|
T --> E(["return head"])
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -64,3 +81,7 @@ class Solution:
|
|||||||
current.next = list1 or list2
|
current.next = list1 or list2
|
||||||
return head
|
return head
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="XIdigk956u0" title="Merge Two Sorted Lists - Leetcode 21 - Python" />
|
||||||
@@ -23,6 +23,28 @@ You must implement a solution with O(1) time complexity for each function.
|
|||||||
- Methods pop, top and getMin operations will always be called on non-empty stacks.
|
- Methods pop, top and getMin operations will always be called on non-empty stacks.
|
||||||
- At most 3 * 10^4 calls will be made to push, pop, top, and getMin.
|
- At most 3 * 10^4 calls will be made to push, pop, top, and getMin.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph state["two parallel stacks — same depth, always"]
|
||||||
|
A["stack — the values"]
|
||||||
|
B["minStack — the minimum as of that depth"]
|
||||||
|
end
|
||||||
|
state --> P
|
||||||
|
subgraph P["push(value)"]
|
||||||
|
P1["stack.append(value)"] --> P2["minStack.append(min(value, minStack[-1]))"]
|
||||||
|
end
|
||||||
|
state --> O
|
||||||
|
subgraph O["pop()"]
|
||||||
|
O1["pop both stacks together"]
|
||||||
|
end
|
||||||
|
state --> R
|
||||||
|
subgraph R["top() / getMin() — O(1)"]
|
||||||
|
R1(["stack[-1] / minStack[-1]"])
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -54,3 +76,7 @@ class MinStack:
|
|||||||
# param_3 = obj.top()
|
# param_3 = obj.top()
|
||||||
# param_4 = obj.getMin()
|
# param_4 = obj.getMin()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="qkLl7nAwDPo" title="Design Min Stack - Amazon Interview Question - Leetcode 155 - Python" />
|
||||||
@@ -33,6 +33,24 @@ sidebar:
|
|||||||
- `1 <= s.length <= 10^4`
|
- `1 <= s.length <= 10^4`
|
||||||
- `s` consists of parentheses only '()[]{}'.
|
- `s` consists of parentheses only '()[]{}'.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["isValid(s)"]) --> I["stack = [], pairs = closer to opener"]
|
||||||
|
I --> L{"more char c?"}
|
||||||
|
L -- no --> Z{"stack empty?"}
|
||||||
|
Z -- yes --> T(["return True"])
|
||||||
|
Z -- no --> U(["return False — unclosed openers left"])
|
||||||
|
L -- yes --> Q{"c is a closer?"}
|
||||||
|
Q -- no --> A["stack.append(c) — it is an opener"]
|
||||||
|
A --> L
|
||||||
|
Q -- yes --> M{"stack non-empty and pairs[c] == stack[-1]?"}
|
||||||
|
M -- yes --> P["stack.pop() — matched"]
|
||||||
|
P --> L
|
||||||
|
M -- no --> F(["return False — mismatch"])
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -52,3 +70,7 @@ class Solution:
|
|||||||
|
|
||||||
return True if not stack else False
|
return True if not stack else False
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="WTzjTskDFMg" title="Valid Parentheses - Stack - Leetcode 20 - Python" />
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
title: '102. Binary Tree Level Order Traversal'
|
||||||
|
description: Given the root of a binary tree, return the level order traversal of its nodes' values. (i.e., from left to right, level by level)
|
||||||
|
sidebar:
|
||||||
|
label: 'Binary Tree Level Order Traversal'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Tree BFS</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `root = [3,9,20,null,null,15,7]`
|
||||||
|
- Output: `[[3],[9,20],[15,7]]`
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `root = [1]`
|
||||||
|
- Output: `[[1]]`
|
||||||
|
|
||||||
|
### Example 3:
|
||||||
|
- Input: `root = []`
|
||||||
|
- Output: `[]`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- The number of nodes in the tree is in the range [0, 2000].
|
||||||
|
- `-1000 <= Node.val <= 1000`
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
# T: O(n) S: O(n)
|
||||||
|
def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
|
||||||
|
res = []
|
||||||
|
|
||||||
|
q = deque()
|
||||||
|
q.append(root)
|
||||||
|
|
||||||
|
while q:
|
||||||
|
qLen = len(q)
|
||||||
|
level = []
|
||||||
|
for i in range(qLen):
|
||||||
|
node = q.popleft()
|
||||||
|
if node:
|
||||||
|
level.append(node.val)
|
||||||
|
q.append(node.left)
|
||||||
|
q.append(node.right)
|
||||||
|
if level:
|
||||||
|
res.append(level)
|
||||||
|
|
||||||
|
return res
|
||||||
|
```
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
title: '104. Maximum Depth of Binary Tree'
|
||||||
|
description: Given the root of a binary tree, return its maximum depth
|
||||||
|
sidebar:
|
||||||
|
label: 'Maximum Depth of Binary Tree'
|
||||||
|
badge: 'Easy'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Tree DFS</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `root = [3,9,20,null,null,15,7]`
|
||||||
|
- Output: `3`
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `root = [1,null,2]`
|
||||||
|
- Output: `2`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- The number of nodes in the tree is in the range [0, 10^4].
|
||||||
|
- `-100 <= Node.val <= 100`
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
class Solution:
|
||||||
|
def maxDepth(self, root: Optional[TreeNode]) -> int:
|
||||||
|
if not root:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))
|
||||||
|
```
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
title: '1448. Count Good Nodes in Binary Tree'
|
||||||
|
description: Given a binary tree root, a node X in the tree is named good if in the path from root to X there are no nodes with a value greater than X
|
||||||
|
sidebar:
|
||||||
|
label: 'Count Good Nodes in Binary Tree'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Tree BFS</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `root = [3,1,4,3,null,1,5]`
|
||||||
|
- Output: `4`
|
||||||
|
- Explanation: Nodes in blue are good. Root Node `(3`) is always a good node. Node `4` -> (3,4) is the maximum value in the path starting from the `root`. Node `5` -> (3,4,5) is the maximum value in the path Node `3` -> (3,1,3) is the maximum value in the path.
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `root = [3,3,null,4,2]`
|
||||||
|
- Output: `3`
|
||||||
|
- Explanation: Node `2` -> `(3`, `3`, `2`) is not good, because `"3`" is higher than it.
|
||||||
|
|
||||||
|
### Example 3:
|
||||||
|
- Input: `root = [1]`
|
||||||
|
- Output: `1`
|
||||||
|
- Explanation: Root is considered as good.
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- The number of nodes in the binary tree is in the range [1, 10^5].
|
||||||
|
- Each node's value is between [-10^4, 10^4].
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
class Solution:
|
||||||
|
def goodNodes(self, root: TreeNode) -> int:
|
||||||
|
def dfs(node, max_val):
|
||||||
|
if not node:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
is_good = 1 if node.val >= max_val else 0
|
||||||
|
new_max = max(max_val, node.val)
|
||||||
|
|
||||||
|
return is_good + dfs(node.left, new_max) + dfs(node.right, new_max)
|
||||||
|
|
||||||
|
return dfs(root, root.val)
|
||||||
|
```
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
title: '199. Binary Tree Right Side View'
|
||||||
|
description: Given the root of a binary tree, imagine yourself standing on the right side of it, return the values of the nodes you can see ordered from top to bottom
|
||||||
|
sidebar:
|
||||||
|
label: 'Binary Tree Right Side View'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Tree BFS</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `root = [1,2,3,null,5,null,4]`
|
||||||
|
- Output: `[1,3,4]`
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `root = [1,2,3,4,null,null,null,5]`
|
||||||
|
- Output: `[1,3,4,5]`
|
||||||
|
|
||||||
|
### Example 3:
|
||||||
|
- Input: `root = [1,null,3]`
|
||||||
|
- Output: `[1,3]`
|
||||||
|
|
||||||
|
### Example 4:
|
||||||
|
- Input: `root = []`
|
||||||
|
- Output: `[]`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- The number of nodes in the tree is in the range [0, 100].
|
||||||
|
- `-100 <= Node.val <= 100`
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
|
||||||
|
res = []
|
||||||
|
q = deque([root])
|
||||||
|
|
||||||
|
while q:
|
||||||
|
rightSide = None
|
||||||
|
qLen = len(q)
|
||||||
|
|
||||||
|
for i in range(qLen):
|
||||||
|
node = q.popleft()
|
||||||
|
if node:
|
||||||
|
rightSide = node
|
||||||
|
q.append(node.left)
|
||||||
|
q.append(node.right)
|
||||||
|
|
||||||
|
if rightSide:
|
||||||
|
res.append(rightSide.val)
|
||||||
|
|
||||||
|
return res
|
||||||
|
```
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
title: '226. Invert Binary Tree'
|
||||||
|
description: Given the root of a binary tree, invert the tree, and return its root
|
||||||
|
sidebar:
|
||||||
|
label: 'Invert Binary Tree'
|
||||||
|
badge: 'Easy'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Tree DFS</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `root = [4,2,7,1,3,6,9]`
|
||||||
|
- Output: `[4,7,2,9,6,3,1]`
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `root = [2,1,3]`
|
||||||
|
- Output: `[2,3,1]`
|
||||||
|
|
||||||
|
### Example 3:
|
||||||
|
- Input: `root = []`
|
||||||
|
- Output: `[]`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- The number of nodes in the tree is in the range [0, 100].
|
||||||
|
- `-100 <= Node.val <= 100`
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
class Solution:
|
||||||
|
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
|
||||||
|
if not root:
|
||||||
|
return None
|
||||||
|
|
||||||
|
left = self.invertTree(root.left)
|
||||||
|
right = self.invertTree(root.right)
|
||||||
|
|
||||||
|
root.left = right
|
||||||
|
root.right = left
|
||||||
|
|
||||||
|
return root
|
||||||
|
```
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
title: '230. Kth Smallest Element in a BST'
|
||||||
|
description: Given the root of a binary search tree, and an integer k, return the k^th smallest value (1-indexed) of all the values of the nodes in the tree
|
||||||
|
sidebar:
|
||||||
|
label: 'Kth Smallest Element in a BST'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Binary Search Tree</Badge>
|
||||||
|
|
||||||
|
::::warning
|
||||||
|
If the BST is modified often (i.e., we can do insert and delete operations) and you need to find the kth smallest frequently, how would you optimize?
|
||||||
|
::::
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `root = [3,1,4,null,2], k = 1`
|
||||||
|
- Output: `1`
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `root = [5,3,6,2,4,null,null,1], k = 3`
|
||||||
|
- Output: `3`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- The number of nodes in the tree is n.
|
||||||
|
- `1 <= k <= n <= 10^4`
|
||||||
|
- `0 <= Node.val <= 10^4`
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
class Solution:
|
||||||
|
def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
|
||||||
|
n = 0
|
||||||
|
stack = []
|
||||||
|
cur = root
|
||||||
|
|
||||||
|
while cur or stack:
|
||||||
|
while cur:
|
||||||
|
stack.append(cur)
|
||||||
|
cur = cur.left
|
||||||
|
|
||||||
|
cur = stack.pop()
|
||||||
|
n += 1
|
||||||
|
if n == k:
|
||||||
|
return cur.val
|
||||||
|
cur = cur.right
|
||||||
|
```
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
title: '235. Lowest Common Ancestor of a Binary Search Tree'
|
||||||
|
description: Given a binary search tree (BST), find the lowest common ancestor (LCA) node of two given nodes in the BST
|
||||||
|
sidebar:
|
||||||
|
label: 'Lowest Common Ancestor of a Binary Search Tree'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Binary Search Tree</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8`
|
||||||
|
- Output: `6`
|
||||||
|
- Explanation: The LCA of nodes `2` and `8` is `6`.
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4`
|
||||||
|
- Output: `2`
|
||||||
|
- Explanation: The LCA of nodes `2` and `4` is `2`, since a node can be a descendant of itself according to the LCA definition.
|
||||||
|
|
||||||
|
### Example 3:
|
||||||
|
- Input: `root = [2,1], p = 2, q = 1`
|
||||||
|
- Output: `2`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- The number of nodes in the tree is in the range [2, 10^5].
|
||||||
|
- `-10^9 <= Node.val <= 10^9`
|
||||||
|
- All Node.val are unique.
|
||||||
|
- `p != q`
|
||||||
|
- p and q will exist in the BST.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, x):
|
||||||
|
# self.val = x
|
||||||
|
# self.left = None
|
||||||
|
# self.right = None
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def lowestCommonAncestor(
|
||||||
|
self, root: "TreeNode", p: "TreeNode", q: "TreeNode"
|
||||||
|
) -> "TreeNode":
|
||||||
|
cur = root
|
||||||
|
while cur:
|
||||||
|
if p.val < cur.val and q.val < cur.val:
|
||||||
|
cur = cur.left
|
||||||
|
elif p.val > cur.val and q.val > cur.val:
|
||||||
|
cur = cur.right
|
||||||
|
else:
|
||||||
|
return cur
|
||||||
|
```
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
title: '236. Lowest Common Ancestor of a Binary Tree'
|
||||||
|
description: Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree
|
||||||
|
sidebar:
|
||||||
|
label: 'Lowest Common Ancestor of a Binary Tree'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Tree DFS</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1`
|
||||||
|
- Output: `3`
|
||||||
|
- Explanation: The LCA of nodes `5` and `1` is `3`.
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4`
|
||||||
|
- Output: `5`
|
||||||
|
- Explanation: The LCA of nodes `5` and `4` is `5`, since a node can be a descendant of itself according to the LCA definition.
|
||||||
|
|
||||||
|
### Example 3:
|
||||||
|
- Input: `root = [1,2], p = 1, q = 2`
|
||||||
|
- Output: `1`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- The number of nodes in the tree is in the range [2, 10^5].
|
||||||
|
- `-10^9 <= Node.val <= 10^9`
|
||||||
|
- All Node.val are unique.
|
||||||
|
- `p != q`
|
||||||
|
- p and q will exist in the tree.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, x):
|
||||||
|
# self.val = x
|
||||||
|
# self.left = None
|
||||||
|
# self.right = None
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def lowestCommonAncestor(
|
||||||
|
self, root: "TreeNode", p: "TreeNode", q: "TreeNode"
|
||||||
|
) -> "TreeNode":
|
||||||
|
|
||||||
|
if not root:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if root == p or root == q:
|
||||||
|
return root
|
||||||
|
|
||||||
|
l = self.lowestCommonAncestor(root.left, p, q)
|
||||||
|
r = self.lowestCommonAncestor(root.right, p, q)
|
||||||
|
|
||||||
|
if l and r:
|
||||||
|
return root
|
||||||
|
else:
|
||||||
|
return l or r
|
||||||
|
```
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
title: '98. Validate Binary Search Tree'
|
||||||
|
description: Given the root of a binary tree, determine if it is a valid binary search tree (BST)
|
||||||
|
sidebar:
|
||||||
|
label: 'Validate Binary Search Tree'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Binary Search Tree</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `root = [2,1,3]`
|
||||||
|
- Output: `true`
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `root = [5,1,4,null,null,3,6]`
|
||||||
|
- Output: `false`
|
||||||
|
- Explanation: The `root` node's value is `5` but its right child's value is `4`.
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- The number of nodes in the tree is in the range [1, 10^4].
|
||||||
|
- `-2^31 <= Node.val <= 2^31 - 1`
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
class Solution:
|
||||||
|
def isValidBST(self, root: Optional[TreeNode]) -> bool:
|
||||||
|
def valid(node, low, high):
|
||||||
|
if not node:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not (low < node.val < high):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return valid(node.left, low, node.val) and valid(node.right, node.val, high)
|
||||||
|
|
||||||
|
return valid(root, float("-inf"), float("inf"))
|
||||||
|
```
|
||||||
@@ -28,6 +28,20 @@ sidebar:
|
|||||||
- `1 <= s.length <= 2 * 10^5`
|
- `1 <= s.length <= 2 * 10^5`
|
||||||
- `s` consists only of printable ASCII characters.
|
- `s` consists only of printable ASCII characters.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["isPalindrome(s)"]) --> N["normal = s stripped of non-alphanumerics, lowercased"]
|
||||||
|
N --> I["i = 0, j = normal.length - 1"]
|
||||||
|
I --> W{"i < j?"}
|
||||||
|
W -- no --> E(["return true — the pointers met in the middle"])
|
||||||
|
W -- yes --> Q{"normal[i] != normal[j]?"}
|
||||||
|
Q -- yes --> F(["return false"])
|
||||||
|
Q -- no --> A["i++; j-- — step both inward"]
|
||||||
|
A --> W
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -49,3 +63,7 @@ var isPalindrome = function(s) {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="jJXJ16kPFWg" title="Valid Palindrome - Leetcode 125 - Python" />
|
||||||
@@ -25,6 +25,18 @@ You must do this by modifying the input array in-place with O(1) extra memory.
|
|||||||
- `1 <= s.length <= 10^5`
|
- `1 <= s.length <= 10^5`
|
||||||
- `s[i]` is a printable ascii character.
|
- `s[i]` is a printable ascii character.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["reverseString(s)"]) --> I["i = 0, j = s.length - 1"]
|
||||||
|
I --> W{"i < j?"}
|
||||||
|
W -- no --> E(["done — s was reversed in place, nothing returned"])
|
||||||
|
W -- yes --> A["swap s[i] and s[j]"]
|
||||||
|
A --> B["i++; j--"]
|
||||||
|
B --> W
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -43,3 +55,7 @@ var reverseString = function(s) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="_d0T_2Lk2qA" title="Reverse String - 3 Ways - Leetcode 344 - Python" />
|
||||||
@@ -26,6 +26,22 @@ Suppose there are lots of incoming s, say s1, s2, ..., sk where k >= 10^9, and y
|
|||||||
- `0 <= t.length <= 10^4`
|
- `0 <= t.length <= 10^4`
|
||||||
- `s` and `t` consist only of lowercase English letters.
|
- `s` and `t` consist only of lowercase English letters.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S(["isSubsequence(s, t)"]) --> G{"len(s) > len(t)?"}
|
||||||
|
G -- yes --> X(["return False"])
|
||||||
|
G -- no --> I["i = 0 into s, j = 0 into t"]
|
||||||
|
I --> W{"i < len(s) and j < len(t)?"}
|
||||||
|
W -- no --> E(["return i == len(s) — every char of s was matched"])
|
||||||
|
W -- yes --> Q{"s[i] == t[j]?"}
|
||||||
|
Q -- yes --> A["i += 1 — consume the match"]
|
||||||
|
Q -- no --> B["j += 1 — always advance t"]
|
||||||
|
A --> B
|
||||||
|
B --> W
|
||||||
|
```
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -42,3 +58,7 @@ class Solution:
|
|||||||
|
|
||||||
return i == len(s)
|
return i == len(s)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
<YouTube id="99RVfqklbCE" title="Is Subsequence - Leetcode 392" />
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: Introduction
|
title: Overview
|
||||||
description: Welcome to your new Blume docs.
|
description: Live SRS campaign dashboard — phases, ladder, review queue, gates, and recent attempts, fetched from the SRS Worker.
|
||||||
---
|
---
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Welcome to **Blume** — markdown-first docs powered by Astro and Vite.
|
Live view of the 56-day campaign. Everything below is fetched client-side
|
||||||
|
from the SRS Worker, so it is always current — no rebuild required.
|
||||||
|
|
||||||
Edit `docs/index.mdx` to get started, then run `blume dev`.
|
<ProgressDashboard />
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
title: Progress
|
|
||||||
description: Live SRS campaign dashboard — phases, ladder, review queue, gates, and recent attempts, fetched from the SRS Worker.
|
|
||||||
---
|
|
||||||
|
|
||||||
Live view of the 56-day campaign. Everything below is fetched client-side
|
|
||||||
from the SRS Worker, so it is always current — no rebuild required.
|
|
||||||
|
|
||||||
<ProgressDashboard />
|
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
---
|
||||||
|
hidden: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# todo — draining the review backlog
|
||||||
|
|
||||||
|
every solution in `work/1` owes a blind re-solve at `work/3` and a second one at `work/7`. both buckets are empty, so 36 problems owe 72 re-solves and not one has been started. snapshot: `reviewqueues("2026-08-31")` from `apps/cli/work.ts` — what `bun run pick` shows — cross-checked against `/api/stats`.
|
||||||
|
|
||||||
|
at 10 problems a day *including* the campaign's own load, 72 re-solves do not fit in one week: this week absorbs 38, the `+3` rung finishes **sat sep 5** (campaign day 20), the last `+7` clears **fri sep 18** (day 33), and normal srs resumes **sat sep 19** — see [when standard srs resumes](#when-standard-srs-resumes). saturday gates are deliberately left out of the count.
|
||||||
|
|
||||||
|
## rules this plan follows
|
||||||
|
|
||||||
|
- **≤ 10 problems a day, everything counted.** each day's new core set and the on-time reviews it earns are booked first; the backlog takes what is left — 7 slots on a light day, 2 on a heavy one.
|
||||||
|
- **coverage before depth.** all 36 `+3` re-solves come first — one blind pass over everything — before any problem gets its `+7`.
|
||||||
|
- **oldest first.** lc 387 and lc 1365 have been sitting 50 days.
|
||||||
|
- **≥ 3 days between a problem's own `+3` and `+7`.** two re-solves in one day test nothing.
|
||||||
|
- **sunday is not a slot.** `workingday()` never mints one; neither does this list.
|
||||||
|
- **re-solve blind, then push.** `bun run pick` (tab cycles to the 3-day and 7-day sections) scaffolds straight into `work/3`/`work/7`. do not open `work/1` first. pushing the file is the log: `close-solved.yml` posts the bucket to `/admin/solved` and d1 moves the problem up the ladder.
|
||||||
|
|
||||||
|
## this week
|
||||||
|
|
||||||
|
### mon aug 31 — 3 new + 7 backlog = 10/10
|
||||||
|
|
||||||
|
*topic 08 — linked list ([#10](https://github.com/prdlk/leetcode/issues/10), closed today)*
|
||||||
|
|
||||||
|
| bucket | problem | difficulty | where | why |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `work/1` | [lc 21 · merge two sorted lists](https://github.com/prdlk/leetcode/issues/78) | easy | linked list | done today |
|
||||||
|
| `work/1` | [lc 206 · reverse linked list](https://github.com/prdlk/leetcode/issues/77) | easy | linked list | done today |
|
||||||
|
| `work/1` | [lc 141 · linked list cycle](https://github.com/prdlk/leetcode/issues/79) | easy | hash table | done today |
|
||||||
|
| `work/3` | lc 387 · first unique character in a string | easy | hash table | 50d stale — first solved 2026-07-12 |
|
||||||
|
| `work/3` | lc 1365 · how many numbers are smaller than the current number | easy | array | 50d stale — first solved 2026-07-12 |
|
||||||
|
| `work/3` | lc 268 · missing number | easy | array | 14d stale — first solved 2026-08-17 |
|
||||||
|
| `work/3` | lc 344 · reverse string | easy | two pointers | 14d stale — first solved 2026-08-17 |
|
||||||
|
| `work/3` | lc 1426 · counting elements | easy | array | 14d stale — first solved 2026-08-17 |
|
||||||
|
| `work/3` | lc 1832 · check if the sentence is pangram | easy | hash table | 14d stale — first solved 2026-08-17 |
|
||||||
|
| `work/3` | [lc 15 · 3sum](https://github.com/prdlk/leetcode/issues/43) | medium | array | 13d stale — first solved 2026-08-18 |
|
||||||
|
|
||||||
|
### tue sep 1 — 2 new + 8 backlog = 10/10
|
||||||
|
|
||||||
|
*topic 09 — hybrid structures ([#11](https://github.com/prdlk/leetcode/issues/11))*
|
||||||
|
|
||||||
|
| bucket | problem | difficulty | where | why |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `work/1` | [lc 146 · lru cache](https://github.com/prdlk/leetcode/issues/87) | medium | topic 09 — hybrid structures | new core problem |
|
||||||
|
| `work/1` | [lc 2 · add two numbers](https://github.com/prdlk/leetcode/issues/88) | medium | topic 09 — hybrid structures | new core problem |
|
||||||
|
| `work/3` | [lc 49 · group anagrams](https://github.com/prdlk/leetcode/issues/30) | medium | array | 13d stale — first solved 2026-08-18 |
|
||||||
|
| `work/3` | [lc 125 · valid palindrome](https://github.com/prdlk/leetcode/issues/41) | easy | two pointers | 13d stale — first solved 2026-08-18 |
|
||||||
|
| `work/3` | [lc 167 · two sum ii - input array is sorted](https://github.com/prdlk/leetcode/issues/42) | medium | array | 13d stale — first solved 2026-08-18 |
|
||||||
|
| `work/3` | [lc 217 · contains duplicate](https://github.com/prdlk/leetcode/issues/27) | easy | array | 13d stale — first solved 2026-08-18 |
|
||||||
|
| `work/3` | [lc 238 · product of array except self](https://github.com/prdlk/leetcode/issues/37) | medium | array | 13d stale — first solved 2026-08-18 |
|
||||||
|
| `work/3` | [lc 242 · valid anagram](https://github.com/prdlk/leetcode/issues/28) | easy | hash table | 13d stale — first solved 2026-08-18 |
|
||||||
|
| `work/3` | [lc 303 · range sum query - immutable](https://github.com/prdlk/leetcode/issues/36) | easy | array | 13d stale — first solved 2026-08-18 |
|
||||||
|
| `work/3` | [lc 560 · subarray sum equals k](https://github.com/prdlk/leetcode/issues/38) | medium | array | 13d stale — first solved 2026-08-18 |
|
||||||
|
|
||||||
|
### wed sep 2 — 3 new + 7 backlog = 10/10
|
||||||
|
|
||||||
|
*topic 10 — matrix index math ([#12](https://github.com/prdlk/leetcode/issues/12))*
|
||||||
|
|
||||||
|
| bucket | problem | difficulty | where | why |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `work/1` | [lc 48 · rotate image](https://github.com/prdlk/leetcode/issues/91) | medium | topic 10 — matrix index math | new core problem |
|
||||||
|
| `work/1` | [lc 54 · spiral matrix](https://github.com/prdlk/leetcode/issues/92) | medium | topic 10 — matrix index math | new core problem |
|
||||||
|
| `work/1` | [lc 73 · set matrix zeroes](https://github.com/prdlk/leetcode/issues/93) | medium | topic 10 — matrix index math | new core problem |
|
||||||
|
| `work/3` | lc 1413 · minimum value to get positive step by step sum | easy | array | 13d stale — first solved 2026-08-18 |
|
||||||
|
| `work/3` | lc 1480 · running sum of 1d array | easy | array | 13d stale — first solved 2026-08-18 |
|
||||||
|
| `work/3` | [lc 33 · search in rotated sorted array](https://github.com/prdlk/leetcode/issues/60) | medium | array | 12d stale — first solved 2026-08-19 |
|
||||||
|
| `work/3` | [lc 704 · binary search](https://github.com/prdlk/leetcode/issues/58) | easy | array | 12d stale — first solved 2026-08-19 |
|
||||||
|
| `work/3` | [lc 875 · koko eating bananas](https://github.com/prdlk/leetcode/issues/59) | medium | array | 12d stale — first solved 2026-08-19 |
|
||||||
|
| `work/3` | [lc 977 · squares of a sorted array](https://github.com/prdlk/leetcode/issues/46) | easy | array | 12d stale — first solved 2026-08-19 |
|
||||||
|
| `work/3` | [lc 121 · best time to buy and sell stock](https://github.com/prdlk/leetcode/issues/50) | easy | array | 11d stale — first solved 2026-08-20 |
|
||||||
|
|
||||||
|
### thu sep 3 — 3 new + 3 on-time reviews + 4 backlog = 10/10
|
||||||
|
|
||||||
|
*topic 11 — tree dfs ([#13](https://github.com/prdlk/leetcode/issues/13))*
|
||||||
|
|
||||||
|
| bucket | problem | difficulty | where | why |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `work/1` | [lc 226 · invert binary tree](https://github.com/prdlk/leetcode/issues/96) | easy | topic 11 — tree dfs | new core problem |
|
||||||
|
| `work/1` | [lc 104 · maximum depth of binary tree](https://github.com/prdlk/leetcode/issues/97) | easy | topic 11 — tree dfs | new core problem |
|
||||||
|
| `work/1` | [lc 236 · lowest common ancestor of a binary tree](https://github.com/prdlk/leetcode/issues/98) | medium | topic 11 — tree dfs | new core problem |
|
||||||
|
| `work/3` | [lc 21 · merge two sorted lists](https://github.com/prdlk/leetcode/issues/78) | easy | linked list | on time — solved mon aug 31 |
|
||||||
|
| `work/3` | [lc 206 · reverse linked list](https://github.com/prdlk/leetcode/issues/77) | easy | linked list | on time — solved mon aug 31 |
|
||||||
|
| `work/3` | [lc 141 · linked list cycle](https://github.com/prdlk/leetcode/issues/79) | easy | hash table | on time — solved mon aug 31 |
|
||||||
|
| `work/3` | [lc 1 · two sum](https://github.com/prdlk/leetcode/issues/29) | easy | array | 7d stale — first solved 2026-08-24 |
|
||||||
|
| `work/3` | [lc 11 · container with most water](https://github.com/prdlk/leetcode/issues/44) | medium | array | 7d stale — first solved 2026-08-24 |
|
||||||
|
| `work/3` | [lc 392 · is subsequence](https://github.com/prdlk/leetcode/issues/45) | easy | two pointers | 7d stale — first solved 2026-08-24 |
|
||||||
|
| `work/3` | [lc 3 · longest substring without repeating characters](https://github.com/prdlk/leetcode/issues/51) | medium | hash table | 6d stale — first solved 2026-08-25 |
|
||||||
|
|
||||||
|
### fri sep 4 — 3 new + 2 on-time reviews + 5 backlog = 10/10
|
||||||
|
|
||||||
|
*topic 12 — tree bfs ([#14](https://github.com/prdlk/leetcode/issues/14))*
|
||||||
|
|
||||||
|
| bucket | problem | difficulty | where | why |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `work/1` | [lc 102 · binary tree level order traversal](https://github.com/prdlk/leetcode/issues/106) | medium | topic 12 — tree bfs | new core problem |
|
||||||
|
| `work/1` | [lc 199 · binary tree right side view](https://github.com/prdlk/leetcode/issues/107) | medium | topic 12 — tree bfs | new core problem |
|
||||||
|
| `work/1` | [lc 1448 · count good nodes in binary tree](https://github.com/prdlk/leetcode/issues/108) | medium | topic 12 — tree bfs | new core problem |
|
||||||
|
| `work/3` | [lc 146 · lru cache](https://github.com/prdlk/leetcode/issues/87) | medium | topic 09 — hybrid structures | on time — solved tue sep 1 |
|
||||||
|
| `work/3` | [lc 2 · add two numbers](https://github.com/prdlk/leetcode/issues/88) | medium | topic 09 — hybrid structures | on time — solved tue sep 1 |
|
||||||
|
| `work/3` | [lc 424 · longest repeating character replacement](https://github.com/prdlk/leetcode/issues/52) | medium | hash table | 6d stale — first solved 2026-08-25 |
|
||||||
|
| `work/3` | [lc 567 · permutation in string](https://github.com/prdlk/leetcode/issues/53) | medium | hash table | 6d stale — first solved 2026-08-25 |
|
||||||
|
| `work/3` | [lc 35 · search insert position](https://github.com/prdlk/leetcode/issues/63) | easy | array | 5d stale — first solved 2026-08-26 |
|
||||||
|
| `work/3` | [lc 74 · search a 2d matrix](https://github.com/prdlk/leetcode/issues/61) | medium | array | 5d stale — first solved 2026-08-26 |
|
||||||
|
| `work/3` | [lc 153 · find minimum in rotated sorted array](https://github.com/prdlk/leetcode/issues/62) | medium | array | 5d stale — first solved 2026-08-26 |
|
||||||
|
|
||||||
|
### sat sep 5 — 3 on-time reviews + 7 backlog = 10/10
|
||||||
|
|
||||||
|
*no new topic — saturday.*
|
||||||
|
|
||||||
|
| bucket | problem | difficulty | where | why |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `work/3` | [lc 48 · rotate image](https://github.com/prdlk/leetcode/issues/91) | medium | topic 10 — matrix index math | on time — solved wed sep 2 |
|
||||||
|
| `work/3` | [lc 54 · spiral matrix](https://github.com/prdlk/leetcode/issues/92) | medium | topic 10 — matrix index math | on time — solved wed sep 2 |
|
||||||
|
| `work/3` | [lc 73 · set matrix zeroes](https://github.com/prdlk/leetcode/issues/93) | medium | topic 10 — matrix index math | on time — solved wed sep 2 |
|
||||||
|
| `work/3` | [lc 20 · valid parentheses](https://github.com/prdlk/leetcode/issues/67) | easy | string | 4d stale — first solved 2026-08-27 |
|
||||||
|
| `work/3` | [lc 150 · evaluate reverse polish notation](https://github.com/prdlk/leetcode/issues/69) | medium | array | 4d stale — first solved 2026-08-27 |
|
||||||
|
| `work/3` | [lc 155 · min stack](https://github.com/prdlk/leetcode/issues/68) | medium | stack | 4d stale — first solved 2026-08-27 |
|
||||||
|
| `work/3` | [lc 739 · daily temperatures](https://github.com/prdlk/leetcode/issues/72) | medium | array | 3d stale — first solved 2026-08-28 |
|
||||||
|
| `work/3` | [lc 853 · car fleet](https://github.com/prdlk/leetcode/issues/73) | medium | array | 3d stale — first solved 2026-08-28 |
|
||||||
|
| `work/7` | lc 387 · first unique character in a string | easy | hash table | `+3` was mon aug 31 |
|
||||||
|
| `work/7` | lc 1365 · how many numbers are smaller than the current number | easy | array | `+3` was mon aug 31 |
|
||||||
|
|
||||||
|
### sun sep 6 — rest day
|
||||||
|
|
||||||
|
nothing. by design.
|
||||||
|
|
||||||
|
## carry-over
|
||||||
|
|
||||||
|
the 34 re-solves that do not fit this week, at the same cap and the same rules. booked = that day's new core set plus the on-time reviews it owes.
|
||||||
|
|
||||||
|
| day | topic | booked | backlog re-solves | load |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| **mon sep 7** | topic 13 — binary search tree | 10 | *none — the day is already full* | 10/10 |
|
||||||
|
| **tue sep 8** | topic 14 — heap / priority queue | 7 | `work/7` — lc 268, lc 344, lc 1426 | 10/10 |
|
||||||
|
| **wed sep 9** | topic 15 — trie | 5 | `work/7` — lc 1832, lc 15, lc 49, lc 125, lc 167 | 10/10 |
|
||||||
|
| **thu sep 10** | topic 16 — graph dfs | 9 | `work/7` — lc 217 | 10/10 |
|
||||||
|
| **fri sep 11** | topic 17 — graph bfs | 8 | `work/7` — lc 238, lc 242 | 10/10 |
|
||||||
|
| **sat sep 12** | *none — saturday* | 2 | `work/7` — lc 303, lc 560, lc 1413, lc 1480, lc 33, lc 704, lc 875, lc 977 | 10/10 |
|
||||||
|
| **mon sep 14** | topic 18 — topological sort | 10 | *none — the day is already full* | 10/10 |
|
||||||
|
| **tue sep 15** | topic 19 — union-find | 5 | `work/7` — lc 121, lc 1, lc 11, lc 392, lc 3 | 10/10 |
|
||||||
|
| **wed sep 16** | topic 20 — backtracking | 5 | `work/7` — lc 424, lc 567, lc 35, lc 74, lc 153 | 10/10 |
|
||||||
|
| **thu sep 17** | topic 21 — 1-d dynamic programming | 8 | `work/7` — lc 20, lc 150 | 10/10 |
|
||||||
|
| **fri sep 18** | topic 22 — multi-d / grid dp | 7 | `work/7` — lc 155, lc 739, lc 853 | 10/10 |
|
||||||
|
|
||||||
|
## when standard srs resumes
|
||||||
|
|
||||||
|
- **sat sep 5** (campaign day 20) — the `+3` rung is empty. every one of the 36 has been recalled blind once; nothing is unretrieved any more.
|
||||||
|
- **fri sep 18** (campaign day 33) — the last backlog `+7` lands. `work/3` and `work/7` hold a file for all 36, so `reviewqueues()` owes nothing; the 28 that have a d1 row are `retired` on the ladder, and the 8 listed at the bottom of this file never had one.
|
||||||
|
- **sat sep 19 onward — standard srs.** the only reviews left are the ones the campaign itself minted: solve a problem, its `+3` and `+7` come back on `workingday(solve + 3)` and `workingday(solve + 7)`, the digest asks for ≤ `review_cap` (3) a day, `levelreviews()` pushes any excess forward, and `bun run pick`'s 3-day and 7-day sections show only what is due. no dated plan needed after this — the digest *is* the plan.
|
||||||
|
|
||||||
|
campaign-native load per working day once the backlog is gone, all of it inside the cap:
|
||||||
|
|
||||||
|
| day | topic | new | `work/3` | `work/7` | load |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| **sat sep 19** | *review-only* | 0 | 3 | 0 | 3/10 |
|
||||||
|
| **mon sep 21** | topic 23 — greedy | 3 | 5 | 2 | 10/10 |
|
||||||
|
| **tue sep 22** | topic 24 — intervals | 3 | 1 | 2 | 6/10 |
|
||||||
|
| **wed sep 23** | *review-only* | 0 | 0 | 3 | 3/10 |
|
||||||
|
| **thu sep 24** | *review-only* | 0 | 3 | 3 | 6/10 |
|
||||||
|
| **fri sep 25** | *review-only* | 0 | 3 | 3 | 6/10 |
|
||||||
|
| **sat sep 26** | *review-only* | 0 | 0 | 0 | 0/10 |
|
||||||
|
| **mon sep 28** | *review-only* | 0 | 0 | 3 | 3/10 |
|
||||||
|
| **tue sep 29** | *review-only* | 0 | 0 | 3 | 3/10 |
|
||||||
|
|
||||||
|
two things to watch there. **mon sep 21 is 10/10 on campaign load alone** — topic 23's core set, two `+7`s, and five of the six `+3`s that land there (thursday's three slide off sunday onto monday, friday's three land natively, and the sixth is pushed to tuesday exactly as `levelreviews()` would) — so one slipped day re-opens a backlog. and from **wed sep 30** the ladder runs dry: `apps/api/data/schedule.json` books its last topic tue sep 22, so the review-only weeks that close the campaign are drills, optional sets and the weekly gates, none of which this file counts.
|
||||||
|
|
||||||
|
## not reviews
|
||||||
|
|
||||||
|
three july scaffolds in `work/1` are still empty, so they owe no re-solve — they are unsolved problems, and `bun run pick` still lists them in its new-problem section:
|
||||||
|
|
||||||
|
| problem | difficulty |
|
||||||
|
| --- | --- |
|
||||||
|
| [lc 347 · top k frequent elements](https://github.com/prdlk/leetcode/issues/31) | medium |
|
||||||
|
| lc 451 · sort characters by frequency | medium |
|
||||||
|
| lc 1636 · sort array by increasing frequency | easy |
|
||||||
|
|
||||||
|
## what this plan exposes
|
||||||
|
|
||||||
|
- **8 of the 36 have no problem issue** (387, 1365, 268, 344, 1426, 1832, 1413, 1480) — pre-campaign solves. the catalog reconcile never invents rows, so d1 does not know them and no digest can ever surface them. `bun run pick` is the only thing that will ask for these reviews.
|
||||||
|
- **`/api/stats` reports a stage that does not exist**: `ladder: { new: 128, "+3": 3, "+7": 0, retired: 0, "+2": 30 }`. `+2` is not in `stages` (`apps/api/src/srs.ts`), so 30 rows sit on a pre-`0002` rung — which is why the digest queue plateaus at 32 due instead of tracking the tables above. that wants a migration, not a re-solve.
|
||||||
@@ -46,7 +46,8 @@ interface Stats {
|
|||||||
}[];
|
}[];
|
||||||
streak: number;
|
streak: number;
|
||||||
queue: { date: string; due: number }[];
|
queue: { date: string; due: number }[];
|
||||||
recent: { date: string; attempts: number; passes: number }[];
|
/** Every campaign day, zeros included; this page reads only the tail. */
|
||||||
|
heat: { date: string; attempts: number; passes: number }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function isStats(value: unknown): value is Stats {
|
function isStats(value: unknown): value is Stats {
|
||||||
@@ -60,7 +61,7 @@ function isStats(value: unknown): value is Stats {
|
|||||||
Array.isArray(v.phases) &&
|
Array.isArray(v.phases) &&
|
||||||
Array.isArray(v.gates) &&
|
Array.isArray(v.gates) &&
|
||||||
Array.isArray(v.queue) &&
|
Array.isArray(v.queue) &&
|
||||||
Array.isArray(v.recent) &&
|
Array.isArray(v.heat) &&
|
||||||
typeof v.streak === "number"
|
typeof v.streak === "number"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -332,11 +333,14 @@ const GREENS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function Recent({ stats }: { stats: Stats }) {
|
function Recent({ stats }: { stats: Stats }) {
|
||||||
|
// heat spans the whole campaign, future days and all. The window wanted here
|
||||||
|
// is the seven days ending today, and campaign.day is that day, 1-based.
|
||||||
|
const week = stats.heat.slice(Math.max(0, stats.campaign.day - 7), stats.campaign.day);
|
||||||
return (
|
return (
|
||||||
<div style={card}>
|
<div style={card}>
|
||||||
<div style={heading}>Last 7 days</div>
|
<div style={heading}>Last 7 days</div>
|
||||||
<div style={{ display: "flex", gap: "0.4rem", flexWrap: "wrap" }}>
|
<div style={{ display: "flex", gap: "0.4rem", flexWrap: "wrap" }}>
|
||||||
{stats.recent.map((r) => (
|
{week.map((r) => (
|
||||||
<span
|
<span
|
||||||
key={r.date}
|
key={r.date}
|
||||||
title={`${r.date}: ${r.passes}/${r.attempts} passed`}
|
title={`${r.date}: ${r.passes}/${r.attempts} passed`}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"api:deploy": "cd apps/api && bun run deploy",
|
"api:deploy": "cd apps/api && bun run deploy",
|
||||||
"api:test": "cd apps/api && bun test src",
|
"api:test": "cd apps/api && bun test src",
|
||||||
"pick": "bun apps/cli/pick.ts",
|
"pick": "bun apps/cli/pick.ts",
|
||||||
|
"stats": "bun apps/cli/stats.ts",
|
||||||
"sync": "bun apps/cli/sync.ts",
|
"sync": "bun apps/cli/sync.ts",
|
||||||
"submit": "bun apps/cli/submit.ts",
|
"submit": "bun apps/cli/submit.ts",
|
||||||
"test": "bun apps/cli/test.ts"
|
"test": "bun apps/cli/test.ts"
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
217. Contains Duplicate
|
||||||
|
Difficulty: Easy
|
||||||
|
https://leetcode.com/problems/contains-duplicate/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given an integer array nums, return true if any value appears at
|
||||||
|
least twice in the array, and return false if every element is
|
||||||
|
distinct.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: nums = [1,2,3,1]
|
||||||
|
|
||||||
|
Output: true
|
||||||
|
|
||||||
|
Explanation:
|
||||||
|
|
||||||
|
The element 1 occurs at the indices 0 and 3.
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: nums = [1,2,3,4]
|
||||||
|
|
||||||
|
Output: false
|
||||||
|
|
||||||
|
Explanation:
|
||||||
|
|
||||||
|
All elements are distinct.
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: nums = [1,1,1,3,3,4,3,2,4,2]
|
||||||
|
|
||||||
|
Output: true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• 1 <= nums.length <= 10^5
|
||||||
|
|
||||||
|
• -10^9 <= nums[i] <= 10^9
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def containsDuplicate(self, nums: List[int]) -> bool:
|
||||||
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""
|
||||||
|
104. Maximum Depth of Binary Tree
|
||||||
|
Difficulty: Easy
|
||||||
|
https://leetcode.com/problems/maximum-depth-of-binary-tree/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given the root of a binary tree, return its maximum depth.
|
||||||
|
|
||||||
|
A binary tree's maximum depth is the number of nodes along the
|
||||||
|
longest path from the root node down to the farthest leaf node.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: root = [3,9,20,null,null,15,7]
|
||||||
|
Output: 3
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: root = [1,null,2]
|
||||||
|
Output: 2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• The number of nodes in the tree is in the range [0, 10^4].
|
||||||
|
|
||||||
|
• -100 <= Node.val <= 100
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
class Solution:
|
||||||
|
def maxDepth(self, root: Optional[TreeNode]) -> int:
|
||||||
|
if not root:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
226. Invert Binary Tree
|
||||||
|
Difficulty: Easy
|
||||||
|
https://leetcode.com/problems/invert-binary-tree/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given the root of a binary tree, invert the tree, and return its root.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: root = [4,2,7,1,3,6,9]
|
||||||
|
Output: [4,7,2,9,6,3,1]
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: root = [2,1,3]
|
||||||
|
Output: [2,3,1]
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: root = []
|
||||||
|
Output: []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• The number of nodes in the tree is in the range [0, 100].
|
||||||
|
|
||||||
|
• -100 <= Node.val <= 100
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
class Solution:
|
||||||
|
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
|
||||||
|
if not root:
|
||||||
|
return None
|
||||||
|
|
||||||
|
left = self.invertTree(root.left)
|
||||||
|
right = self.invertTree(root.right)
|
||||||
|
|
||||||
|
root.left = right
|
||||||
|
root.right = left
|
||||||
|
|
||||||
|
return root
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
"""
|
||||||
|
271. Encode and Decode Strings
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/encode-and-decode-strings/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Design an algorithm to encode a list of strings to a string. The
|
||||||
|
encoded string is then sent over the network and is decoded back to
|
||||||
|
the original list of strings.
|
||||||
|
|
||||||
|
Machine 1 (sender) has the function:
|
||||||
|
|
||||||
|
string encode(vector<string> strs) {
|
||||||
|
// ... your code
|
||||||
|
return encoded_string;
|
||||||
|
}
|
||||||
|
Machine 2 (receiver) has the function:
|
||||||
|
|
||||||
|
vector<string> decode(string s) {
|
||||||
|
//... your code
|
||||||
|
return strs;
|
||||||
|
}
|
||||||
|
|
||||||
|
So Machine 1 does:
|
||||||
|
|
||||||
|
string encoded_string = encode(strs);
|
||||||
|
|
||||||
|
and Machine 2 does:
|
||||||
|
|
||||||
|
vector<string> strs2 = decode(encoded_string);
|
||||||
|
|
||||||
|
strs2 in Machine 2 should be the same as strs in Machine 1.
|
||||||
|
|
||||||
|
Implement the encode and decode methods.
|
||||||
|
|
||||||
|
You are not allowed to solve the problem using any serialize methods
|
||||||
|
(such as eval).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: dummy_input = ["Hello","World"]
|
||||||
|
Output: ["Hello","World"]
|
||||||
|
Explanation:
|
||||||
|
Machine 1:
|
||||||
|
Codec encoder = new Codec();
|
||||||
|
String msg = encoder.encode(strs);
|
||||||
|
Machine 1 ---msg---> Machine 2
|
||||||
|
|
||||||
|
Machine 2:
|
||||||
|
Codec decoder = new Codec();
|
||||||
|
String[] strs = decoder.decode(msg);
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: dummy_input = [""]
|
||||||
|
Output: [""]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• 1 <= strs.length <= 200
|
||||||
|
|
||||||
|
• 0 <= strs[i].length <= 200
|
||||||
|
|
||||||
|
• strs[i] contains any possible characters out of 256 valid ASCII
|
||||||
|
characters.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Follow up: Could you write a generalized algorithm to work on any
|
||||||
|
possible set of characters?
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Codec:
|
||||||
|
def encode(self, strs: List[str]) -> str:
|
||||||
|
"""Encodes a list of strings to a single string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def decode(self, s: str) -> List[str]:
|
||||||
|
"""Decodes a single string to a list of strings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Your Codec object will be instantiated and called as such:
|
||||||
|
# codec = Codec()
|
||||||
|
# codec.decode(codec.encode(strs))
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"""
|
||||||
|
48. Rotate Image
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/rotate-image/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
You are given an n x n 2D matrix representing an image, rotate the
|
||||||
|
image by 90 degrees (clockwise).
|
||||||
|
|
||||||
|
You have to rotate the image in-place, which means you have to modify
|
||||||
|
the input 2D matrix directly. DO NOT allocate another 2D matrix and do
|
||||||
|
the rotation.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: matrix = [[1,2,3],[4,5,6],[7,8,9]]
|
||||||
|
Output: [[7,4,1],[8,5,2],[9,6,3]]
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
|
||||||
|
Output: [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• n == matrix.length == matrix[i].length
|
||||||
|
|
||||||
|
• 1 <= n <= 20
|
||||||
|
|
||||||
|
• -1000 <= matrix[i][j] <= 1000
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def rotate(self, matrix: List[List[int]]) -> None:
|
||||||
|
n = len(matrix)
|
||||||
|
for r in range(n):
|
||||||
|
for c in range(r + 1, n):
|
||||||
|
matrix[r][c], matrix[c][r] = matrix[c][r], matrix[r][c]
|
||||||
|
|
||||||
|
for r in range(n):
|
||||||
|
matrix[r].reverse()
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
"""
|
||||||
|
54. Spiral Matrix
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/spiral-matrix/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given an m x n matrix, return all elements of the matrix in spiral
|
||||||
|
order.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: matrix = [[1,2,3],[4,5,6],[7,8,9]]
|
||||||
|
Output: [1,2,3,6,9,8,7,4,5]
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
|
||||||
|
Output: [1,2,3,4,8,12,11,10,9,5,6,7]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• m == matrix.length
|
||||||
|
|
||||||
|
• n == matrix[i].length
|
||||||
|
|
||||||
|
• 1 <= m, n <= 10
|
||||||
|
|
||||||
|
• -100 <= matrix[i][j] <= 100
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
|
||||||
|
res = []
|
||||||
|
top, bottom = 0, len(matrix) - 1
|
||||||
|
left, right = 0, len(matrix[0]) - 1
|
||||||
|
|
||||||
|
while top <= bottom and left <= right:
|
||||||
|
# 1. Top Row
|
||||||
|
for c in range(left, right + 1):
|
||||||
|
res.append(matrix[top][c])
|
||||||
|
top += 1
|
||||||
|
|
||||||
|
# 2. Right Column
|
||||||
|
for r in range(top, bottom + 1):
|
||||||
|
res.append(matrix[r][right])
|
||||||
|
right -= 1
|
||||||
|
|
||||||
|
# 3. Bottom Row
|
||||||
|
if top <= bottom:
|
||||||
|
for c in range(right, left - 1, -1):
|
||||||
|
res.append(matrix[bottom][c])
|
||||||
|
bottom -= 1
|
||||||
|
|
||||||
|
# 4. Left Column
|
||||||
|
if left <= right:
|
||||||
|
for r in range(bottom, top - 1, -1):
|
||||||
|
res.append(matrix[r][left])
|
||||||
|
left += 1
|
||||||
|
|
||||||
|
return res
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
"""
|
||||||
|
73. Set Matrix Zeroes
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/set-matrix-zeroes/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given an m x n integer matrix matrix, if an element is 0, set its
|
||||||
|
entire row and column to 0's.
|
||||||
|
|
||||||
|
You must do it in place.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: matrix = [[1,1,1],[1,0,1],[1,1,1]]
|
||||||
|
Output: [[1,0,1],[0,0,0],[1,0,1]]
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
|
||||||
|
Output: [[0,0,0,0],[0,4,5,0],[0,3,1,0]]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• m == matrix.length
|
||||||
|
|
||||||
|
• n == matrix[0].length
|
||||||
|
|
||||||
|
• 1 <= m, n <= 200
|
||||||
|
|
||||||
|
• -2^31 <= matrix[i][j] <= 2^31 - 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Follow up:
|
||||||
|
|
||||||
|
• A straightforward solution using O(mn) space is probably a bad
|
||||||
|
idea.
|
||||||
|
|
||||||
|
• A simple improvement uses O(m + n) space, but still not the best
|
||||||
|
solution.
|
||||||
|
|
||||||
|
• Could you devise a constant space solution?
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def setZeroes(self, matrix: List[List[int]]) -> None:
|
||||||
|
"""
|
||||||
|
Do not return anything, modify matrix in-place instead.
|
||||||
|
"""
|
||||||
|
m, n = len(matrix), len(matrix[0])
|
||||||
|
|
||||||
|
# 1. remember if row 0 or column 0 have their own zeros
|
||||||
|
row0_zero = any(matrix[0][c] == 0 for c in range(n))
|
||||||
|
col0_zero = any(matrix[r][0] == 0 for r in range(m))
|
||||||
|
|
||||||
|
# 2. use row 0 and column 0 as marker lists
|
||||||
|
for r in range(1, m):
|
||||||
|
for c in range(1, n):
|
||||||
|
if matrix[r][c] == 0:
|
||||||
|
matrix[r][0] = 0
|
||||||
|
matrix[0][c] = 0
|
||||||
|
|
||||||
|
# 3. zero the inner cells from the markers
|
||||||
|
for r in range(1, m):
|
||||||
|
for c in range(1, n):
|
||||||
|
if matrix[r][0] == 0 or matrix[0][c] == 0:
|
||||||
|
matrix[r][c] = 0
|
||||||
|
|
||||||
|
# 4. handle row 0 and column 0 last
|
||||||
|
if row0_zero:
|
||||||
|
for c in range(n):
|
||||||
|
matrix[0][c] = 0
|
||||||
|
if col0_zero:
|
||||||
|
for r in range(m):
|
||||||
|
matrix[r][0] = 0
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
"""
|
||||||
|
146. LRU Cache
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/lru-cache/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Design a data structure that follows the constraints of a Least
|
||||||
|
Recently Used (LRU) cache.
|
||||||
|
|
||||||
|
Implement the LRUCache class:
|
||||||
|
|
||||||
|
• LRUCache(int capacity) Initialize the LRU cache with positive size
|
||||||
|
capacity.
|
||||||
|
|
||||||
|
• int get(int key) Return the value of the key if the key exists,
|
||||||
|
otherwise return -1.
|
||||||
|
|
||||||
|
• void put(int key, int value) Update the value of the key if the
|
||||||
|
key exists. Otherwise, add the key-value pair to the cache. If the
|
||||||
|
number of keys exceeds the capacity from this operation, evict the
|
||||||
|
least recently used key.
|
||||||
|
|
||||||
|
The functions get and put must each run in O(1) average time
|
||||||
|
complexity.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input
|
||||||
|
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get",
|
||||||
|
"get"]
|
||||||
|
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
|
||||||
|
Output
|
||||||
|
[null, null, null, 1, null, -1, null, -1, 3, 4]
|
||||||
|
|
||||||
|
Explanation
|
||||||
|
LRUCache lRUCache = new LRUCache(2);
|
||||||
|
lRUCache.put(1, 1); // cache is {1=1}
|
||||||
|
lRUCache.put(2, 2); // cache is {1=1, 2=2}
|
||||||
|
lRUCache.get(1); // return 1
|
||||||
|
lRUCache.put(3, 3); // LRU key was 2, evicts key 2, cache is {1=1,
|
||||||
|
3=3}
|
||||||
|
lRUCache.get(2); // returns -1 (not found)
|
||||||
|
lRUCache.put(4, 4); // LRU key was 1, evicts key 1, cache is {4=4,
|
||||||
|
3=3}
|
||||||
|
lRUCache.get(1); // return -1 (not found)
|
||||||
|
lRUCache.get(3); // return 3
|
||||||
|
lRUCache.get(4); // return 4
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• 1 <= capacity <= 3000
|
||||||
|
|
||||||
|
• 0 <= key <= 10^4
|
||||||
|
|
||||||
|
• 0 <= value <= 10^5
|
||||||
|
|
||||||
|
• At most 2 * 10^5 calls will be made to get and put.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Node:
|
||||||
|
def __init__(self, key=0, value=0):
|
||||||
|
self.key = key
|
||||||
|
self.value = value
|
||||||
|
self.prev = None
|
||||||
|
self.next = None
|
||||||
|
|
||||||
|
|
||||||
|
class LRUCache:
|
||||||
|
def __init__(self, capacity: int):
|
||||||
|
self.capacity = capacity
|
||||||
|
self.cache = {}
|
||||||
|
self.head = Node()
|
||||||
|
self.tail = Node()
|
||||||
|
self.head.next = self.tail # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
self.tail.prev = self.head # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
|
||||||
|
def _remove_node(self, node):
|
||||||
|
prev_node = node.prev
|
||||||
|
next_node = node.next
|
||||||
|
prev_node.next = next_node
|
||||||
|
next_node.prev = prev_node
|
||||||
|
|
||||||
|
def _add_node(self, node):
|
||||||
|
# Insert just before the tail (most recent at end)
|
||||||
|
node.next = self.tail
|
||||||
|
node.prev = self.tail.prev
|
||||||
|
self.tail.prev.next = node # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
self.tail.prev = node
|
||||||
|
|
||||||
|
def get(self, key: int) -> int:
|
||||||
|
if key not in self.cache:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
node = self.cache[key]
|
||||||
|
self._remove_node(node)
|
||||||
|
self._add_node(node)
|
||||||
|
return node.value
|
||||||
|
|
||||||
|
def put(self, key: int, value: int) -> None:
|
||||||
|
if key in self.cache:
|
||||||
|
node = self.cache[key]
|
||||||
|
node.value = value
|
||||||
|
self._remove_node(node)
|
||||||
|
self._add_node(node)
|
||||||
|
else:
|
||||||
|
new_node = Node(key, value)
|
||||||
|
self.cache[key] = new_node
|
||||||
|
self._add_node(new_node)
|
||||||
|
|
||||||
|
if len(self.cache) > self.capacity:
|
||||||
|
lru = self.head.next # Oldest node is next to the head
|
||||||
|
self._remove_node(lru)
|
||||||
|
del self.cache[lru.key] # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
|
||||||
|
|
||||||
|
# Your LRUCache object will be instantiated and called as such:
|
||||||
|
# obj = LRUCache(capacity)
|
||||||
|
# param_1 = obj.get(key)
|
||||||
|
# obj.put(key,value)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"""
|
||||||
|
2. Add Two Numbers
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/add-two-numbers/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
You are given two non-empty linked lists representing two
|
||||||
|
non-negative integers. The digits are stored in reverse order, and
|
||||||
|
each of their nodes contains a single digit. Add the two numbers and
|
||||||
|
return the sum as a linked list.
|
||||||
|
|
||||||
|
You may assume the two numbers do not contain any leading zero,
|
||||||
|
except the number 0 itself.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: l1 = [2,4,3], l2 = [5,6,4]
|
||||||
|
Output: [7,0,8]
|
||||||
|
Explanation: 342 + 465 = 807.
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: l1 = [0], l2 = [0]
|
||||||
|
Output: [0]
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
|
||||||
|
Output: [8,9,9,9,0,0,0,1]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• The number of nodes in each linked list is in the range [1, 100].
|
||||||
|
|
||||||
|
• 0 <= Node.val <= 9
|
||||||
|
|
||||||
|
• It is guaranteed that the list represents a number that does not
|
||||||
|
have leading zeros.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Definition for singly-linked list.
|
||||||
|
# class ListNode:
|
||||||
|
# def __init__(self, val=0, next=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.next = next
|
||||||
|
class Solution:
|
||||||
|
def addTwoNumbers(
|
||||||
|
self, l1: Optional[ListNode], l2: Optional[ListNode]
|
||||||
|
) -> Optional[ListNode]:
|
||||||
|
|
||||||
|
dummy = ListNode()
|
||||||
|
carry = 0
|
||||||
|
curr = dummy
|
||||||
|
while l1 or l2 or carry:
|
||||||
|
s = (l1.val if l1 else 0) + (l2.val if l2 else 0) + carry
|
||||||
|
carry, val = divmod(s, 10)
|
||||||
|
|
||||||
|
curr.next = ListNode(val)
|
||||||
|
curr = curr.next
|
||||||
|
|
||||||
|
l1 = l1.next if l1 else None
|
||||||
|
l2 = l2.next if l2 else None
|
||||||
|
|
||||||
|
return dummy.next
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
"""
|
||||||
|
102. Binary Tree Level Order Traversal
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/binary-tree-level-order-traversal/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given the root of a binary tree, return the level order traversal of
|
||||||
|
its nodes' values. (i.e., from left to right, level by level).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: root = [3,9,20,null,null,15,7]
|
||||||
|
Output: [[3],[9,20],[15,7]]
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: root = [1]
|
||||||
|
Output: [[1]]
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: root = []
|
||||||
|
Output: []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• The number of nodes in the tree is in the range [0, 2000].
|
||||||
|
|
||||||
|
• -1000 <= Node.val <= 1000
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
# T: O(n) S: O(n)
|
||||||
|
def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
|
||||||
|
res = []
|
||||||
|
|
||||||
|
q = deque()
|
||||||
|
q.append(root)
|
||||||
|
|
||||||
|
while q:
|
||||||
|
qLen = len(q)
|
||||||
|
level = []
|
||||||
|
for i in range(qLen):
|
||||||
|
node = q.popleft()
|
||||||
|
if node:
|
||||||
|
level.append(node.val)
|
||||||
|
q.append(node.left)
|
||||||
|
q.append(node.right)
|
||||||
|
if level:
|
||||||
|
res.append(level)
|
||||||
|
|
||||||
|
return res
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
"""
|
||||||
|
1448. Count Good Nodes in Binary Tree
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/count-good-nodes-in-binary-tree/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given a binary tree root, a node X in the tree is named good if in
|
||||||
|
the path from root to X there are no nodes with a value greater than
|
||||||
|
X.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Return the number of good nodes in the binary tree.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Input: root = [3,1,4,3,null,1,5]
|
||||||
|
Output: 4
|
||||||
|
Explanation: Nodes in blue are good.
|
||||||
|
Root Node (3) is always a good node.
|
||||||
|
Node 4 -> (3,4) is the maximum value in the path starting from the
|
||||||
|
root.
|
||||||
|
Node 5 -> (3,4,5) is the maximum value in the path
|
||||||
|
Node 3 -> (3,1,3) is the maximum value in the path.
|
||||||
|
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Input: root = [3,3,null,4,2]
|
||||||
|
Output: 3
|
||||||
|
Explanation: Node 2 -> (3, 3, 2) is not good, because "3" is higher
|
||||||
|
than it.
|
||||||
|
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Input: root = [1]
|
||||||
|
Output: 1
|
||||||
|
Explanation: Root is considered as good.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
• The number of nodes in the binary tree is in the range [1, 10^5].
|
||||||
|
|
||||||
|
• Each node's value is between [-10^4, 10^4].
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
class Solution:
|
||||||
|
def goodNodes(self, root: TreeNode) -> int:
|
||||||
|
def dfs(node, max_val):
|
||||||
|
if not node:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
is_good = 1 if node.val >= max_val else 0
|
||||||
|
new_max = max(max_val, node.val)
|
||||||
|
|
||||||
|
return is_good + dfs(node.left, new_max) + dfs(node.right, new_max)
|
||||||
|
|
||||||
|
return dfs(root, root.val)
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
199. Binary Tree Right Side View
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/binary-tree-right-side-view/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given the root of a binary tree, imagine yourself standing on the
|
||||||
|
right side of it, return the values of the nodes you can see ordered
|
||||||
|
from top to bottom.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: root = [1,2,3,null,5,null,4]
|
||||||
|
|
||||||
|
Output: [1,3,4]
|
||||||
|
|
||||||
|
Explanation:
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: root = [1,2,3,4,null,null,null,5]
|
||||||
|
|
||||||
|
Output: [1,3,4,5]
|
||||||
|
|
||||||
|
Explanation:
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: root = [1,null,3]
|
||||||
|
|
||||||
|
Output: [1,3]
|
||||||
|
|
||||||
|
Example 4:
|
||||||
|
|
||||||
|
Input: root = []
|
||||||
|
|
||||||
|
Output: []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• The number of nodes in the tree is in the range [0, 100].
|
||||||
|
|
||||||
|
• -100 <= Node.val <= 100
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
|
||||||
|
res = []
|
||||||
|
q = deque([root])
|
||||||
|
|
||||||
|
while q:
|
||||||
|
rightSide = None
|
||||||
|
qLen = len(q)
|
||||||
|
|
||||||
|
for i in range(qLen):
|
||||||
|
node = q.popleft()
|
||||||
|
if node:
|
||||||
|
rightSide = node
|
||||||
|
q.append(node.left)
|
||||||
|
q.append(node.right)
|
||||||
|
|
||||||
|
if rightSide:
|
||||||
|
res.append(rightSide.val)
|
||||||
|
|
||||||
|
return res
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"""
|
||||||
|
230. Kth Smallest Element in a BST
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/kth-smallest-element-in-a-bst/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given the root of a binary search tree, and an integer k, return the
|
||||||
|
k^th smallest value (1-indexed) of all the values of the nodes in the
|
||||||
|
tree.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: root = [3,1,4,null,2], k = 1
|
||||||
|
Output: 1
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: root = [5,3,6,2,4,null,null,1], k = 3
|
||||||
|
Output: 3
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• The number of nodes in the tree is n.
|
||||||
|
|
||||||
|
• 1 <= k <= n <= 10^4
|
||||||
|
|
||||||
|
• 0 <= Node.val <= 10^4
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Follow up: If the BST is modified often (i.e., we can do insert and
|
||||||
|
delete operations) and you need to find the kth smallest frequently,
|
||||||
|
how would you optimize?
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
class Solution:
|
||||||
|
def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
|
||||||
|
n = 0
|
||||||
|
stack = []
|
||||||
|
cur = root
|
||||||
|
|
||||||
|
while cur or stack:
|
||||||
|
while cur:
|
||||||
|
stack.append(cur)
|
||||||
|
cur = cur.left
|
||||||
|
|
||||||
|
cur = stack.pop()
|
||||||
|
n += 1
|
||||||
|
if n == k:
|
||||||
|
return cur.val
|
||||||
|
cur = cur.right
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"""
|
||||||
|
235. Lowest Common Ancestor of a Binary Search Tree
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given a binary search tree (BST), find the lowest common ancestor
|
||||||
|
(LCA) node of two given nodes in the BST.
|
||||||
|
|
||||||
|
According to the definition of LCA on Wikipedia: “The lowest
|
||||||
|
common ancestor is defined between two nodes p and q as the lowest
|
||||||
|
node in T that has both p and q as descendants (where we allow a node
|
||||||
|
to be a descendant of itself).”
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
|
||||||
|
Output: 6
|
||||||
|
Explanation: The LCA of nodes 2 and 8 is 6.
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
|
||||||
|
Output: 2
|
||||||
|
Explanation: The LCA of nodes 2 and 4 is 2, since a node can be a
|
||||||
|
descendant of itself according to the LCA definition.
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: root = [2,1], p = 2, q = 1
|
||||||
|
Output: 2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• The number of nodes in the tree is in the range [2, 10^5].
|
||||||
|
|
||||||
|
• -10^9 <= Node.val <= 10^9
|
||||||
|
|
||||||
|
• All Node.val are unique.
|
||||||
|
|
||||||
|
• p != q
|
||||||
|
|
||||||
|
• p and q will exist in the BST.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, x):
|
||||||
|
# self.val = x
|
||||||
|
# self.left = None
|
||||||
|
# self.right = None
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def lowestCommonAncestor(
|
||||||
|
self, root: "TreeNode", p: "TreeNode", q: "TreeNode"
|
||||||
|
) -> "TreeNode":
|
||||||
|
cur = root
|
||||||
|
while cur:
|
||||||
|
if p.val < cur.val and q.val < cur.val:
|
||||||
|
cur = cur.left
|
||||||
|
elif p.val > cur.val and q.val > cur.val:
|
||||||
|
cur = cur.right
|
||||||
|
else:
|
||||||
|
return cur
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
"""
|
||||||
|
236. Lowest Common Ancestor of a Binary Tree
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given a binary tree, find the lowest common ancestor (LCA) of two
|
||||||
|
given nodes in the tree.
|
||||||
|
|
||||||
|
According to the definition of LCA on Wikipedia: “The lowest
|
||||||
|
common ancestor is defined between two nodes p and q as the lowest
|
||||||
|
node in T that has both p and q as descendants (where we allow a node
|
||||||
|
to be a descendant of itself).”
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
|
||||||
|
Output: 3
|
||||||
|
Explanation: The LCA of nodes 5 and 1 is 3.
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
|
||||||
|
Output: 5
|
||||||
|
Explanation: The LCA of nodes 5 and 4 is 5, since a node can be a
|
||||||
|
descendant of itself according to the LCA definition.
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: root = [1,2], p = 1, q = 2
|
||||||
|
Output: 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• The number of nodes in the tree is in the range [2, 10^5].
|
||||||
|
|
||||||
|
• -10^9 <= Node.val <= 10^9
|
||||||
|
|
||||||
|
• All Node.val are unique.
|
||||||
|
|
||||||
|
• p != q
|
||||||
|
|
||||||
|
• p and q will exist in the tree.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, x):
|
||||||
|
# self.val = x
|
||||||
|
# self.left = None
|
||||||
|
# self.right = None
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def lowestCommonAncestor(
|
||||||
|
self, root: "TreeNode", p: "TreeNode", q: "TreeNode"
|
||||||
|
) -> "TreeNode":
|
||||||
|
|
||||||
|
if not root:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if root == p or root == q:
|
||||||
|
return root
|
||||||
|
|
||||||
|
l = self.lowestCommonAncestor(root.left, p, q)
|
||||||
|
r = self.lowestCommonAncestor(root.right, p, q)
|
||||||
|
|
||||||
|
if l and r:
|
||||||
|
return root
|
||||||
|
else:
|
||||||
|
return l or r
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""
|
||||||
|
98. Validate Binary Search Tree
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/validate-binary-search-tree/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given the root of a binary tree, determine if it is a valid binary
|
||||||
|
search tree (BST).
|
||||||
|
|
||||||
|
A valid BST is defined as follows:
|
||||||
|
|
||||||
|
• The left subtree of a node contains only nodes with keys strictly
|
||||||
|
less than the node's key.
|
||||||
|
|
||||||
|
• The right subtree of a node contains only nodes with keys strictly
|
||||||
|
greater than the node's key.
|
||||||
|
|
||||||
|
• Both the left and right subtrees must also be binary search trees.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: root = [2,1,3]
|
||||||
|
Output: true
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: root = [5,1,4,null,null,3,6]
|
||||||
|
Output: false
|
||||||
|
Explanation: The root node's value is 5 but its right child's value
|
||||||
|
is 4.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• The number of nodes in the tree is in the range [1, 10^4].
|
||||||
|
|
||||||
|
• -2^31 <= Node.val <= 2^31 - 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
class Solution:
|
||||||
|
def isValidBST(self, root: Optional[TreeNode]) -> bool:
|
||||||
|
def valid(node, low, high):
|
||||||
|
if not node:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not (low < node.val < high):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return valid(node.left, low, node.val) and valid(node.right, node.val, high)
|
||||||
|
|
||||||
|
return valid(root, float("-inf"), float("inf"))
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""
|
||||||
|
1365. How Many Numbers Are Smaller Than the Current Number
|
||||||
|
Difficulty: Easy
|
||||||
|
https://leetcode.com/problems/how-many-numbers-are-smaller-than-the-current-number/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given the array nums, for each nums[i] find out how many numbers in
|
||||||
|
the array are smaller than it. That is, for each nums[i] you have to
|
||||||
|
count the number of valid j's such that j != i and nums[j] < nums[i].
|
||||||
|
|
||||||
|
Return the answer in an array.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: nums = [8,1,2,2,3]
|
||||||
|
Output: [4,0,1,1,3]
|
||||||
|
Explanation:
|
||||||
|
For nums[0]=8 there exist four smaller numbers than it (1, 2, 2 and
|
||||||
|
3).
|
||||||
|
For nums[1]=1 does not exist any smaller number than it.
|
||||||
|
For nums[2]=2 there exist one smaller number than it (1).
|
||||||
|
For nums[3]=2 there exist one smaller number than it (1).
|
||||||
|
For nums[4]=3 there exist three smaller numbers than it (1, 2 and 2).
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: nums = [6,5,4,8]
|
||||||
|
Output: [2,1,0,3]
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: nums = [7,7,7,7]
|
||||||
|
Output: [0,0,0,0]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• 2 <= nums.length <= 500
|
||||||
|
|
||||||
|
• 0 <= nums[i] <= 100
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def smallerNumbersThanCurrent(self, nums: List[int]) -> List[int]:
|
||||||
|
freq = Counter(nums)
|
||||||
|
sorted_nums = sorted(freq.keys())
|
||||||
|
smaller = {}
|
||||||
|
count = 0
|
||||||
|
for num in sorted_nums:
|
||||||
|
smaller[num] = count
|
||||||
|
count += freq[num]
|
||||||
|
|
||||||
|
return [smaller[num] for num in nums]
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
1426. Counting Elements
|
||||||
|
Difficulty: Easy
|
||||||
|
https://leetcode.com/problems/counting-elements/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given an integer array arr, count how many elements x there are, such
|
||||||
|
that x + 1 is also in arr. If there are duplicates in arr, count them
|
||||||
|
separately.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: arr = [1,2,3]
|
||||||
|
Output: 2
|
||||||
|
Explanation: 1 and 2 are counted cause 2 and 3 are in arr.
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: arr = [1,1,3,3,5,5,7,7]
|
||||||
|
Output: 0
|
||||||
|
Explanation: No numbers are counted, cause there is no 2, 4, 6, or 8
|
||||||
|
in arr.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• 1 <= arr.length <= 1000
|
||||||
|
|
||||||
|
• 0 <= arr[i] <= 1000
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def countElements(self, arr: List[int]) -> int:
|
||||||
|
arr_set = set(arr)
|
||||||
|
count = 0
|
||||||
|
for n in arr:
|
||||||
|
sum = n + 1
|
||||||
|
if sum in arr_set:
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""
|
||||||
|
268. Missing Number
|
||||||
|
Difficulty: Easy
|
||||||
|
https://leetcode.com/problems/missing-number/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given an array nums containing n distinct numbers in the range [0,
|
||||||
|
n], return the only number in the range that is missing from the
|
||||||
|
array.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: nums = [3,0,1]
|
||||||
|
|
||||||
|
Output: 2
|
||||||
|
|
||||||
|
Explanation:
|
||||||
|
|
||||||
|
n = 3 since there are 3 numbers, so all numbers are in the range
|
||||||
|
[0,3]. 2 is the missing number in the range since it does not appear
|
||||||
|
in nums.
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: nums = [0,1]
|
||||||
|
|
||||||
|
Output: 2
|
||||||
|
|
||||||
|
Explanation:
|
||||||
|
|
||||||
|
n = 2 since there are 2 numbers, so all numbers are in the range
|
||||||
|
[0,2]. 2 is the missing number in the range since it does not appear
|
||||||
|
in nums.
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: nums = [9,6,4,2,3,5,7,0,1]
|
||||||
|
|
||||||
|
Output: 8
|
||||||
|
|
||||||
|
Explanation:
|
||||||
|
|
||||||
|
n = 9 since there are 9 numbers, so all numbers are in the range
|
||||||
|
[0,9]. 8 is the missing number in the range since it does not appear
|
||||||
|
in nums.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• n == nums.length
|
||||||
|
|
||||||
|
• 1 <= n <= 10^4
|
||||||
|
|
||||||
|
• 0 <= nums[i] <= n
|
||||||
|
|
||||||
|
• All the numbers of nums are unique.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Follow up: Could you implement a solution using only O(1) extra space
|
||||||
|
complexity and O(n) runtime complexity?
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def missingNumber(self, nums: List[int]) -> int:
|
||||||
|
n = len(nums)
|
||||||
|
expected_sum = (n * (n + 1)) // 2
|
||||||
|
actual_sum = sum(nums)
|
||||||
|
return expected_sum - actual_sum
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""
|
||||||
|
1832. Check if the Sentence Is Pangram
|
||||||
|
Difficulty: Easy
|
||||||
|
https://leetcode.com/problems/check-if-the-sentence-is-pangram/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
A pangram is a sentence where every letter of the English alphabet
|
||||||
|
appears at least once.
|
||||||
|
|
||||||
|
Given a string sentence containing only lowercase English letters,
|
||||||
|
return true if sentence is a pangram, or false otherwise.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: sentence = "thequickbrownfoxjumpsoverthelazydog"
|
||||||
|
Output: true
|
||||||
|
Explanation: sentence contains at least one of every letter of the
|
||||||
|
English alphabet.
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: sentence = "leetcode"
|
||||||
|
Output: false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• 1 <= sentence.length <= 1000
|
||||||
|
|
||||||
|
• sentence consists of lowercase English letters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def checkIfPangram(self, sentence: str) -> bool:
|
||||||
|
return len(set(sentence.lower())) == 26
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
387. First Unique Character in a String
|
||||||
|
Difficulty: Easy
|
||||||
|
https://leetcode.com/problems/first-unique-character-in-a-string/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given a string s, find the first non-repeating character in it and
|
||||||
|
return its index. If it does not exist, return -1.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: s = "leetcode"
|
||||||
|
|
||||||
|
Output: 0
|
||||||
|
|
||||||
|
Explanation:
|
||||||
|
|
||||||
|
The character 'l' at index 0 is the first character that does not
|
||||||
|
occur at any other index.
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: s = "loveleetcode"
|
||||||
|
|
||||||
|
Output: 2
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: s = "aabb"
|
||||||
|
|
||||||
|
Output: -1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• 1 <= s.length <= 10^5
|
||||||
|
|
||||||
|
• s consists of only lowercase English letters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def firstUniqChar(self, s: str) -> int:
|
||||||
|
map = Counter(s)
|
||||||
|
for i, c in enumerate(s):
|
||||||
|
if map[c] == 1:
|
||||||
|
return i
|
||||||
|
return -1
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"""
|
||||||
|
344. Reverse String
|
||||||
|
Difficulty: Easy
|
||||||
|
https://leetcode.com/problems/reverse-string/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Write a function that reverses a string. The input string is given as
|
||||||
|
an array of characters s.
|
||||||
|
|
||||||
|
You must do this by modifying the input array in-place with O(1)
|
||||||
|
extra memory.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: s = ["h","e","l","l","o"]
|
||||||
|
Output: ["o","l","l","e","h"]
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: s = ["H","a","n","n","a","h"]
|
||||||
|
Output: ["h","a","n","n","a","H"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• 1 <= s.length <= 10^5
|
||||||
|
|
||||||
|
• s[i] is a printable ascii character.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def reverseString(self, s: List[str]) -> None:
|
||||||
|
l = 0
|
||||||
|
r = len(s) - 1
|
||||||
|
while l < r:
|
||||||
|
s[l], s[r] = s[r], s[l]
|
||||||
|
l += 1
|
||||||
|
r -= 1
|
||||||
|
|
||||||
|
"""
|
||||||
|
Do not return anything, modify s in-place instead.
|
||||||
|
"""
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"""
|
||||||
|
15. 3Sum
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/3sum/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given an integer array nums, return all the triplets [nums[i],
|
||||||
|
nums[j], nums[k]] such that i != j, i != k, and j != k, and nums[i] +
|
||||||
|
nums[j] + nums[k] == 0.
|
||||||
|
|
||||||
|
Notice that the solution set must not contain duplicate triplets.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: nums = [-1,0,1,2,-1,-4]
|
||||||
|
Output: [[-1,-1,2],[-1,0,1]]
|
||||||
|
Explanation:
|
||||||
|
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0.
|
||||||
|
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0.
|
||||||
|
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0.
|
||||||
|
The distinct triplets are [-1,0,1] and [-1,-1,2].
|
||||||
|
Notice that the order of the output and the order of the triplets
|
||||||
|
does not matter.
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: nums = [0,1,1]
|
||||||
|
Output: []
|
||||||
|
Explanation: The only possible triplet does not sum up to 0.
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: nums = [0,0,0]
|
||||||
|
Output: [[0,0,0]]
|
||||||
|
Explanation: The only possible triplet sums up to 0.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• 3 <= nums.length <= 3000
|
||||||
|
|
||||||
|
• -10^5 <= nums[i] <= 10^5
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def threeSum(self, nums: list[int]) -> list[list[int]]:
|
||||||
|
nums.sort()
|
||||||
|
result = []
|
||||||
|
n = len(nums)
|
||||||
|
|
||||||
|
for i in range(n):
|
||||||
|
# skip all zero
|
||||||
|
if i > 0 and nums[i] == nums[i - 1]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# two pointers
|
||||||
|
left = i + 1
|
||||||
|
right = n - 1
|
||||||
|
target = -nums[i]
|
||||||
|
|
||||||
|
while left < right:
|
||||||
|
current = nums[left] + nums[right]
|
||||||
|
|
||||||
|
if current == target:
|
||||||
|
result.append([nums[i], nums[left], nums[right]])
|
||||||
|
|
||||||
|
# skip duplicates
|
||||||
|
while left < right and nums[left] == nums[left + 1]:
|
||||||
|
left += 1
|
||||||
|
while left < right and nums[right] == nums[right - 1]:
|
||||||
|
right -= 1
|
||||||
|
|
||||||
|
# shift pointers
|
||||||
|
left += 1
|
||||||
|
right -= 1
|
||||||
|
|
||||||
|
# since sorted, if current < target, then move left
|
||||||
|
elif current < target:
|
||||||
|
left += 1
|
||||||
|
# since sorted, if current > target, then move right
|
||||||
|
else:
|
||||||
|
right -= 1
|
||||||
|
|
||||||
|
return result
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
"""
|
||||||
|
49. Group Anagrams
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/group-anagrams/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given an array of strings strs, group the anagrams together. You can
|
||||||
|
return the answer in any order.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: strs = ["eat","tea","tan","ate","nat","bat"]
|
||||||
|
|
||||||
|
Output: [["bat"],["nat","tan"],["ate","eat","tea"]]
|
||||||
|
|
||||||
|
Explanation:
|
||||||
|
|
||||||
|
• There is no string in strs that can be rearranged to form "bat".
|
||||||
|
|
||||||
|
• The strings "nat" and "tan" are anagrams as they can be rearranged
|
||||||
|
to form each other.
|
||||||
|
|
||||||
|
• The strings "ate", "eat", and "tea" are anagrams as they can be
|
||||||
|
rearranged to form each other.
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: strs = [""]
|
||||||
|
|
||||||
|
Output: [[""]]
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: strs = ["a"]
|
||||||
|
|
||||||
|
Output: [["a"]]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• 1 <= strs.length <= 10^4
|
||||||
|
|
||||||
|
• 0 <= strs[i].length <= 100
|
||||||
|
|
||||||
|
• strs[i] consists of lowercase English letters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
|
||||||
|
if not strs:
|
||||||
|
return strs
|
||||||
|
res = defaultdict(list)
|
||||||
|
for s in strs:
|
||||||
|
count = [0] * 26
|
||||||
|
for c in s:
|
||||||
|
count[ord(c) - ord("a")] += 1
|
||||||
|
res[tuple(count)].append(s)
|
||||||
|
|
||||||
|
return res.values()
|
||||||
Reference in new issue
Block a user