diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f983521..945d0832 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,7 +79,8 @@ jobs: strategy: fail-fast: false matrix: - shard: [1/12, 2/12, 3/12, 4/12, 5/12, 6/12, 7/12, 8/12, 9/12, 10/12, 11/12, 12/12] + shard: + [1/14, 2/14, 3/14, 4/14, 5/14, 6/14, 7/14, 8/14, 9/14, 10/14, 11/14, 12/14, 13/14, 14/14] steps: - uses: actions/checkout@v6 diff --git a/docs/superpowers/specs/2026-04-05-stacks-queues-expansion-design.md b/docs/superpowers/specs/2026-04-05-stacks-queues-expansion-design.md new file mode 100644 index 00000000..cfe40ba9 --- /dev/null +++ b/docs/superpowers/specs/2026-04-05-stacks-queues-expansion-design.md @@ -0,0 +1,325 @@ +# Stacks & Queues Expansion Design + +**Date:** 2026-04-05 +**Status:** Draft +**Scope:** Expand stacks-queues category from 1 to 28 algorithms with 3 new tracker variants + +## Context + +The stacks-queues category currently has only `valid-parentheses` under `validation/`. This is the thinnest category in AlgoFlow despite stacks and queues being fundamental data structures taught in every CS course. The goal is comprehensive coverage of classic stack and queue algorithms, balanced across both data structures (16 stack, 12 queue), organized by technique. + +4 monotonic stack algorithms already exist in `arrays/stack-based/` (next-greater-element, daily-temperatures, largest-rectangle-histogram, trapping-rain-water) — these will NOT be duplicated. + +## Algorithm Inventory (28 total, 27 new) + +### validation (3) + +| Algorithm | Default Input | Tracker | Difficulty | +|-----------|--------------|---------|------------| +| valid-parentheses *(existing)* | `"({[]})"` | StackQueueTracker | Easy | +| min-remove-to-make-valid | `"a(b(c)d"` | StackQueueTracker | Medium | +| longest-valid-parentheses | `"(()())"` | StackQueueTracker | Hard | + +### monotonic-stack (3) + +| Algorithm | Default Input | Tracker | Difficulty | +|-----------|--------------|---------|------------| +| online-stock-span | `[100, 80, 60, 70, 60, 75, 85]` | NumericStackTracker | Medium | +| remove-k-digits | `{ num: "1432219", k: 3 }` | StackQueueTracker | Medium | +| sum-of-subarray-minimums | `[3, 1, 2, 4]` | NumericStackTracker | Medium | + +### expression-evaluation (3) + +| Algorithm | Default Input | Tracker | Difficulty | +|-----------|--------------|---------|------------| +| evaluate-reverse-polish | `["2","1","+","3","*"]` | ExpressionTracker | Medium | +| basic-calculator | `"1 + (2 - 3)"` | ExpressionTracker | Hard | +| infix-to-postfix | `"a+b*(c-d)"` | ExpressionTracker | Medium | + +### stack-design (4) + +| Algorithm | Default Input | Tracker | Difficulty | +|-----------|--------------|---------|------------| +| min-stack | `[5, 3, 7, 1, 8]` | NumericStackTracker | Medium | +| asteroid-collision | `[5, 10, -5]` | NumericStackTracker | Medium | +| decode-string | `"3[a2[c]]"` | StackQueueTracker | Medium | +| max-frequency-stack | `[5, 7, 5, 7, 4, 5]` | NumericStackTracker | Hard | + +### stack-applications (3) + +| Algorithm | Default Input | Tracker | Difficulty | +|-----------|--------------|---------|------------| +| simplify-path | `"/a/./b/../../c/"` | StackQueueTracker | Medium | +| backspace-string-compare | `{ s: "ab#c", t: "ad#c" }` | StackQueueTracker | Easy | +| remove-all-adjacent-duplicates | `"abbaca"` | StackQueueTracker | Easy | + +### queue-operations (4) + +| Algorithm | Default Input | Tracker | Difficulty | +|-----------|--------------|---------|------------| +| sliding-window-maximum | `{ nums: [1,3,-1,-3,5,3,6,7], k: 3 }` | QueueTracker | Hard | +| implement-queue-using-stacks | `[1, 2, 3, 4, 5]` | QueueTracker | Easy | +| implement-stack-using-queues | `[1, 2, 3, 4, 5]` | QueueTracker | Easy | +| number-of-recent-calls | `[1, 100, 3001, 3002]` | QueueTracker | Easy | + +### queue-design (4) + +| Algorithm | Default Input | Tracker | Difficulty | +|-----------|--------------|---------|------------| +| design-circular-queue | `{ operations: ["enqueue 1","enqueue 2","dequeue","enqueue 3"], capacity: 3 }` | QueueTracker | Medium | +| design-circular-deque | `{ operations: ["pushBack 1","pushFront 2","popBack","pushBack 3"], capacity: 3 }` | QueueTracker | Medium | +| task-scheduler | `{ tasks: ["A","A","A","B","B","B"], cooldown: 2 }` | QueueTracker | Medium | +| moving-average-from-stream | `{ values: [1, 10, 3, 5], windowSize: 3 }` | QueueTracker | Easy | + +### queue-applications (4) + +| Algorithm | Default Input | Tracker | Difficulty | +|-----------|--------------|---------|------------| +| generate-binary-numbers | `{ count: 10 }` | QueueTracker | Easy | +| interleave-first-half-queue | `[1, 2, 3, 4, 5, 6]` | QueueTracker | Medium | +| first-non-repeating-char-stream | `"aabcbcd"` | QueueTracker | Medium | +| flatten-nested-list-iterator | `[[1,[2]],3,[4,[5,6]]]` | QueueTracker | Medium | + +**Difficulty spread:** 7 Easy, 15 Medium, 4 Hard (+ 1 existing Easy) + +## Tracker Variants (4 total) + +### StackQueueTracker (existing, unchanged) +- **For:** String/character-based stack problems +- **Algorithms:** valid-parentheses, min-remove-to-make-valid, longest-valid-parentheses, decode-string, simplify-path, backspace-string-compare, remove-all-adjacent-duplicates, remove-k-digits +- **Constructor:** `(inputString: string, lineMap: LineMap)` + +### NumericStackTracker (new) +- **File:** `src/trackers/numeric-stack-tracker.ts` +- **For:** Number array + stack problems +- **Algorithms:** online-stock-span, sum-of-subarray-minimums, min-stack, asteroid-collision, max-frequency-stack +- **Constructor:** `(inputArray: number[], lineMap: LineMap)` +- **Key methods:** `initialize()`, `processElement(idx)`, `pushIndex(idx)`, `popAndResolve(poppedIdx, resolvedValue)`, `maintainMonotonic(poppedIdx)`, `pushAuxiliary(value)`, `popAuxiliary()`, `complete(resultArray)` +- **Visual state fields used:** `inputArray`, `stackElements`, `auxiliaryStack`, `resultArray`, `monotonicOrder` + +### ExpressionTracker (new) +- **File:** `src/trackers/expression-tracker.ts` +- **For:** Token-based expression parsing and evaluation +- **Algorithms:** evaluate-reverse-polish, basic-calculator, infix-to-postfix +- **Constructor:** `(tokens: string[], lineMap: LineMap)` +- **Key methods:** `initialize()`, `processToken(idx)`, `pushOperand(value)`, `pushOperator(op)`, `popAndEvaluate(op, operandA, operandB, result)`, `outputToken(token)`, `complete(result)` +- **Visual state fields used:** `inputChars` (tokens), `stackElements`, `outputElements` + +### QueueTracker (new) +- **File:** `src/trackers/queue-tracker.ts` +- **For:** Queue, deque, and circular buffer problems +- **Algorithms:** sliding-window-maximum, implement-queue-using-stacks, implement-stack-using-queues, number-of-recent-calls, design-circular-queue, design-circular-deque, task-scheduler, moving-average-from-stream, generate-binary-numbers, interleave-first-half-queue, first-non-repeating-char-stream, flatten-nested-list-iterator +- **Constructor:** `(inputArray: (string | number)[], lineMap: LineMap)` +- **Key methods:** `initialize()`, `enqueue(value)`, `dequeue()`, `enqueueFront(value)`, `dequeueRear()`, `peekFront()`, `processElement(idx)`, `transferToAuxiliary()`, `initCircularBuffer(capacity)`, `circularEnqueue(value)`, `circularDequeue()`, `complete()` +- **Visual state fields used:** `queueElements`, `stackElements` (for two-stack queue), `circularBuffer`, `inputArray` + +## Visual State Extensions + +### New optional fields on `StackQueueVisualState` + +```typescript +export interface StackQueueVisualState { + kind: "stack-queue"; + stackElements: StackElement[]; + inputChars: InputChar[]; + inputIndex: number; + statusMessage: string | null; + + // New optional fields + inputArray?: NumericInputElement[]; + queueElements?: StackElement[]; + auxiliaryStack?: StackElement[]; + outputElements?: OutputElement[]; + resultArray?: ResultElement[]; + monotonicOrder?: "increasing" | "decreasing" | null; + circularBuffer?: CircularBufferState; + phase?: string; +} +``` + +### New supporting types + +```typescript +export type NumericInputState = "default" | "current" | "processed" | "result-pending" | "resolved"; +export interface NumericInputElement { + value: number; + index: number; + state: NumericInputState; +} + +export type OutputElementState = "default" | "new" | "computed"; +export interface OutputElement { + value: string; + state: OutputElementState; +} + +export type ResultElementState = "default" | "pending" | "resolved"; +export interface ResultElement { + value: number | null; + index: number; + state: ResultElementState; +} + +export interface CircularBufferState { + elements: (string | number | null)[]; + frontIndex: number; + rearIndex: number; + capacity: number; +} +``` + +### New StepTypes + +Add to the `StepType` union: `"peek"`, `"evaluate"`, `"output"`, `"maintain-monotonic"`, `"enqueue-front"`, `"dequeue-rear"`, `"transfer"`, `"resolve"`. + +## Visualizer Extensions + +The `StackQueueVisualizer` at `src/components/visualization/StackQueueVisualizer.tsx` conditionally renders new sections: + +``` ++---------------------------------------------+ +| Input Row (inputChars OR inputArray) | ++---------------------------------------------+ +| | +| Stack Queue Auxiliary | +| [col] [horizontal] [col] | +| | ++---------------------------------------------+ +| Output Row (outputElements / resultArray) | ++---------------------------------------------+ +| Circular Buffer (ring visualization) | ++---------------------------------------------+ +| Status + Phase + Monotonic badge | ++---------------------------------------------+ +``` + +**Rendering rules:** +- `inputArray` present -> numbered indexed cells instead of character boxes +- `queueElements` present -> horizontal row, front/rear labels, left-to-right +- `auxiliaryStack` present -> second stack column beside main stack with label +- `outputElements` present -> horizontal token row below main area +- `resultArray` present -> indexed cells with pending/resolved coloring +- `circularBuffer` present -> ring/circle with front/rear pointer arrows and capacity slots +- `monotonicOrder` set -> small badge near stack ("Monotonic Decreasing") +- `phase` set -> label near status message +- All sections use existing CSS variables and Framer Motion with `useReducedMotion` + +**New color mappings:** +- `NumericInputState`: reuse InputCharState palette + `result-pending: "--color-viz-comparing"`, `resolved: "--color-viz-sorted"` +- `OutputElementState`: `default: "--color-viz-default"`, `new: "--color-viz-current"`, `computed: "--color-viz-sorted"` +- `ResultElementState`: `default: "--color-viz-default"`, `pending: "--color-viz-comparing"`, `resolved: "--color-viz-sorted"` + +## InputEditor Updates + +Current: `InputEditor.tsx` hardcodes `StringInputEditor` for `STACKS_QUEUES`. +Updated: Switch to generic introspection pattern (same as sets, matrices, linked-lists). + +The InputEditor inspects `defaultInput` shape: +- `inputString: string` -> `StringInputEditor` +- `number[]` or `string[]` -> `ArrayInputEditor` +- Complex objects -> `GenericIntrospectEditor` + +## fn-import.d.ts Naming + +**Collision avoidance** — existing exports that conflict: +- `taskSchedulerHeap` (line 257) -> our export: `taskSchedulerQueue` +- `slidingWindowMaxDeque` (line 158) -> our export: `slidingWindowMaxMonotonic` +- `dailyTemperatures` (line 162) -> NOT adding (exists in arrays/stack-based) +- `trappingRainWater` (line 156) -> NOT adding (exists in arrays/stack-based) +- `largestRectangleHistogram` (line 157) -> NOT adding (exists in arrays/stack-based) + +**27 new exports to add under `// Stacks & Queues` section:** +- `minRemoveToMakeValid` +- `longestValidParentheses` +- `onlineStockSpan` +- `removeKDigits` +- `sumOfSubarrayMinimums` +- `evaluateReversePolish` +- `basicCalculator` +- `infixToPostfix` +- `minStack` (the operation runner, not the data structure class) +- `asteroidCollision` +- `decodeString` +- `maxFrequencyStack` +- `simplifyPath` +- `backspaceStringCompare` +- `removeAllAdjacentDuplicates` +- `slidingWindowMaxMonotonic` +- `implementQueueUsingStacks` +- `implementStackUsingQueues` +- `numberOfRecentCalls` +- `designCircularQueue` +- `designCircularDeque` +- `taskSchedulerQueue` +- `movingAverageFromStream` +- `generateBinaryNumbers` +- `interleaveFirstHalfQueue` +- `firstNonRepeatingCharStream` +- `flattenNestedListIterator` + +Each must be verified against the full 320-entry file before writing. + +## E2E Considerations + +- E2E auto-discovers all algorithms from filesystem — no manual test registration needed +- `e2e/helpers/inputs.ts` needs entries for algorithms with non-standard input shapes: + - `sliding-window-maximum` (`{ nums, k }`) + - `backspace-string-compare` (`{ s, t }`) + - `design-circular-queue` (`{ operations, capacity }`) + - `design-circular-deque` (`{ operations, capacity }`) + - `task-scheduler` (`{ tasks, cooldown }`) + - `moving-average-from-stream` (`{ values, windowSize }`) + - `generate-binary-numbers` (`{ count }`) + - `remove-k-digits` (`{ num, k }`) + +## Implementation Phases + +| Phase | What | Model | Rationale | +|-------|------|-------|-----------| +| 1. Foundation | Types, 3 trackers, tracker barrel exports | Sonnet | Mechanical pattern-following | +| 2. Visualizer | Extend StackQueueVisualizer with conditional sections | Sonnet | UI component, pattern-based | +| 3. Batch 1 | validation (2) + stack-applications (3) — StackQueueTracker | Sonnet | Closest to existing pattern | +| 4. Batch 2 | monotonic-stack (3) + stack-design (4) — NumericStackTracker | Sonnet | New tracker usage | +| 5. Batch 3 | expression-evaluation (3) — ExpressionTracker | Sonnet | Unique token logic | +| 6. Batch 4 | queue-operations (4) + queue-design (4) + queue-applications (4) — QueueTracker | Sonnet | Largest batch, all queue-based | +| 7. InputEditor | Update STACKS_QUEUES case for diverse inputs | Sonnet | Small targeted change | +| 8. Code Review | Review all implementations against plan and coding standards | Opus | Deep analysis needed | +| 9. QA Testing | lint, format, typecheck, unit tests, storybook build | Opus | Judgment on failures | +| 10. Browser Preview | MCP Claude preview — navigate all new algorithms | Opus | Visual judgment | +| 11. E2E Tests | Run E2E suite, fix breakage | Opus | Diagnostic reasoning | + +## Per-Algorithm File Checklist + +Each new algorithm produces 9 files: +1. `index.ts` — AlgorithmDefinition + registry.register() +2. `step-generator.ts` — step generation with tracker +3. `step-generator.test.ts` — step generation tests +4. `educational.ts` — all 7 educational sections +5. `.test.ts` — correctness tests +6. `Pipeline.stories.tsx` — Storybook pipeline story +7. `sources/.ts` — TypeScript source with `@step:` markers +8. `sources/.py` — Python source with `@step:` markers +9. `sources/.java` — Java source with `@step:` markers + +Plus: 1 entry in `src/types/fn-import.d.ts` + +**Total new files:** 27 algorithms x 9 files + 3 tracker files + tracker barrel update + type extensions + visualizer extension + InputEditor update = **~250 files** + +## Verification Plan + +1. `npm run lint` -> `npm run format` -> `npm run typecheck` -> `npm test` (all pass) +2. `npm run storybook:build` (27 new pipeline stories compile) +3. Browser preview: navigate to each new algorithm, verify step generation, playback, visualization +4. E2E tests auto-discover and test all 28 stacks-queues algorithms +5. No duplicate entries in fn-import.d.ts +6. No name collisions with existing algorithms in other categories + +## Critical Files + +- `src/types/execution.ts` — extend StackQueueVisualState, add StepTypes, add supporting types +- `src/trackers/stack-queue-tracker.ts` — reference for new tracker variants +- `src/trackers/index.ts` — export new trackers +- `src/components/visualization/StackQueueVisualizer.tsx` — extend rendering +- `src/components/input-editor/InputEditor.tsx` — update STACKS_QUEUES case +- `src/types/fn-import.d.ts` — 27 new entries, no duplicates +- `e2e/helpers/inputs.ts` — entries for non-standard input shapes diff --git a/e2e/helpers/inputs.ts b/e2e/helpers/inputs.ts index d7435e45..d6c4328f 100644 --- a/e2e/helpers/inputs.ts +++ b/e2e/helpers/inputs.ts @@ -363,6 +363,47 @@ export const inputTests: InputTest[] = [ algo: "Valid Parentheses", test: (page) => fillTextInput(page, "({[]})"), }, + { + algo: "Min Remove to Make Valid", + test: (page) => fillTextInput(page, "a(b(c)d"), + }, + { + algo: "Longest Valid Parentheses", + test: (page) => fillTextInput(page, "(()())"), + }, + { + algo: "Simplify Path", + test: (page) => fillTextInput(page, "/a/./b/../../c/"), + }, + { + algo: "Remove All Adjacent Duplicates", + test: (page) => fillTextInput(page, "abbaca"), + }, + { + algo: "Decode String", + test: (page) => fillTextInput(page, "3[a2[c]]"), + }, + { + algo: "Remove K Digits", + test: async (page) => { + await fillTextInput(page, "1432219"); + await fillNumberInput(page, 3); + }, + }, + { + algo: "Sliding Window Maximum", + test: async (page) => { + await fillTextInput(page, "1, 3, -1, -3, 5, 3, 6, 7"); + await fillNumberInput(page, 3); + }, + }, + { + algo: "Moving Average from Stream", + test: async (page) => { + await fillTextInput(page, "1, 10, 3, 5"); + await fillNumberInput(page, 3); + }, + }, { algo: "Build Min Heap", test: (page) => fillTextInput(page, "8, 3, 7, 1, 5"), diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 46d2cfc5..7b0b8ef0 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -12,7 +12,7 @@ export default defineConfig({ fullyParallel: true, forbidOnly: isCI, retries: isCI ? 1 : 0, - workers: isCI ? 1 : undefined, + workers: isCI ? 1 : "75%", globalTimeout: isCI ? 600_000 : undefined, reporter: isCI ? [["github"], ["list"], ["html", { open: "never" }]] : [["list"]], use: { diff --git a/e2e/specs/algorithms/arrays.spec.ts b/e2e/specs/algorithms/arrays.spec.ts index 86309883..00221bf1 100644 --- a/e2e/specs/algorithms/arrays.spec.ts +++ b/e2e/specs/algorithms/arrays.spec.ts @@ -6,23 +6,17 @@ const { allAlgorithms, algorithmCategories } = discoverAlgorithms(); const categoryAlgos = allAlgorithms.filter((_, idx) => algorithmCategories[idx] === "arrays"); for (const algo of categoryAlgos) { - test.describe(`Arrays: ${algo}`, () => { - test(`selects "${algo}" and generates steps`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepInfo = await getStepIndex(page); - expect(stepInfo).not.toBeNull(); - expect(stepInfo!.total).toBeGreaterThan(0); - }); + test(`Arrays: ${algo} — selects, generates steps, renders`, async ({ page }) => { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.waitForSelector("button[aria-label='Search algorithms']"); + await selectAlgorithm(page, algo); - test(`${algo}: visualization renders`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const values = await getDisplayedArrayValues(page); - expect(values).not.toBeNull(); - expect(values!.length).toBeGreaterThan(0); - }); + const stepInfo = await getStepIndex(page); + expect(stepInfo).not.toBeNull(); + expect(stepInfo!.total).toBeGreaterThan(0); + + const values = await getDisplayedArrayValues(page); + expect(values).not.toBeNull(); + expect(values!.length).toBeGreaterThan(0); }); } diff --git a/e2e/specs/algorithms/dynamic-programming.spec.ts b/e2e/specs/algorithms/dynamic-programming.spec.ts index 9938bd80..71d08b06 100644 --- a/e2e/specs/algorithms/dynamic-programming.spec.ts +++ b/e2e/specs/algorithms/dynamic-programming.spec.ts @@ -8,28 +8,20 @@ const categoryAlgos = allAlgorithms.filter( ); for (const algo of categoryAlgos) { - test.describe(`Dynamic Programming: ${algo}`, () => { - test(`selects "${algo}" and generates steps`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepInfo = await getStepIndex(page); - expect(stepInfo).not.toBeNull(); - expect(stepInfo!.total).toBeGreaterThan(0); - }); + test(`Dynamic Programming: ${algo} — selects, generates steps, renders`, async ({ page }) => { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.waitForSelector("button[aria-label='Search algorithms']"); + await selectAlgorithm(page, algo); - test(`${algo}: visualization renders`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - // DP table or array cells visible - // DP table cells use h-12 w-16 class or font-mono text-lg inside rounded border divs - const dpCell = page.locator(".h-12.w-16").first(); - const arrayViz = page.locator(".flex.flex-1.justify-center.gap-1").first(); - const hasViz = - (await dpCell.isVisible().catch(() => false)) || - (await arrayViz.isVisible().catch(() => false)); - expect(hasViz).toBe(true); - }); + const stepInfo = await getStepIndex(page); + expect(stepInfo).not.toBeNull(); + expect(stepInfo!.total).toBeGreaterThan(0); + + const dpCell = page.locator(".h-12.w-16").first(); + const arrayViz = page.locator(".flex.flex-1.justify-center.gap-1").first(); + const hasViz = + (await dpCell.isVisible().catch(() => false)) || + (await arrayViz.isVisible().catch(() => false)); + expect(hasViz).toBe(true); }); } diff --git a/e2e/specs/algorithms/graph.spec.ts b/e2e/specs/algorithms/graph.spec.ts index 0d448907..0423a591 100644 --- a/e2e/specs/algorithms/graph.spec.ts +++ b/e2e/specs/algorithms/graph.spec.ts @@ -6,22 +6,16 @@ const { allAlgorithms, algorithmCategories } = discoverAlgorithms(); const categoryAlgos = allAlgorithms.filter((_, idx) => algorithmCategories[idx] === "graph"); for (const algo of categoryAlgos) { - test.describe(`Graph: ${algo}`, () => { - test(`selects "${algo}" and generates steps`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepInfo = await getStepIndex(page); - expect(stepInfo).not.toBeNull(); - expect(stepInfo!.total).toBeGreaterThan(0); - }); + test(`Graph: ${algo} — selects, generates steps, renders`, async ({ page }) => { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.waitForSelector("button[aria-label='Search algorithms']"); + await selectAlgorithm(page, algo); - test(`${algo}: SVG visualization renders`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const svgElement = page.locator("svg").first(); - await expect(svgElement).toBeVisible(); - }); + const stepInfo = await getStepIndex(page); + expect(stepInfo).not.toBeNull(); + expect(stepInfo!.total).toBeGreaterThan(0); + + const svgElement = page.locator("svg").first(); + await expect(svgElement).toBeVisible(); }); } diff --git a/e2e/specs/algorithms/hash-maps.spec.ts b/e2e/specs/algorithms/hash-maps.spec.ts index c4ebc902..accbbee1 100644 --- a/e2e/specs/algorithms/hash-maps.spec.ts +++ b/e2e/specs/algorithms/hash-maps.spec.ts @@ -6,25 +6,19 @@ const { allAlgorithms, algorithmCategories } = discoverAlgorithms(); const categoryAlgos = allAlgorithms.filter((_, idx) => algorithmCategories[idx] === "hash-maps"); for (const algo of categoryAlgos) { - test.describe(`Hash Maps: ${algo}`, () => { - test(`selects "${algo}" and generates steps`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepInfo = await getStepIndex(page); - expect(stepInfo).not.toBeNull(); - expect(stepInfo!.total).toBeGreaterThan(0); - }); + test(`Hash Maps: ${algo} — selects, generates steps, renders`, async ({ page }) => { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.waitForSelector("button[aria-label='Search algorithms']"); + await selectAlgorithm(page, algo); - test(`${algo}: visualization renders`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepCounter = page - .locator("span") - .filter({ hasText: /^\d+ \/ \d+$/ }) - .first(); - await expect(stepCounter).toBeVisible(); - }); + const stepInfo = await getStepIndex(page); + expect(stepInfo).not.toBeNull(); + expect(stepInfo!.total).toBeGreaterThan(0); + + const stepCounter = page + .locator("span") + .filter({ hasText: /^\d+ \/ \d+$/ }) + .first(); + await expect(stepCounter).toBeVisible(); }); } diff --git a/e2e/specs/algorithms/heaps.spec.ts b/e2e/specs/algorithms/heaps.spec.ts index 8bf56737..d3ca3070 100644 --- a/e2e/specs/algorithms/heaps.spec.ts +++ b/e2e/specs/algorithms/heaps.spec.ts @@ -6,23 +6,16 @@ const { allAlgorithms, algorithmCategories } = discoverAlgorithms(); const categoryAlgos = allAlgorithms.filter((_, idx) => algorithmCategories[idx] === "heaps"); for (const algo of categoryAlgos) { - test.describe(`Heaps: ${algo}`, () => { - test(`selects "${algo}" and generates steps`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepInfo = await getStepIndex(page); - expect(stepInfo).not.toBeNull(); - expect(stepInfo!.total).toBeGreaterThan(0); - }); + test(`Heaps: ${algo} — selects, generates steps, renders`, async ({ page }) => { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.waitForSelector("button[aria-label='Search algorithms']"); + await selectAlgorithm(page, algo); - test(`${algo}: heap visualization renders`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - // Heap uses SVG tree visualization - const svgElement = page.locator("svg").first(); - await expect(svgElement).toBeVisible(); - }); + const stepInfo = await getStepIndex(page); + expect(stepInfo).not.toBeNull(); + expect(stepInfo!.total).toBeGreaterThan(0); + + const svgElement = page.locator("svg").first(); + await expect(svgElement).toBeVisible(); }); } diff --git a/e2e/specs/algorithms/linked-lists.spec.ts b/e2e/specs/algorithms/linked-lists.spec.ts index da81cf2f..5bc9f801 100644 --- a/e2e/specs/algorithms/linked-lists.spec.ts +++ b/e2e/specs/algorithms/linked-lists.spec.ts @@ -6,22 +6,16 @@ const { allAlgorithms, algorithmCategories } = discoverAlgorithms(); const categoryAlgos = allAlgorithms.filter((_, idx) => algorithmCategories[idx] === "linked-lists"); for (const algo of categoryAlgos) { - test.describe(`Linked Lists: ${algo}`, () => { - test(`selects "${algo}" and generates steps`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepInfo = await getStepIndex(page); - expect(stepInfo).not.toBeNull(); - expect(stepInfo!.total).toBeGreaterThan(0); - }); + test(`Linked Lists: ${algo} — selects, generates steps, renders`, async ({ page }) => { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.waitForSelector("button[aria-label='Search algorithms']"); + await selectAlgorithm(page, algo); - test(`${algo}: SVG visualization renders`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const svgElement = page.locator("svg").first(); - await expect(svgElement).toBeVisible(); - }); + const stepInfo = await getStepIndex(page); + expect(stepInfo).not.toBeNull(); + expect(stepInfo!.total).toBeGreaterThan(0); + + const svgElement = page.locator("svg").first(); + await expect(svgElement).toBeVisible(); }); } diff --git a/e2e/specs/algorithms/matrices.spec.ts b/e2e/specs/algorithms/matrices.spec.ts index 0d8f5316..1b07dc01 100644 --- a/e2e/specs/algorithms/matrices.spec.ts +++ b/e2e/specs/algorithms/matrices.spec.ts @@ -6,23 +6,16 @@ const { allAlgorithms, algorithmCategories } = discoverAlgorithms(); const categoryAlgos = allAlgorithms.filter((_, idx) => algorithmCategories[idx] === "matrices"); for (const algo of categoryAlgos) { - test.describe(`Matrices: ${algo}`, () => { - test(`selects "${algo}" and generates steps`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepInfo = await getStepIndex(page); - expect(stepInfo).not.toBeNull(); - expect(stepInfo!.total).toBeGreaterThan(0); - }); + test(`Matrices: ${algo} — selects, generates steps, renders`, async ({ page }) => { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.waitForSelector("button[aria-label='Search algorithms']"); + await selectAlgorithm(page, algo); - test(`${algo}: matrix visualization renders`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - // Matrix visualizer uses table or grid of cells - const matrixCell = page.locator("td, [class*='matrix'] div, [class*='grid'] div").first(); - await expect(matrixCell).toBeVisible(); - }); + const stepInfo = await getStepIndex(page); + expect(stepInfo).not.toBeNull(); + expect(stepInfo!.total).toBeGreaterThan(0); + + const matrixCell = page.locator("td, [class*='matrix'] div, [class*='grid'] div").first(); + await expect(matrixCell).toBeVisible(); }); } diff --git a/e2e/specs/algorithms/pathfinding.spec.ts b/e2e/specs/algorithms/pathfinding.spec.ts index 3ceb7c07..ef78da5f 100644 --- a/e2e/specs/algorithms/pathfinding.spec.ts +++ b/e2e/specs/algorithms/pathfinding.spec.ts @@ -6,24 +6,18 @@ const { allAlgorithms, algorithmCategories } = discoverAlgorithms(); const categoryAlgos = allAlgorithms.filter((_, idx) => algorithmCategories[idx] === "pathfinding"); for (const algo of categoryAlgos) { - test.describe(`Pathfinding: ${algo}`, () => { - test(`selects "${algo}" and generates steps`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepInfo = await getStepIndex(page); - expect(stepInfo).not.toBeNull(); - expect(stepInfo!.total).toBeGreaterThan(0); - }); + test(`Pathfinding: ${algo} — selects, generates steps, renders`, async ({ page }) => { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.waitForSelector("button[aria-label='Search algorithms']"); + await selectAlgorithm(page, algo); - test(`${algo}: grid visualization renders`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const gridCells = page.locator("[class*='grid'] > div"); - await gridCells.first().waitFor({ timeout: 5000 }); - const cellCount = await gridCells.count(); - expect(cellCount).toBeGreaterThan(0); - }); + const stepInfo = await getStepIndex(page); + expect(stepInfo).not.toBeNull(); + expect(stepInfo!.total).toBeGreaterThan(0); + + const gridCells = page.locator("[class*='grid'] > div"); + await gridCells.first().waitFor({ timeout: 5000 }); + const cellCount = await gridCells.count(); + expect(cellCount).toBeGreaterThan(0); }); } diff --git a/e2e/specs/algorithms/searching.spec.ts b/e2e/specs/algorithms/searching.spec.ts index 2ff5a9a7..b4d8a0c9 100644 --- a/e2e/specs/algorithms/searching.spec.ts +++ b/e2e/specs/algorithms/searching.spec.ts @@ -6,23 +6,17 @@ const { allAlgorithms, algorithmCategories } = discoverAlgorithms(); const categoryAlgos = allAlgorithms.filter((_, idx) => algorithmCategories[idx] === "searching"); for (const algo of categoryAlgos) { - test.describe(`Searching: ${algo}`, () => { - test(`selects "${algo}" and generates steps`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepInfo = await getStepIndex(page); - expect(stepInfo).not.toBeNull(); - expect(stepInfo!.total).toBeGreaterThan(0); - }); + test(`Searching: ${algo} — selects, generates steps, renders`, async ({ page }) => { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.waitForSelector("button[aria-label='Search algorithms']"); + await selectAlgorithm(page, algo); - test(`${algo}: visualization renders`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const values = await getDisplayedArrayValues(page); - expect(values).not.toBeNull(); - expect(values!.length).toBeGreaterThan(0); - }); + const stepInfo = await getStepIndex(page); + expect(stepInfo).not.toBeNull(); + expect(stepInfo!.total).toBeGreaterThan(0); + + const values = await getDisplayedArrayValues(page); + expect(values).not.toBeNull(); + expect(values!.length).toBeGreaterThan(0); }); } diff --git a/e2e/specs/algorithms/sets.spec.ts b/e2e/specs/algorithms/sets.spec.ts index aba99232..9180edb3 100644 --- a/e2e/specs/algorithms/sets.spec.ts +++ b/e2e/specs/algorithms/sets.spec.ts @@ -6,25 +6,19 @@ const { allAlgorithms, algorithmCategories } = discoverAlgorithms(); const categoryAlgos = allAlgorithms.filter((_, idx) => algorithmCategories[idx] === "sets"); for (const algo of categoryAlgos) { - test.describe(`Sets: ${algo}`, () => { - test(`selects "${algo}" and generates steps`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepInfo = await getStepIndex(page); - expect(stepInfo).not.toBeNull(); - expect(stepInfo!.total).toBeGreaterThan(0); - }); + test(`Sets: ${algo} — selects, generates steps, renders`, async ({ page }) => { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.waitForSelector("button[aria-label='Search algorithms']"); + await selectAlgorithm(page, algo); - test(`${algo}: visualization renders`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepCounter = page - .locator("span") - .filter({ hasText: /^\d+ \/ \d+$/ }) - .first(); - await expect(stepCounter).toBeVisible(); - }); + const stepInfo = await getStepIndex(page); + expect(stepInfo).not.toBeNull(); + expect(stepInfo!.total).toBeGreaterThan(0); + + const stepCounter = page + .locator("span") + .filter({ hasText: /^\d+ \/ \d+$/ }) + .first(); + await expect(stepCounter).toBeVisible(); }); } diff --git a/e2e/specs/algorithms/sorting.spec.ts b/e2e/specs/algorithms/sorting.spec.ts index 750aa32f..d092cf69 100644 --- a/e2e/specs/algorithms/sorting.spec.ts +++ b/e2e/specs/algorithms/sorting.spec.ts @@ -13,37 +13,27 @@ const { allAlgorithms, algorithmCategories } = discoverAlgorithms(); const categoryAlgos = allAlgorithms.filter((_, idx) => algorithmCategories[idx] === "sorting"); for (const algo of categoryAlgos) { - test.describe(`Sorting: ${algo}`, () => { - test(`selects "${algo}" and generates steps`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepInfo = await getStepIndex(page); - expect(stepInfo).not.toBeNull(); - expect(stepInfo!.total).toBeGreaterThan(0); - }); + test(`Sorting: ${algo} — selects, generates steps, renders, sorts`, async ({ page }) => { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.waitForSelector("button[aria-label='Search algorithms']"); + await selectAlgorithm(page, algo); - test(`${algo}: visualization renders`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const values = await getDisplayedArrayValues(page); - expect(values).not.toBeNull(); - expect(values!.length).toBeGreaterThan(0); - }); + const stepInfo = await getStepIndex(page); + expect(stepInfo).not.toBeNull(); + expect(stepInfo!.total).toBeGreaterThan(0); - test(`${algo}: final values sorted`, async ({ page }) => { - if (algo === STALIN_SORT) return; - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); + const values = await getDisplayedArrayValues(page); + expect(values).not.toBeNull(); + expect(values!.length).toBeGreaterThan(0); + + if (algo !== STALIN_SORT) { await goToLastStep(page); - const values = await getDisplayedArrayValues(page); - expect(values).not.toBeNull(); - expect(values!.length).toBeGreaterThan(0); - for (let valueIndex = 1; valueIndex < values!.length; valueIndex++) { - expect(values![valueIndex]).toBeGreaterThanOrEqual(values![valueIndex - 1]); + const finalValues = await getDisplayedArrayValues(page); + expect(finalValues).not.toBeNull(); + expect(finalValues!.length).toBeGreaterThan(0); + for (let valueIndex = 1; valueIndex < finalValues!.length; valueIndex++) { + expect(finalValues![valueIndex]).toBeGreaterThanOrEqual(finalValues![valueIndex - 1]); } - }); + } }); } diff --git a/e2e/specs/algorithms/stacks-queues.spec.ts b/e2e/specs/algorithms/stacks-queues.spec.ts index 80f7ea99..65e40b02 100644 --- a/e2e/specs/algorithms/stacks-queues.spec.ts +++ b/e2e/specs/algorithms/stacks-queues.spec.ts @@ -8,25 +8,21 @@ const categoryAlgos = allAlgorithms.filter( ); for (const algo of categoryAlgos) { - test.describe(`Stacks & Queues: ${algo}`, () => { - test(`selects "${algo}" and generates steps`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepInfo = await getStepIndex(page); - expect(stepInfo).not.toBeNull(); - expect(stepInfo!.total).toBeGreaterThan(0); - }); + test(`Stacks & Queues: ${algo} — selects, generates steps, renders`, async ({ page }) => { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.waitForSelector("button[aria-label='Search algorithms']"); + await selectAlgorithm(page, algo); - test(`${algo}: visualization renders`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepCounter = page - .locator("span") - .filter({ hasText: /^\d+ \/ \d+$/ }) - .first(); - await expect(stepCounter).toBeVisible(); - }); + // Verify step generation + const stepInfo = await getStepIndex(page); + expect(stepInfo).not.toBeNull(); + expect(stepInfo!.total).toBeGreaterThan(0); + + // Verify visualization renders + const stepCounter = page + .locator("span") + .filter({ hasText: /^\d+ \/ \d+$/ }) + .first(); + await expect(stepCounter).toBeVisible(); }); } diff --git a/e2e/specs/algorithms/strings.spec.ts b/e2e/specs/algorithms/strings.spec.ts index 463ea7ea..e78d9c0e 100644 --- a/e2e/specs/algorithms/strings.spec.ts +++ b/e2e/specs/algorithms/strings.spec.ts @@ -6,25 +6,19 @@ const { allAlgorithms, algorithmCategories } = discoverAlgorithms(); const categoryAlgos = allAlgorithms.filter((_, idx) => algorithmCategories[idx] === "strings"); for (const algo of categoryAlgos) { - test.describe(`Strings: ${algo}`, () => { - test(`selects "${algo}" and generates steps`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepInfo = await getStepIndex(page); - expect(stepInfo).not.toBeNull(); - expect(stepInfo!.total).toBeGreaterThan(0); - }); + test(`Strings: ${algo} — selects, generates steps, renders`, async ({ page }) => { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.waitForSelector("button[aria-label='Search algorithms']"); + await selectAlgorithm(page, algo); - test(`${algo}: visualization renders`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepCounter = page - .locator("span") - .filter({ hasText: /^\d+ \/ \d+$/ }) - .first(); - await expect(stepCounter).toBeVisible(); - }); + const stepInfo = await getStepIndex(page); + expect(stepInfo).not.toBeNull(); + expect(stepInfo!.total).toBeGreaterThan(0); + + const stepCounter = page + .locator("span") + .filter({ hasText: /^\d+ \/ \d+$/ }) + .first(); + await expect(stepCounter).toBeVisible(); }); } diff --git a/e2e/specs/algorithms/trees.spec.ts b/e2e/specs/algorithms/trees.spec.ts index c43947a8..6413de40 100644 --- a/e2e/specs/algorithms/trees.spec.ts +++ b/e2e/specs/algorithms/trees.spec.ts @@ -6,22 +6,16 @@ const { allAlgorithms, algorithmCategories } = discoverAlgorithms(); const categoryAlgos = allAlgorithms.filter((_, idx) => algorithmCategories[idx] === "trees"); for (const algo of categoryAlgos) { - test.describe(`Trees: ${algo}`, () => { - test(`selects "${algo}" and generates steps`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const stepInfo = await getStepIndex(page); - expect(stepInfo).not.toBeNull(); - expect(stepInfo!.total).toBeGreaterThan(0); - }); + test(`Trees: ${algo} — selects, generates steps, renders`, async ({ page }) => { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.waitForSelector("button[aria-label='Search algorithms']"); + await selectAlgorithm(page, algo); - test(`${algo}: SVG tree visualization renders`, async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForSelector("button[aria-label='Search algorithms']"); - await selectAlgorithm(page, algo); - const svgElement = page.locator("svg").first(); - await expect(svgElement).toBeVisible(); - }); + const stepInfo = await getStepIndex(page); + expect(stepInfo).not.toBeNull(); + expect(stepInfo!.total).toBeGreaterThan(0); + + const svgElement = page.locator("svg").first(); + await expect(svgElement).toBeVisible(); }); } diff --git a/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/BasicCalculatorPipeline.stories.tsx b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/BasicCalculatorPipeline.stories.tsx new file mode 100644 index 00000000..f42a1298 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/BasicCalculatorPipeline.stories.tsx @@ -0,0 +1,57 @@ +/** + * Storybook stories for the Basic Calculator algorithm pipeline. + * Uses the real step generator with "1 + (2 - 3)", rendering the + * StackQueueVisualizer at key evaluation states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateBasicCalculatorSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateBasicCalculatorSteps({ expression: "1 + (2 - 3)" }); +const complexSteps = generateBasicCalculatorSteps({ expression: "(1+(4+5+2)-3)+(6+8)" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Basic Calculator", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — full token list unprocessed, empty stack */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing with tokens partially evaluated */ +export const MidProcessing: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Completed evaluation — final result with empty stack */ +export const EvaluationComplete: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Complex nested expression — mid-evaluation with parentheses on stack */ +export const ComplexExpression: Story = { + args: { + visualState: complexSteps[Math.floor(complexSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/basic-calculator.test.ts b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/basic-calculator.test.ts new file mode 100644 index 00000000..7f556243 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/basic-calculator.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from "vitest"; +import { basicCalculator } from "./sources/basic-calculator.ts?fn"; + +describe("basicCalculator", () => { + it("evaluates a simple addition", () => { + expect(basicCalculator("1 + 1")).toBe(2); + }); + + it("evaluates mixed addition and subtraction with leading/trailing spaces", () => { + expect(basicCalculator(" 2-1 + 2 ")).toBe(3); + }); + + it("evaluates a complex nested expression", () => { + expect(basicCalculator("(1+(4+5+2)-3)+(6+8)")).toBe(23); + }); + + it("evaluates the default input expression", () => { + expect(basicCalculator("1 + (2 - 3)")).toBe(0); + }); + + it("evaluates a single positive number", () => { + expect(basicCalculator("42")).toBe(42); + }); + + it("evaluates a simple subtraction", () => { + expect(basicCalculator("10 - 3")).toBe(7); + }); + + it("evaluates deeply nested parentheses", () => { + expect(basicCalculator("(((1 + 2)))")).toBe(3); + }); + + it("handles negative result from subtraction inside parentheses", () => { + expect(basicCalculator("1 - (2 + 3)")).toBe(-4); + }); +}); diff --git a/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/educational.ts b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/educational.ts new file mode 100644 index 00000000..228aabc0 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/educational.ts @@ -0,0 +1,59 @@ +import type { EducationalContent } from "@/types"; + +export const basicCalculatorEducational: EducationalContent = { + overview: + "**Basic Calculator** evaluates a string expression containing integers, `+`, `-`, `(`, and `)`. The challenge is correctly handling nested parentheses, where the sign outside a pair of parentheses must be applied to every value inside.\n\nA stack solves this elegantly: when a `(` is encountered, push the current running total and sign onto the stack and start fresh. When `)` is encountered, pop those saved values and merge the sub-expression result back in.", + + howItWorks: + "The algorithm tokenizes the expression and processes each token with a running total and a current sign:\n\n" + + "1. **Number token** → add `currentSign × number` to `runningTotal`.\n" + + "2. **`+`** → set `currentSign = 1`.\n" + + "3. **`-`** → set `currentSign = -1`.\n" + + "4. **`(`** → push `runningTotal` and `currentSign` onto the stack, then reset both (`runningTotal = 0`, `currentSign = 1`).\n" + + "5. **`)`** → pop `poppedSign` and `prevTotal` from the stack. Combine: `runningTotal = prevTotal + poppedSign × runningTotal`.\n\n" + + "### Example trace on `1 + (2 - 3)`\n\n" + + "```\n" + + "token action runningTotal currentSign stack\n" + + "1 add 1×1=1 1 1 []\n" + + "+ sign = +1 1 1 []\n" + + "( push 1, push +1; reset 0 1 [1, 1]\n" + + "2 add 1×2=2 2 1 [1, 1]\n" + + "- sign = -1 2 -1 [1, 1]\n" + + "3 add -1×3=-3; total=-1 -1 -1 [1, 1]\n" + + ") pop +1 and 1; 1+(+1×-1) 0 -1 []\n" + + "end return 0\n" + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each token is processed exactly once. Tokenizing the expression is also `O(n)`, so the overall time is linear in the length of the input string.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The stack can hold at most one pair of values per nesting level. In the worst case (maximally nested parentheses), the stack grows to `O(n)` entries.", + + bestAndWorstCase: + "**Best case** — a flat expression with no parentheses (e.g. `1 + 2 - 3`): the stack is never used, so `O(n)` time and `O(1)` space.\n\n" + + "**Worst case** — deeply nested parentheses (e.g. `(((1 + 2)))`) or a very long expression: `O(n)` time and `O(n)` stack space for the saved totals and signs.", + + realWorldUses: [ + "**Spreadsheet formula engines:** Cell formulas with nested parentheses like `=((A1+B1)*C1)` are evaluated using this exact approach.", + "**Programming language interpreters:** Simple scripting engines parse and evaluate arithmetic expressions using sign-propagation stacks before building a full AST.", + "**Scientific calculators:** Real-time expression evaluation as users type nested sub-expressions relies on stack-based sign tracking.", + "**Query parsers:** Search engines that support parenthesized boolean queries (`AND`, `OR`, `NOT`) use a similar sign-on-stack pattern for precedence resolution.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(n) single-pass evaluation — no AST construction or recursive descent required.", + "Naturally handles arbitrarily deep nesting through the stack.", + "Simple, readable implementation that closely mirrors the problem description.", + ], + limitations: [ + "Does not support multiplication or division — extending it requires operator-precedence handling (e.g. shunting-yard algorithm).", + "O(n) extra stack space in the worst case for deeply nested expressions.", + "Requires valid, pre-validated input — malformed expressions (unmatched parentheses) can cause stack underflow.", + ], + }, + + whenToUseIt: + "Use this pattern whenever you need to evaluate expressions that contain only addition, subtraction, and parentheses. If the expression also involves `*` or `/`, use the shunting-yard algorithm or recursive descent parsing instead. The sign-propagation stack is the most efficient solution for the LeetCode 224 constraint set.", +}; diff --git a/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/index.ts b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/index.ts new file mode 100644 index 00000000..983b0302 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { basicCalculator } from "./sources/basic-calculator.ts?fn"; +import { generateBasicCalculatorSteps } from "./step-generator"; +import type { BasicCalculatorInput } from "./step-generator"; +import { basicCalculatorEducational } from "./educational"; + +import typescriptSource from "./sources/basic-calculator.ts?raw"; +import pythonSource from "./sources/basic-calculator.py?raw"; +import javaSource from "./sources/BasicCalculator.java?raw"; + +function executeBasicCalculator(input: BasicCalculatorInput): number { + return basicCalculator(input.expression) as number; +} + +const basicCalculatorDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BASIC_CALCULATOR!, + name: "Basic Calculator", + category: CATEGORY.STACKS_QUEUES!, + technique: "expression-evaluation", + description: + "Evaluate a simple expression string with +, -, (, ) using a stack to propagate signs across nested parentheses", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { expression: "1 + (2 - 3)" }, + }, + execute: executeBasicCalculator, + generateSteps: generateBasicCalculatorSteps, + educational: basicCalculatorEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(basicCalculatorDefinition); diff --git a/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/sources/BasicCalculator.java b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/sources/BasicCalculator.java new file mode 100644 index 00000000..ceaac9ee --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/sources/BasicCalculator.java @@ -0,0 +1,49 @@ +// Basic Calculator — evaluate a simple expression string with +, -, (, ) using a stack for sign propagation +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.ArrayList; +import java.util.List; + +class BasicCalculator { + public int basicCalculator(String expression) { + Deque signStack = new ArrayDeque<>(); // @step:initialize + int runningTotal = 0; // @step:initialize + int currentSign = 1; // @step:initialize + + List tokens = tokenize(expression); // @step:initialize + + for (String currentToken : tokens) { // @step:visit + if (currentToken.matches("\\d+")) { + runningTotal += currentSign * Integer.parseInt(currentToken); // @step:evaluate + } else if (currentToken.equals("+")) { + currentSign = 1; // @step:visit + } else if (currentToken.equals("-")) { + currentSign = -1; // @step:visit + } else if (currentToken.equals("(")) { + // Save current running total and sign, then reset for the sub-expression + signStack.push(runningTotal); // @step:push + signStack.push(currentSign); // @step:push + runningTotal = 0; // @step:push + currentSign = 1; // @step:push + } else if (currentToken.equals(")")) { + // Pop sign and previous total, merge sub-expression result into parent context + int poppedSign = signStack.pop(); // @step:pop + int prevTotal = signStack.pop(); // @step:pop + runningTotal = prevTotal + poppedSign * runningTotal; // @step:pop + } + } + + return runningTotal; // @step:complete + } + + private List tokenize(String expression) { + List tokens = new ArrayList<>(); + Matcher matcher = Pattern.compile("\\d+|[+\\-()]").matcher(expression); + while (matcher.find()) { + tokens.add(matcher.group()); + } + return tokens; + } +} diff --git a/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/sources/basic-calculator.py b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/sources/basic-calculator.py new file mode 100644 index 00000000..2a52f2f8 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/sources/basic-calculator.py @@ -0,0 +1,34 @@ +# Basic Calculator — evaluate a simple expression string with +, -, (, ) using a stack for sign propagation +import re + +def basic_calculator(expression: str) -> int: + sign_stack: list[int] = [] # @step:initialize + running_total: int = 0 # @step:initialize + current_sign: int = 1 # @step:initialize + + tokens = re.findall(r'\d+|[+\-()] ', expression) or re.findall(r'\d+|[+\-()]', expression) # @step:initialize + + for current_token in tokens: # @step:visit + current_token = current_token.strip() + if not current_token: + continue + + if current_token.isdigit() or (len(current_token) > 1 and current_token.isdigit()): + running_total += current_sign * int(current_token) # @step:evaluate + elif current_token == '+': + current_sign = 1 # @step:visit + elif current_token == '-': + current_sign = -1 # @step:visit + elif current_token == '(': + # Save current running total and sign, then reset for the sub-expression + sign_stack.append(running_total) # @step:push + sign_stack.append(current_sign) # @step:push + running_total = 0 # @step:push + current_sign = 1 # @step:push + elif current_token == ')': + # Pop sign and previous total, merge sub-expression result into parent context + popped_sign = sign_stack.pop() # @step:pop + prev_total = sign_stack.pop() # @step:pop + running_total = prev_total + popped_sign * running_total # @step:pop + + return running_total # @step:complete diff --git a/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/sources/basic-calculator.ts b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/sources/basic-calculator.ts new file mode 100644 index 00000000..662dc44d --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/sources/basic-calculator.ts @@ -0,0 +1,33 @@ +// Basic Calculator — evaluate a simple expression string with +, -, (, ) using a stack for sign propagation +function basicCalculator(expression: string): number { + const signStack: number[] = []; // @step:initialize + let runningTotal = 0; // @step:initialize + let currentSign = 1; // @step:initialize + + const tokens = expression.match(/\d+|[+\-()]/g) ?? []; // @step:initialize + + for (let tokenIdx = 0; tokenIdx < tokens.length; tokenIdx++) { + const currentToken = tokens[tokenIdx]!; // @step:visit + + if (/\d+/.test(currentToken)) { + runningTotal += currentSign * parseInt(currentToken, 10); // @step:evaluate + } else if (currentToken === "+") { + currentSign = 1; // @step:visit + } else if (currentToken === "-") { + currentSign = -1; // @step:visit + } else if (currentToken === "(") { + // Save current running total and sign, then reset for the sub-expression + signStack.push(runningTotal); // @step:push + signStack.push(currentSign); // @step:push + runningTotal = 0; // @step:push + currentSign = 1; // @step:push + } else if (currentToken === ")") { + // Pop sign and previous total, merge sub-expression result into parent context + const poppedSign = signStack.pop()!; // @step:pop + const prevTotal = signStack.pop()!; // @step:pop + runningTotal = prevTotal + poppedSign * runningTotal; // @step:pop + } + } + + return runningTotal; // @step:complete +} diff --git a/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/step-generator.test.ts b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/step-generator.test.ts new file mode 100644 index 00000000..a2ef15b5 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/step-generator.test.ts @@ -0,0 +1,72 @@ +import { describe, it, expect } from "vitest"; +import { generateBasicCalculatorSteps } from "./step-generator"; + +describe("generateBasicCalculatorSteps", () => { + it("produces steps for the default input", () => { + const steps = generateBasicCalculatorSteps({ expression: "1 + (2 - 3)" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateBasicCalculatorSteps({ expression: "1 + (2 - 3)" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateBasicCalculatorSteps({ expression: "1 + (2 - 3)" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateBasicCalculatorSteps({ expression: "1 + (2 - 3)" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateBasicCalculatorSteps({ expression: "1 + (2 - 3)" }); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits a visit step for each token", () => { + // "1 + (2 - 3)" tokenizes to ["1", "+", "(", "2", "-", "3", ")"] = 7 tokens + const steps = generateBasicCalculatorSteps({ expression: "1 + (2 - 3)" }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(7); + }); + + it("emits a push step for each open parenthesis", () => { + const steps = generateBasicCalculatorSteps({ expression: "1 + (2 - 3)" }); + const pushSteps = steps.filter((step) => step.type === "push"); + // One push for "(" and one for each operand number: "1", "2" = 3 pushes + expect(pushSteps.length).toBeGreaterThanOrEqual(1); + }); + + it("emits an evaluate step for the closing parenthesis", () => { + const steps = generateBasicCalculatorSteps({ expression: "1 + (2 - 3)" }); + const evaluateSteps = steps.filter((step) => step.type === "evaluate"); + expect(evaluateSteps.length).toBe(1); + }); + + it("the complete step description contains the final result", () => { + const steps = generateBasicCalculatorSteps({ expression: "1 + 1" }); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.description).toContain("2"); + }); + + it("handles a flat expression with no parentheses", () => { + const steps = generateBasicCalculatorSteps({ expression: "2 + 3" }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + expect(steps[steps.length - 1]?.description).toContain("5"); + }); + + it("handles a complex nested expression", () => { + const steps = generateBasicCalculatorSteps({ expression: "(1+(4+5+2)-3)+(6+8)" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + expect(steps[steps.length - 1]?.description).toContain("23"); + }); +}); diff --git a/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/step-generator.ts b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/step-generator.ts new file mode 100644 index 00000000..bfe1f140 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/basic-calculator/step-generator.ts @@ -0,0 +1,96 @@ +/** Step generator for Basic Calculator — produces ExecutionStep[] using ExpressionTracker. */ + +import type { ExecutionStep } from "@/types"; +import { ExpressionTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BC_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BASIC_CALCULATOR!); + +export interface BasicCalculatorInput { + expression: string; +} + +function tokenizeExpression(expression: string): string[] { + return expression.match(/\d+|[+\-()]/g) ?? []; +} + +export function generateBasicCalculatorSteps(input: BasicCalculatorInput): ExecutionStep[] { + const { expression } = input; + const tokens = tokenizeExpression(expression); + const tracker = new ExpressionTracker(tokens, BC_LINE_MAP); + + const signStack: number[] = []; + let runningTotal = 0; + let currentSign = 1; + + tracker.initialize({ expression, runningTotal, currentSign, stackDepth: 0 }); + + for (let tokenIdx = 0; tokenIdx < tokens.length; tokenIdx++) { + const currentToken = tokens[tokenIdx]!; + + tracker.processToken(tokenIdx, { + tokenIdx, + currentToken, + runningTotal, + currentSign, + stackDepth: signStack.length / 2, + }); + + if (/^\d+$/.test(currentToken)) { + const numericValue = parseInt(currentToken, 10); + runningTotal += currentSign * numericValue; + tracker.pushOperand(currentToken, tokenIdx, { + tokenIdx, + currentToken, + numericValue, + runningTotal, + currentSign, + }); + } else if (currentToken === "+") { + currentSign = 1; + } else if (currentToken === "-") { + currentSign = -1; + } else if (currentToken === "(") { + signStack.push(runningTotal); + signStack.push(currentSign); + const savedTotal = runningTotal; + const savedSign = currentSign; + runningTotal = 0; + currentSign = 1; + tracker.pushOperator("(", tokenIdx, { + tokenIdx, + currentToken, + savedTotal, + savedSign, + runningTotal, + currentSign, + stackDepth: signStack.length / 2, + }); + } else if (currentToken === ")") { + const poppedSign = signStack.pop()!; + const prevTotal = signStack.pop()!; + const subResult = runningTotal; + runningTotal = prevTotal + poppedSign * subResult; + tracker.popAndEvaluate( + ")", + String(poppedSign === 1 ? "+" : "-"), + String(subResult), + String(runningTotal), + { + tokenIdx, + currentToken, + poppedSign, + prevTotal, + subResult, + runningTotal, + stackDepth: signStack.length / 2, + }, + ); + } + } + + tracker.complete(String(runningTotal), { result: runningTotal }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/EvaluateReversePolishPipeline.stories.tsx b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/EvaluateReversePolishPipeline.stories.tsx new file mode 100644 index 00000000..e02dea2b --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/EvaluateReversePolishPipeline.stories.tsx @@ -0,0 +1,58 @@ +/** + * Storybook stories for the Evaluate Reverse Polish algorithm pipeline. + * Uses the real step generator with ["2", "1", "+", "3", "*"], rendering the + * StackQueueVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateEvaluateReversePolishSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateEvaluateReversePolishSteps({ tokens: ["2", "1", "+", "3", "*"] }); +const complexSteps = generateEvaluateReversePolishSteps({ + tokens: ["4", "13", "5", "/", "+"], +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Evaluate Reverse Polish", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — full token list unprocessed, empty operand stack */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing with operands pushed and first evaluation complete */ +export const MidProcessing: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Completed evaluation — single result value on the stack */ +export const EvaluationComplete: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Division then addition — ["4", "13", "5", "/", "+"] = 6 */ +export const DivisionThenAddition: Story = { + args: { + visualState: complexSteps[complexSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/educational.ts b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/educational.ts new file mode 100644 index 00000000..715dc136 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/educational.ts @@ -0,0 +1,64 @@ +import type { EducationalContent } from "@/types"; + +export const evaluateReversePolishEducational: EducationalContent = { + overview: + "**Evaluate Reverse Polish Notation** (RPN), also called postfix notation, is an expression format where operators follow their operands. For example, `2 1 + 3 *` means `(2 + 1) * 3 = 9`.\n\nA stack is the natural data structure for this: push every number, and when an operator is encountered, pop two operands, compute, and push the result back. No parentheses or precedence rules are needed — the notation encodes order of operations directly.", + + howItWorks: + "The algorithm scans left to right through the token array:\n\n" + + "1. **Number token** → parse and push onto the operand stack.\n" + + "2. **Operator token** (`+`, `-`, `*`, `/`):\n" + + " - Pop the top two values: `operandB` (last pushed), then `operandA`.\n" + + " - Compute `operandA operator operandB`.\n" + + " - Push the result back onto the stack.\n" + + "3. **End of tokens** → the single value remaining on the stack is the answer.\n\n" + + '### Example trace on `["2", "1", "+", "3", "*"]`\n\n' + + "```\n" + + "token action stack\n" + + "2 push [2]\n" + + "1 push [2, 1]\n" + + "+ pop 1, pop 2 []\n" + + " compute 2 + 1 = 3\n" + + " push 3 [3]\n" + + "3 push [3, 3]\n" + + "* pop 3, pop 3 []\n" + + " compute 3 * 3 = 9\n" + + " push 9 [9]\n" + + "end return stack[0] = 9\n" + + "```\n\n" + + "Division truncates toward zero (matching LeetCode 150's requirement), so `-7 / 2 = -3`, not `-4`.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each token is visited exactly once. Each push and pop is `O(1)`, so total time is linear in the number of tokens.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "In the worst case (all operands, no operators) the entire token list is pushed onto the stack, using `O(n)` extra space.", + + bestAndWorstCase: + '**Best case** — a single-token expression like `["5"]`: one push and one return, `O(1)` work.\n\n' + + '**Worst case** — all tokens are operands followed by operators (e.g. `["1", "2", "3", "+", "+"]`): every token is processed in sequence, giving `O(n)` time and up to `O(n/2)` stack depth.\n\n' + + "The algorithm always completes in a single linear pass — there is no early termination path.", + + realWorldUses: [ + "**Stack-based virtual machines:** The Java Virtual Machine (JVM) and CPython interpreter use postfix-like bytecode internally to evaluate expressions.", + "**Scientific calculators:** Classic HP RPN calculators use postfix notation, letting users avoid parentheses by entering operations in execution order.", + "**Compiler back-ends:** After parsing an infix expression into an AST, many compilers emit postfix instructions for easier machine-code generation.", + "**Spreadsheet formula engines:** Some engines convert user-entered infix formulas to RPN internally for efficient evaluation.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(n) time — single pass with no backtracking or lookahead.", + "No parentheses needed — precedence is implicit in token order.", + "Simple, branch-minimal loop body with no recursion.", + ], + limitations: [ + "O(n) stack space in the worst case.", + "Assumes well-formed input — invalid RPN (too few operands for an operator) is undefined behavior without extra validation.", + "Less human-readable than standard infix notation; requires preprocessing to generate from user-entered expressions.", + ], + }, + + whenToUseIt: + "Use this pattern whenever you need to evaluate a postfix expression or process an operator stream that was generated by an infix-to-postfix conversion (shunting-yard algorithm). If you are building a full expression evaluator that accepts infix input, first convert to RPN, then apply this algorithm. For single-operator precedence levels (only + and -), a simpler left-to-right scan without a stack is sufficient.", +}; diff --git a/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/evaluate-reverse-polish.test.ts b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/evaluate-reverse-polish.test.ts new file mode 100644 index 00000000..e39d2a4a --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/evaluate-reverse-polish.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from "vitest"; +import { evaluateReversePolish } from "./sources/evaluate-reverse-polish.ts?fn"; + +describe("evaluateReversePolish", () => { + it("evaluates a simple addition followed by multiplication", () => { + expect(evaluateReversePolish(["2", "1", "+", "3", "*"])).toBe(9); + }); + + it("evaluates division then addition", () => { + expect(evaluateReversePolish(["4", "13", "5", "/", "+"])).toBe(6); + }); + + it("evaluates the complex LeetCode example", () => { + expect( + evaluateReversePolish(["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"]), + ).toBe(22); + }); + + it("evaluates a single operand", () => { + expect(evaluateReversePolish(["42"])).toBe(42); + }); + + it("evaluates simple addition", () => { + expect(evaluateReversePolish(["3", "4", "+"])).toBe(7); + }); + + it("evaluates subtraction", () => { + expect(evaluateReversePolish(["10", "3", "-"])).toBe(7); + }); + + it("evaluates multiplication", () => { + expect(evaluateReversePolish(["5", "6", "*"])).toBe(30); + }); + + it("truncates division toward zero for positive operands", () => { + expect(evaluateReversePolish(["7", "2", "/"])).toBe(3); + }); + + it("truncates division toward zero for negative result", () => { + expect(evaluateReversePolish(["7", "-3", "/"])).toBe(-2); + }); + + it("handles negative operands in arithmetic", () => { + expect(evaluateReversePolish(["-3", "4", "*"])).toBe(-12); + }); + + it("evaluates a chained expression with multiple operators", () => { + // (2 + 3) * (4 - 1) = 5 * 3 = 15 + expect(evaluateReversePolish(["2", "3", "+", "4", "1", "-", "*"])).toBe(15); + }); +}); diff --git a/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/index.ts b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/index.ts new file mode 100644 index 00000000..8f71cf76 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { evaluateReversePolish } from "./sources/evaluate-reverse-polish.ts?fn"; +import { generateEvaluateReversePolishSteps } from "./step-generator"; +import type { EvaluateReversePolishInput } from "./step-generator"; +import { evaluateReversePolishEducational } from "./educational"; + +import typescriptSource from "./sources/evaluate-reverse-polish.ts?raw"; +import pythonSource from "./sources/evaluate-reverse-polish.py?raw"; +import javaSource from "./sources/EvaluateReversePolish.java?raw"; + +function executeEvaluateReversePolish(input: EvaluateReversePolishInput): number { + return evaluateReversePolish(input.tokens) as number; +} + +const evaluateReversePolishDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.EVALUATE_REVERSE_POLISH!, + name: "Evaluate Reverse Polish", + category: CATEGORY.STACKS_QUEUES!, + technique: "expression-evaluation", + description: + "Evaluate a postfix expression using an operand stack — push numbers, pop two and compute on each operator", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { tokens: ["2", "1", "+", "3", "*"] }, + }, + execute: executeEvaluateReversePolish, + generateSteps: generateEvaluateReversePolishSteps, + educational: evaluateReversePolishEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(evaluateReversePolishDefinition); diff --git a/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/sources/EvaluateReversePolish.java b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/sources/EvaluateReversePolish.java new file mode 100644 index 00000000..2ac2d38e --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/sources/EvaluateReversePolish.java @@ -0,0 +1,26 @@ +// Evaluate Reverse Polish Notation — push operands, pop two and compute on operators +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Set; + +public class EvaluateReversePolish { + public static int evaluateReversePolish(String[] tokens) { + Deque operandStack = new ArrayDeque<>(); // @step:initialize + Set operators = Set.of("+", "-", "*", "/"); // @step:initialize + for (String currentToken : tokens) { // @step:visit + if (operators.contains(currentToken)) { + int operandB = operandStack.pop(); // @step:evaluate + int operandA = operandStack.pop(); // @step:evaluate + int result; + if (currentToken.equals("+")) result = operandA + operandB; // @step:evaluate + else if (currentToken.equals("-")) result = operandA - operandB; // @step:evaluate + else if (currentToken.equals("*")) result = operandA * operandB; // @step:evaluate + else result = (int) (operandA / (double) operandB); // @step:evaluate + operandStack.push(result); // @step:push + } else { + operandStack.push(Integer.parseInt(currentToken)); // @step:push + } + } + return operandStack.peek(); // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/sources/evaluate-reverse-polish.py b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/sources/evaluate-reverse-polish.py new file mode 100644 index 00000000..7e5836f6 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/sources/evaluate-reverse-polish.py @@ -0,0 +1,20 @@ +# Evaluate Reverse Polish Notation — push operands, pop two and compute on operators +def evaluate_reverse_polish(tokens): + operand_stack = [] # @step:initialize + operators = {"+", "-", "*", "/"} # @step:initialize + for current_token in tokens: # @step:visit + if current_token in operators: + operand_b = operand_stack.pop() # @step:evaluate + operand_a = operand_stack.pop() # @step:evaluate + if current_token == "+": # @step:evaluate + result = operand_a + operand_b + elif current_token == "-": # @step:evaluate + result = operand_a - operand_b + elif current_token == "*": # @step:evaluate + result = operand_a * operand_b + else: + result = int(operand_a / operand_b) # @step:evaluate (truncate toward zero) + operand_stack.append(result) # @step:push + else: + operand_stack.append(int(current_token)) # @step:push + return operand_stack[0] # @step:complete diff --git a/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/sources/evaluate-reverse-polish.ts b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/sources/evaluate-reverse-polish.ts new file mode 100644 index 00000000..92e705fe --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/sources/evaluate-reverse-polish.ts @@ -0,0 +1,24 @@ +// Evaluate Reverse Polish Notation — push operands, pop two and compute on operators +function evaluateReversePolish(tokens: string[]): number { + const operandStack: number[] = []; // @step:initialize + const operators = new Set(["+", "-", "*", "/"]); // @step:initialize + for (let tokenIdx = 0; tokenIdx < tokens.length; tokenIdx++) { + const currentToken = tokens[tokenIdx]!; // @step:visit + if (operators.has(currentToken)) { + const operandB = operandStack.pop()!; // @step:evaluate + const operandA = operandStack.pop()!; // @step:evaluate + let result: number; + if (currentToken === "+") + result = operandA + operandB; // @step:evaluate + else if (currentToken === "-") + result = operandA - operandB; // @step:evaluate + else if (currentToken === "*") + result = operandA * operandB; // @step:evaluate + else result = Math.trunc(operandA / operandB); // @step:evaluate + operandStack.push(result); // @step:push + } else { + operandStack.push(parseInt(currentToken, 10)); // @step:push + } + } + return operandStack[0]!; // @step:complete +} diff --git a/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/step-generator.test.ts b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/step-generator.test.ts new file mode 100644 index 00000000..7be8efdb --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/step-generator.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from "vitest"; +import { generateEvaluateReversePolishSteps } from "./step-generator"; + +describe("generateEvaluateReversePolishSteps", () => { + it("produces steps for the default input", () => { + const steps = generateEvaluateReversePolishSteps({ tokens: ["2", "1", "+", "3", "*"] }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateEvaluateReversePolishSteps({ tokens: ["2", "1", "+", "3", "*"] }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateEvaluateReversePolishSteps({ tokens: ["2", "1", "+", "3", "*"] }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateEvaluateReversePolishSteps({ tokens: ["2", "1", "+", "3", "*"] }); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateEvaluateReversePolishSteps({ tokens: ["2", "1", "+", "3", "*"] }); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits visit steps for each token", () => { + const tokens = ["2", "1", "+", "3", "*"]; + const steps = generateEvaluateReversePolishSteps({ tokens }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(tokens.length); + }); + + it("emits push steps for operands, operators, and results", () => { + const steps = generateEvaluateReversePolishSteps({ tokens: ["2", "1", "+", "3", "*"] }); + // Operands: "2", "1", "3" → 3 pushes; operators: "+", "*" → 2 pushes; results → 2 pushes = 7 + const pushSteps = steps.filter((step) => step.type === "push"); + expect(pushSteps.length).toBe(7); + }); + + it("emits evaluate steps for each operator", () => { + const steps = generateEvaluateReversePolishSteps({ tokens: ["2", "1", "+", "3", "*"] }); + const evaluateSteps = steps.filter((step) => step.type === "evaluate"); + expect(evaluateSteps.length).toBe(2); + }); + + it("the complete step description contains the final result", () => { + const steps = generateEvaluateReversePolishSteps({ tokens: ["2", "1", "+", "3", "*"] }); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.description).toContain("9"); + }); + + it("handles a single operand token", () => { + const steps = generateEvaluateReversePolishSteps({ tokens: ["42"] }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.description).toContain("42"); + }); + + it("handles the complex LeetCode example", () => { + const complexTokens = ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"]; + const steps = generateEvaluateReversePolishSteps({ tokens: complexTokens }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.description).toContain("22"); + }); +}); diff --git a/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/step-generator.ts b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/step-generator.ts new file mode 100644 index 00000000..79361031 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/evaluate-reverse-polish/step-generator.ts @@ -0,0 +1,77 @@ +/** Step generator for Evaluate Reverse Polish Notation — produces ExecutionStep[] using ExpressionTracker. */ + +import type { ExecutionStep } from "@/types"; +import { ExpressionTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const ERP_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.EVALUATE_REVERSE_POLISH!); + +export interface EvaluateReversePolishInput { + tokens: string[]; +} + +const OPERATORS = new Set(["+", "-", "*", "/"]); + +function computeResult(operandA: number, operator: string, operandB: number): number { + if (operator === "+") return operandA + operandB; + if (operator === "-") return operandA - operandB; + if (operator === "*") return operandA * operandB; + return Math.trunc(operandA / operandB); +} + +export function generateEvaluateReversePolishSteps( + input: EvaluateReversePolishInput, +): ExecutionStep[] { + const { tokens } = input; + const tracker = new ExpressionTracker(tokens, ERP_LINE_MAP); + const operandStack: number[] = []; + + tracker.initialize({ tokens }); + + for (let tokenIdx = 0; tokenIdx < tokens.length; tokenIdx++) { + const currentToken = tokens[tokenIdx]!; + + tracker.processToken(tokenIdx, { tokenIdx, currentToken, stack: [...operandStack] }); + + if (OPERATORS.has(currentToken)) { + const operandB = operandStack.pop()!; + const operandA = operandStack.pop()!; + const result = computeResult(operandA, currentToken, operandB); + operandStack.push(result); + + // Push operator visually so popAndEvaluate can pop operator + two operands = 3 total + tracker.pushOperator(currentToken, tokenIdx, { + tokenIdx, + currentToken, + operandA, + operandB, + stack: [operandA, operandB, currentToken], + }); + + tracker.popAndEvaluate(currentToken, String(operandA), String(operandB), String(result), { + tokenIdx, + currentToken, + operandA, + operandB, + result, + stack: [...operandStack], + }); + } else { + const operandValue = parseInt(currentToken, 10); + operandStack.push(operandValue); + + tracker.pushOperand(currentToken, tokenIdx, { + tokenIdx, + currentToken, + operandValue, + stack: [...operandStack], + }); + } + } + + const finalResult = operandStack[0] ?? 0; + tracker.complete(String(finalResult), { result: finalResult }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/InfixToPostfixPipeline.stories.tsx b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/InfixToPostfixPipeline.stories.tsx new file mode 100644 index 00000000..71d732a3 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/InfixToPostfixPipeline.stories.tsx @@ -0,0 +1,56 @@ +/** + * Storybook stories for the Infix to Postfix (Shunting-Yard) algorithm pipeline. + * Uses the real step generator with "a+b*(c-d)", rendering the + * StackQueueVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateInfixToPostfixSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateInfixToPostfixSteps({ expression: "a+b*(c-d)" }); +const simpleSteps = generateInfixToPostfixSteps({ expression: "(a+b)*c" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Infix to Postfix", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — full token stream unprocessed, empty operator stack and output */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing — some tokens processed with operators accumulated on the stack */ +export const MidProcessing: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Completed — operator stack drained, full postfix output queue visible */ +export const ConversionComplete: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Parenthesized expression — shows how parens group operators correctly */ +export const ParenthesizedExpression: Story = { + args: { + visualState: simpleSteps[simpleSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/educational.ts b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/educational.ts new file mode 100644 index 00000000..124dcd57 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/educational.ts @@ -0,0 +1,69 @@ +import type { EducationalContent } from "@/types"; + +export const infixToPostfixEducational: EducationalContent = { + overview: + "**Infix to Postfix** (also called the **Shunting-Yard algorithm**, invented by Edsger Dijkstra) converts a human-readable infix expression like `a + b * c` into postfix (Reverse Polish Notation) form `a b c * +`.\n\n" + + "In postfix notation, operators follow their operands, eliminating the need for parentheses or precedence rules during evaluation. This makes postfix expressions ideal for stack-based evaluators used in calculators, compilers, and virtual machines.", + + howItWorks: + "The algorithm scans tokens left to right, routing operands directly to output and managing operators on a stack according to precedence:\n\n" + + "1. **Operand** → append directly to the output queue.\n" + + "2. **Operator** → pop and output any stack operators with higher or equal precedence, then push the current operator.\n" + + "3. **`(`** → push onto the operator stack (acts as a barrier).\n" + + "4. **`)`** → pop and output all operators until the matching `(` is found, then discard the `(`.\n" + + "5. **End of input** → pop and output all remaining operators from the stack.\n\n" + + "### Example trace on `a+b*(c-d)`\n\n" + + "```\n" + + "token action output stack\n" + + "a operand → output [a] []\n" + + "+ push (stack empty) [a] [+]\n" + + "b operand → output [a b] [+]\n" + + "* prec(*) > prec(+), push [a b] [+ *]\n" + + "( push paren [a b] [+ * (]\n" + + "c operand → output [a b c] [+ * (]\n" + + "- push (barrier at '(') [a b c] [+ * ( -]\n" + + "d operand → output [a b c d] [+ * ( -]\n" + + ") pop until '(': output - [a b c d -] [+ *]\n" + + "end drain stack: * then + [a b c d - * +] []\n" + + "```\n\n" + + "Result: `a b c d - * +`", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each token is read once. Each operator is pushed onto the stack at most once and popped at most once, so all stack operations are amortized `O(1)`. Total work is linear in the number of tokens.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The operator stack holds at most `O(n)` elements in the worst case (e.g., a deeply nested expression). The output queue also grows to `O(n)`.", + + bestAndWorstCase: + "**Best case** — a single operand with no operators: the expression is output immediately in `O(1)` with no stack activity.\n\n" + + "**Worst case** — a heavily parenthesized expression or one with many low-precedence operators that accumulate on the stack before being popped: still `O(n)` because every token is handled at most twice (push and pop).\n\n" + + "The algorithm has no early-exit path — it always processes every token.", + + realWorldUses: [ + "**Compilers and interpreters:** Most expression compilers first apply the shunting-yard algorithm to convert infix source code into postfix bytecode, then evaluate it with a simple operand stack.", + "**Scientific calculators:** HP RPN calculators and many scientific calculators accept postfix input internally, which is produced from infix keystrokes using this algorithm.", + "**Spreadsheet formula engines:** Applications like Excel parse infix formulas into an internal postfix representation before evaluating them.", + "**Database query optimizers:** SQL expression trees are often built from infix arithmetic and boolean sub-expressions using shunting-yard-style parsing.", + "**Expression templating and rule engines:** Business rule engines that accept infix conditions convert them to postfix for fast, stack-based evaluation at runtime.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(n) time and space — single left-to-right scan with simple stack operations.", + "Handles operator precedence and associativity without recursive parsing.", + "Output is directly consumable by a stack-based evaluator — the two algorithms compose naturally.", + "Easily extended to support additional operators, functions, or custom precedence levels.", + ], + limitations: [ + "Handles only binary operators by default; unary operators (e.g., negation) require extra logic.", + "Assumes well-formed input — mismatched parentheses produce incorrect output without added error handling.", + "Does not evaluate the expression itself; a second pass (RPN evaluation) is required.", + "Right-associative operators (e.g., exponentiation `^`) need a precedence adjustment to pop only strictly higher (not equal) precedence operators.", + ], + }, + + whenToUseIt: + "Use the shunting-yard algorithm whenever you need to parse or evaluate infix arithmetic expressions — in compilers, calculators, rule engines, or any system where users enter standard math notation. " + + "If your expressions are already in postfix form, skip directly to an RPN evaluator. " + + "For very complex grammars (function calls, multiple argument lists, right-associative operators, prefix/postfix unary operators), consider a full recursive-descent parser instead, which handles these cases more naturally.", +}; diff --git a/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/index.ts b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/index.ts new file mode 100644 index 00000000..e17ea499 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { infixToPostfix } from "./sources/infix-to-postfix.ts?fn"; +import { generateInfixToPostfixSteps } from "./step-generator"; +import type { InfixToPostfixInput } from "./step-generator"; +import { infixToPostfixEducational } from "./educational"; + +import typescriptSource from "./sources/infix-to-postfix.ts?raw"; +import pythonSource from "./sources/infix-to-postfix.py?raw"; +import javaSource from "./sources/InfixToPostfix.java?raw"; + +function executeInfixToPostfix(input: InfixToPostfixInput): string { + return infixToPostfix(input.expression) as string; +} + +const infixToPostfixDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.INFIX_TO_POSTFIX!, + name: "Infix to Postfix", + category: CATEGORY.STACKS_QUEUES!, + technique: "expression-evaluation", + description: + "Dijkstra's Shunting-Yard algorithm — convert infix expression to postfix (RPN) using an operator stack with precedence-based popping", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { expression: "a+b*(c-d)" }, + }, + execute: executeInfixToPostfix, + generateSteps: generateInfixToPostfixSteps, + educational: infixToPostfixEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(infixToPostfixDefinition); diff --git a/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/infix-to-postfix.test.ts b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/infix-to-postfix.test.ts new file mode 100644 index 00000000..fa5b6c54 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/infix-to-postfix.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from "vitest"; +import { infixToPostfix } from "./sources/infix-to-postfix.ts?fn"; + +describe("infixToPostfix", () => { + it("converts a+b*(c-d) to a b c d - * +", () => { + expect(infixToPostfix("a+b*(c-d)")).toBe("a b c d - * +"); + }); + + it("converts a+b to a b +", () => { + expect(infixToPostfix("a+b")).toBe("a b +"); + }); + + it("converts (a+b)*c to a b + c *", () => { + expect(infixToPostfix("(a+b)*c")).toBe("a b + c *"); + }); + + it("converts a+b+c to a b + c +", () => { + expect(infixToPostfix("a+b+c")).toBe("a b + c +"); + }); + + it("converts a single operand to itself", () => { + expect(infixToPostfix("a")).toBe("a"); + }); + + it("converts a*b+c to a b * c +", () => { + expect(infixToPostfix("a*b+c")).toBe("a b * c +"); + }); + + it("converts a+b*c to a b c * + (multiplication binds tighter)", () => { + expect(infixToPostfix("a+b*c")).toBe("a b c * +"); + }); + + it("handles nested parentheses: (a+b)*(c+d) to a b + c d + *", () => { + expect(infixToPostfix("(a+b)*(c+d)")).toBe("a b + c d + *"); + }); + + it("handles right-deep nesting: a+(b+(c+d)) to a b c d + + +", () => { + expect(infixToPostfix("a+(b+(c+d))")).toBe("a b c d + + +"); + }); + + it("handles all four operators with correct precedence", () => { + expect(infixToPostfix("a+b*c-d/e")).toBe("a b c * + d e / -"); + }); +}); diff --git a/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/sources/InfixToPostfix.java b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/sources/InfixToPostfix.java new file mode 100644 index 00000000..5fc991fa --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/sources/InfixToPostfix.java @@ -0,0 +1,55 @@ +// Infix to Postfix — Dijkstra's Shunting-Yard: convert infix expression to postfix (RPN) +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class InfixToPostfix { + public static String infixToPostfix(String expression) { + Map operatorPrecedence = Map.of("+", 1, "-", 1, "*", 2, "/", 2); // @step:initialize + List outputQueue = new ArrayList<>(); // @step:initialize + Deque operatorStack = new ArrayDeque<>(); // @step:initialize + + // Tokenize: extract operands, operators, and parentheses + Pattern tokenPattern = Pattern.compile("[A-Za-z0-9]+|[+\\-*/()]"); // @step:initialize + Matcher matcher = tokenPattern.matcher(expression); + List tokens = new ArrayList<>(); + while (matcher.find()) tokens.add(matcher.group()); + + for (String currentToken : tokens) { // @step:visit + if (currentToken.matches("[A-Za-z0-9]+")) { + // Operand — send directly to output + outputQueue.add(currentToken); // @step:output + } else if (operatorPrecedence.containsKey(currentToken)) { + // Operator — pop higher/equal-precedence operators to output first + while ( // @step:compare + !operatorStack.isEmpty() + && !operatorStack.peek().equals("(") + && operatorPrecedence.getOrDefault(operatorStack.peek(), 0) + >= operatorPrecedence.getOrDefault(currentToken, 0) + ) { + outputQueue.add(operatorStack.pop()); // @step:pop + } + operatorStack.push(currentToken); // @step:push + } else if (currentToken.equals("(")) { + operatorStack.push(currentToken); // @step:push + } else if (currentToken.equals(")")) { + // Pop to output until matching '(' is found + while (!operatorStack.isEmpty() && !operatorStack.peek().equals("(")) { + outputQueue.add(operatorStack.pop()); // @step:pop + } + operatorStack.pop(); // @step:pop — discard the '(' + } + } + + // Drain remaining operators to output + while (!operatorStack.isEmpty()) { + outputQueue.add(operatorStack.pop()); // @step:pop + } + + return String.join(" ", outputQueue); // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/sources/infix-to-postfix.py b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/sources/infix-to-postfix.py new file mode 100644 index 00000000..a6e57b34 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/sources/infix-to-postfix.py @@ -0,0 +1,38 @@ +# Infix to Postfix — Dijkstra's Shunting-Yard: convert infix expression to postfix (RPN) +import re + +def infix_to_postfix(expression): + operator_precedence = {"+": 1, "-": 1, "*": 2, "/": 2} # @step:initialize + output_queue = [] # @step:initialize + operator_stack = [] # @step:initialize + + # Tokenize: split on spaces or extract operands/operators/parens + tokens = re.findall(r"[A-Za-z0-9]+|[+\-*/()]", expression) # @step:initialize + + for current_token in tokens: # @step:visit + if re.match(r"^[A-Za-z0-9]+$", current_token): + # Operand — send directly to output + output_queue.append(current_token) # @step:output + elif current_token in operator_precedence: + # Operator — pop higher/equal-precedence operators to output first + while ( # @step:compare + operator_stack + and operator_stack[-1] != "(" + and operator_precedence.get(operator_stack[-1], 0) + >= operator_precedence.get(current_token, 0) + ): + output_queue.append(operator_stack.pop()) # @step:pop + operator_stack.append(current_token) # @step:push + elif current_token == "(": + operator_stack.append(current_token) # @step:push + elif current_token == ")": + # Pop to output until matching '(' is found + while operator_stack and operator_stack[-1] != "(": + output_queue.append(operator_stack.pop()) # @step:pop + operator_stack.pop() # @step:pop — discard the '(' + + # Drain remaining operators to output + while operator_stack: + output_queue.append(operator_stack.pop()) # @step:pop + + return " ".join(output_queue) # @step:complete diff --git a/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/sources/infix-to-postfix.ts b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/sources/infix-to-postfix.ts new file mode 100644 index 00000000..edc971a8 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/sources/infix-to-postfix.ts @@ -0,0 +1,44 @@ +// Infix to Postfix — Dijkstra's Shunting-Yard: convert infix expression to postfix (RPN) +function infixToPostfix(expression: string): string { + const operatorPrecedence: Record = { "+": 1, "-": 1, "*": 2, "/": 2 }; // @step:initialize + const outputQueue: string[] = []; // @step:initialize + const operatorStack: string[] = []; // @step:initialize + + // Tokenize: split on spaces, or split multi-char operands from single-char operators/parens + const tokens = expression.match(/[A-Za-z0-9]+|[+\-*/()]/g) ?? []; // @step:initialize + + for (let tokenIdx = 0; tokenIdx < tokens.length; tokenIdx++) { + const currentToken = tokens[tokenIdx]!; // @step:visit + + if (/^[A-Za-z0-9]+$/.test(currentToken)) { + // Operand — send directly to output + outputQueue.push(currentToken); // @step:output + } else if (currentToken in operatorPrecedence) { + // Operator — pop higher/equal-precedence operators to output first + while ( + operatorStack.length > 0 && + operatorStack[operatorStack.length - 1] !== "(" && + (operatorPrecedence[operatorStack[operatorStack.length - 1]!] ?? 0) >= + (operatorPrecedence[currentToken] ?? 0) // @step:compare + ) { + outputQueue.push(operatorStack.pop()!); // @step:pop + } + operatorStack.push(currentToken); // @step:push + } else if (currentToken === "(") { + operatorStack.push(currentToken); // @step:push + } else if (currentToken === ")") { + // Pop to output until matching '(' is found + while (operatorStack.length > 0 && operatorStack[operatorStack.length - 1] !== "(") { + outputQueue.push(operatorStack.pop()!); // @step:pop + } + operatorStack.pop(); // @step:pop — discard the '(' + } + } + + // Drain remaining operators to output + while (operatorStack.length > 0) { + outputQueue.push(operatorStack.pop()!); // @step:pop + } + + return outputQueue.join(" "); // @step:complete +} diff --git a/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/step-generator.test.ts b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/step-generator.test.ts new file mode 100644 index 00000000..800e6bd5 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/step-generator.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect } from "vitest"; +import { generateInfixToPostfixSteps } from "./step-generator"; + +describe("generateInfixToPostfixSteps", () => { + it("produces steps for the default input", () => { + const steps = generateInfixToPostfixSteps({ expression: "a+b*(c-d)" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateInfixToPostfixSteps({ expression: "a+b*(c-d)" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateInfixToPostfixSteps({ expression: "a+b*(c-d)" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateInfixToPostfixSteps({ expression: "a+b*(c-d)" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateInfixToPostfixSteps({ expression: "a+b*(c-d)" }); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits output steps for each operand in a+b", () => { + const steps = generateInfixToPostfixSteps({ expression: "a+b" }); + const outputSteps = steps.filter((step) => step.type === "output"); + // Both operands and final operator pop produce output steps + expect(outputSteps.length).toBeGreaterThanOrEqual(2); + }); + + it("emits push steps for operators", () => { + const steps = generateInfixToPostfixSteps({ expression: "a+b" }); + const pushSteps = steps.filter((step) => step.type === "push"); + expect(pushSteps.length).toBeGreaterThanOrEqual(1); + }); + + it("the complete step variables contain the postfix result", () => { + const steps = generateInfixToPostfixSteps({ expression: "a+b" }); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.type).toBe("complete"); + expect(completeStep.variables).toHaveProperty("postfix"); + expect(completeStep.variables["postfix"]).toBe("a b +"); + }); + + it("handles a single operand with no operators", () => { + const steps = generateInfixToPostfixSteps({ expression: "a" }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("handles parenthesized expressions", () => { + const steps = generateInfixToPostfixSteps({ expression: "(a+b)*c" }); + expect(steps.length).toBeGreaterThan(0); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.variables["postfix"]).toBe("a b + c *"); + }); + + it("produces correct postfix for a+b+c (left-associativity)", () => { + const steps = generateInfixToPostfixSteps({ expression: "a+b+c" }); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.variables["postfix"]).toBe("a b + c +"); + }); +}); diff --git a/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/step-generator.ts b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/step-generator.ts new file mode 100644 index 00000000..fbecf5a6 --- /dev/null +++ b/src/algorithms/stacks-queues/expression-evaluation/infix-to-postfix/step-generator.ts @@ -0,0 +1,156 @@ +/** Step generator for Infix to Postfix (Shunting-Yard) — produces ExecutionStep[] using ExpressionTracker. */ + +import type { ExecutionStep } from "@/types"; +import { ExpressionTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const ITP_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.INFIX_TO_POSTFIX!); + +export interface InfixToPostfixInput { + expression: string; +} + +const OPERATOR_PRECEDENCE: Record = { "+": 1, "-": 1, "*": 2, "/": 2 }; + +function tokenize(expression: string): string[] { + return expression.match(/[A-Za-z0-9]+|[+\-*/()]/g) ?? []; +} + +export function generateInfixToPostfixSteps(input: InfixToPostfixInput): ExecutionStep[] { + const { expression } = input; + const tokens = tokenize(expression); + const tracker = new ExpressionTracker(tokens, ITP_LINE_MAP); + const operatorStack: string[] = []; + + tracker.initialize({ expression, tokens }); + + for (let tokenIdx = 0; tokenIdx < tokens.length; tokenIdx++) { + const currentToken = tokens[tokenIdx]!; + + tracker.processToken(tokenIdx, { + tokenIdx, + currentToken, + operatorStack: [...operatorStack], + }); + + if (/^[A-Za-z0-9]+$/.test(currentToken)) { + // Operand — goes directly to output + tracker.outputToken(currentToken, { + tokenIdx, + currentToken, + action: "operand to output", + operatorStack: [...operatorStack], + }); + } else if (currentToken in OPERATOR_PRECEDENCE) { + // Pop operators with higher or equal precedence to output + while ( + operatorStack.length > 0 && + operatorStack[operatorStack.length - 1] !== "(" && + (OPERATOR_PRECEDENCE[operatorStack[operatorStack.length - 1]!] ?? 0) >= + (OPERATOR_PRECEDENCE[currentToken] ?? 0) + ) { + const stackTop = operatorStack[operatorStack.length - 1]!; + tracker.popToOutput( + { + tokenIdx, + currentToken, + poppedOperator: stackTop, + reason: "higher or equal precedence", + operatorStack: operatorStack.slice(0, -1), + }, + `Pop '${stackTop}' to output — precedence >= '${currentToken}'`, + ); + operatorStack.pop(); + } + // Push current operator onto the stack + operatorStack.push(currentToken); + tracker.pushOperator(currentToken, tokenIdx, { + tokenIdx, + currentToken, + operatorStack: [...operatorStack], + }); + } else if (currentToken === "(") { + operatorStack.push(currentToken); + tracker.pushOperator(currentToken, tokenIdx, { + tokenIdx, + currentToken, + operatorStack: [...operatorStack], + }); + } else if (currentToken === ")") { + // Pop operators to output until the matching '(' is found + while (operatorStack.length > 0 && operatorStack[operatorStack.length - 1] !== "(") { + const stackTop = operatorStack[operatorStack.length - 1]!; + tracker.popToOutput( + { + tokenIdx, + currentToken, + poppedOperator: stackTop, + reason: "closing paren — drain until '('", + operatorStack: operatorStack.slice(0, -1), + }, + `Pop '${stackTop}' to output — inside parentheses`, + ); + operatorStack.pop(); + } + // Discard the '(' + tracker.popOperator( + { + tokenIdx, + currentToken, + operatorStack: operatorStack.slice(0, -1), + }, + "Discard '(' from stack", + ); + operatorStack.pop(); + } + } + + // Drain remaining operators from the stack to output + while (operatorStack.length > 0) { + const stackTop = operatorStack[operatorStack.length - 1]!; + tracker.popToOutput( + { + poppedOperator: stackTop, + reason: "drain remaining operators", + operatorStack: operatorStack.slice(0, -1), + }, + `Pop '${stackTop}' to output — end of expression`, + ); + operatorStack.pop(); + } + + // Compute the postfix result by replaying the pure shunting-yard logic + const outputQueue: string[] = []; + const replayStack: string[] = []; + for (const token of tokens) { + if (/^[A-Za-z0-9]+$/.test(token)) { + outputQueue.push(token); + } else if (token in OPERATOR_PRECEDENCE) { + while ( + replayStack.length > 0 && + replayStack[replayStack.length - 1] !== "(" && + (OPERATOR_PRECEDENCE[replayStack[replayStack.length - 1]!] ?? 0) >= + (OPERATOR_PRECEDENCE[token] ?? 0) + ) { + outputQueue.push(replayStack.pop()!); + } + replayStack.push(token); + } else if (token === "(") { + replayStack.push(token); + } else if (token === ")") { + while (replayStack.length > 0 && replayStack[replayStack.length - 1] !== "(") { + outputQueue.push(replayStack.pop()!); + } + replayStack.pop(); + } + } + while (replayStack.length > 0) { + outputQueue.push(replayStack.pop()!); + } + const finalPostfix = outputQueue.join(" "); + + tracker.complete(finalPostfix, { postfix: finalPostfix }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/OnlineStockSpanPipeline.stories.tsx b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/OnlineStockSpanPipeline.stories.tsx new file mode 100644 index 00000000..0f4996f3 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/OnlineStockSpanPipeline.stories.tsx @@ -0,0 +1,56 @@ +/** + * Storybook stories for the Online Stock Span algorithm pipeline. + * Uses the real step generator with [100, 80, 60, 70, 60, 75, 85], rendering the + * StackQueueVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateOnlineStockSpanSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateOnlineStockSpanSteps({ prices: [100, 80, 60, 70, 60, 75, 85] }); +const increasingSteps = generateOnlineStockSpanSteps({ prices: [10, 20, 30, 40, 50] }); + +const meta: Meta = { + title: "Algorithm Pipelines/Online Stock Span", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — full price array unprocessed, empty stack */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing with some prices on the monotonic stack */ +export const MidProcessing: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Final state — all prices processed, spans resolved */ +export const AllSpansResolved: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Increasing prices — every new price pops all previous entries, spans grow to full length */ +export const IncreasingPrices: Story = { + args: { + visualState: increasingSteps[increasingSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/educational.ts b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/educational.ts new file mode 100644 index 00000000..d16ded9c --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/educational.ts @@ -0,0 +1,61 @@ +import type { EducationalContent } from "@/types"; + +export const onlineStockSpanEducational: EducationalContent = { + overview: + "**Online Stock Span** (LeetCode 901) computes, for each day's stock price, the number of consecutive days up to and including today where the price was less than or equal to today's price.\n\nA **monotonic decreasing stack** of `(price, span)` pairs makes this efficient: whenever a new price arrives, pop all stack entries with a smaller or equal price, accumulate their spans, then push the new pair. Each element is pushed and popped at most once, giving amortized O(1) per query.", + + howItWorks: + "The algorithm maintains a stack of `(price, span)` pairs ordered so that prices are strictly decreasing from bottom to top:\n\n" + + "1. **For each price** → set `spanCount = 1`.\n" + + "2. **While** the stack is non-empty and the top price is ≤ today's price:\n" + + " - Add the top span to `spanCount`.\n" + + " - Pop the top entry.\n" + + "3. **Push** `(todayPrice, spanCount)` onto the stack.\n" + + "4. Record `spanCount` as today's result.\n\n" + + "### Example trace on `[100, 80, 60, 70, 60, 75, 85]`\n\n" + + "```\n" + + "day price stack (price, span) span\n" + + " 0 100 [(100,1)] 1\n" + + " 1 80 [(100,1),(80,1)] 1\n" + + " 2 60 [(100,1),(80,1),(60,1)] 1\n" + + " 3 70 [(100,1),(80,1),(70,2)] 2 pop 60\n" + + " 4 60 [(100,1),(80,1),(70,2),(60,1)] 1\n" + + " 5 75 [(100,1),(80,1),(75,4)] 4 pop 60,70\n" + + " 6 85 [(100,1),(85,6)] 6 pop 80,75\n" + + "result: [1,1,1,2,1,4,6]\n" + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)` amortized**\n\n" + + "Each price is pushed onto the stack exactly once and popped at most once. Even though the inner `while` loop can run multiple times, the total number of push + pop operations across all `n` days is bounded by `2n`, so the amortized cost per day is `O(1)`.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "In the worst case (prices are strictly decreasing, e.g. `[100, 90, 80, ...]`) no element is ever popped, so the stack grows to size `n`.", + + bestAndWorstCase: + "**Best case** — strictly increasing prices (e.g. `[10, 20, 30, 40]`): every previous element is popped on each step, the stack stays at size 1, and each query does the most work — but still `O(n)` total.\n\n" + + "**Worst case** — strictly decreasing prices (e.g. `[100, 90, 80, 70]`): no elements are ever popped, the stack grows to size `n`, and every query is `O(1)`. Space usage is maximized.\n\n" + + "Both cases are `O(n)` time overall due to the amortized argument.", + + realWorldUses: [ + "**Stock market dashboards:** Displaying the streak of days a stock has stayed below or at its current price — a common signal in technical analysis.", + "**Streaming data analytics:** Computing rolling windows or streaks over continuous data feeds where you cannot look back arbitrarily far.", + "**Game leaderboards:** Counting how long a player has maintained a score at or above a given threshold.", + "**Sensor monitoring:** Detecting consecutive readings within an acceptable range for anomaly detection.", + ], + + strengthsAndLimitations: { + strengths: [ + "Amortized O(1) per query — handles real-time price streams with minimal latency.", + "Single-pass algorithm with no lookahead — well-suited for online (streaming) scenarios.", + "Stack naturally encodes all the information needed to answer future queries without re-scanning history.", + ], + limitations: [ + "O(n) auxiliary space in the worst case (strictly decreasing input).", + "Only answers the 'span looking backwards' query — not suitable for forward-looking span questions without modification.", + "Requires storing `(price, span)` pairs; a naïve counter approach would be O(n²) per query.", + ], + }, + + whenToUseIt: + "Use the monotonic stack approach whenever you need to count consecutive preceding elements that satisfy a monotonic condition (≤, ≥, <, >) relative to the current element. It generalises to problems like Daily Temperatures (next greater element), Largest Rectangle in Histogram, and Trapping Rain Water. If you only need a simple running maximum or minimum, a plain variable suffices — the stack shines when you need full span or range information.", +}; diff --git a/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/index.ts b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/index.ts new file mode 100644 index 00000000..2e29ffe7 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { onlineStockSpan } from "./sources/online-stock-span.ts?fn"; +import { generateOnlineStockSpanSteps } from "./step-generator"; +import type { OnlineStockSpanInput } from "./step-generator"; +import { onlineStockSpanEducational } from "./educational"; + +import typescriptSource from "./sources/online-stock-span.ts?raw"; +import pythonSource from "./sources/online-stock-span.py?raw"; +import javaSource from "./sources/OnlineStockSpan.java?raw"; + +function executeOnlineStockSpan(input: OnlineStockSpanInput): number[] { + return onlineStockSpan(input.prices) as number[]; +} + +const onlineStockSpanDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.ONLINE_STOCK_SPAN!, + name: "Online Stock Span", + category: CATEGORY.STACKS_QUEUES!, + technique: "monotonic-stack", + description: + "For each day's stock price, count consecutive preceding days (including today) where the price was ≤ today's price using a monotonic decreasing stack of (price, span) pairs", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { prices: [100, 80, 60, 70, 60, 75, 85] }, + }, + execute: executeOnlineStockSpan, + generateSteps: generateOnlineStockSpanSteps, + educational: onlineStockSpanEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(onlineStockSpanDefinition); diff --git a/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/online-stock-span.test.ts b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/online-stock-span.test.ts new file mode 100644 index 00000000..834abdd0 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/online-stock-span.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from "vitest"; +import { onlineStockSpan } from "./sources/online-stock-span.ts?fn"; + +describe("onlineStockSpan", () => { + it("produces the correct spans for the default example", () => { + expect(onlineStockSpan([100, 80, 60, 70, 60, 75, 85])).toEqual([1, 1, 1, 2, 1, 4, 6]); + }); + + it("returns span of 1 for a single price", () => { + expect(onlineStockSpan([50])).toEqual([1]); + }); + + it("returns all 1s for strictly decreasing prices", () => { + expect(onlineStockSpan([100, 90, 80, 70])).toEqual([1, 1, 1, 1]); + }); + + it("returns incrementing spans for strictly increasing prices", () => { + expect(onlineStockSpan([10, 20, 30, 40])).toEqual([1, 2, 3, 4]); + }); + + it("returns all equal to length for all equal prices", () => { + expect(onlineStockSpan([50, 50, 50, 50])).toEqual([1, 2, 3, 4]); + }); + + it("handles a price that spans back over multiple drops and rises", () => { + // [3, 1, 2] → day 0: span=1, day 1: span=1, day 2: price 2 >= 1 so span=2 + expect(onlineStockSpan([3, 1, 2])).toEqual([1, 1, 2]); + }); + + it("handles two prices where second is greater than first", () => { + expect(onlineStockSpan([5, 10])).toEqual([1, 2]); + }); + + it("handles two prices where second is less than first", () => { + expect(onlineStockSpan([10, 5])).toEqual([1, 1]); + }); + + it("handles two equal prices", () => { + expect(onlineStockSpan([7, 7])).toEqual([1, 2]); + }); + + it("computes correct spans for a zigzag pattern", () => { + // [1, 3, 1, 3, 1] → spans: [1, 2, 1, 4, 1] + expect(onlineStockSpan([1, 3, 1, 3, 1])).toEqual([1, 2, 1, 4, 1]); + }); +}); diff --git a/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/sources/OnlineStockSpan.java b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/sources/OnlineStockSpan.java new file mode 100644 index 00000000..e15c9699 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/sources/OnlineStockSpan.java @@ -0,0 +1,27 @@ +// Online Stock Span — for each day's price, count consecutive days (including today) where price <= today's price +import java.util.ArrayDeque; +import java.util.Deque; + +public class OnlineStockSpan { + public static int[] onlineStockSpan(int[] prices) { + int[] result = new int[prices.length]; // @step:initialize + // Stack holds int[]{price, span} pairs in monotonic decreasing order by price + Deque stack = new ArrayDeque<>(); // @step:initialize + + for (int priceIdx = 0; priceIdx < prices.length; priceIdx++) { + int currentPrice = prices[priceIdx]; // @step:visit + int spanCount = 1; // @step:visit + + // Pop all stack entries with price <= currentPrice, accumulating their spans + while (!stack.isEmpty() && stack.peek()[0] <= currentPrice) { // @step:compare + spanCount += stack.peek()[1]; // @step:maintain-monotonic + stack.pop(); // @step:maintain-monotonic + } + + stack.push(new int[]{currentPrice, spanCount}); // @step:push + result[priceIdx] = spanCount; // @step:resolve + } + + return result; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/sources/online-stock-span.py b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/sources/online-stock-span.py new file mode 100644 index 00000000..8ac12da6 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/sources/online-stock-span.py @@ -0,0 +1,18 @@ +# Online Stock Span — for each day's price, count consecutive days (including today) where price <= today's price +def online_stock_span(prices): + result = [0] * len(prices) # @step:initialize + # Stack holds (price, span) tuples in monotonic decreasing order by price + stack = [] # @step:initialize + + for price_idx, current_price in enumerate(prices): # @step:visit + span_count = 1 # @step:visit + + # Pop all stack entries with price <= current_price, accumulating their spans + while stack and stack[-1][0] <= current_price: # @step:compare + span_count += stack[-1][1] # @step:maintain-monotonic + stack.pop() # @step:maintain-monotonic + + stack.append((current_price, span_count)) # @step:push + result[price_idx] = span_count # @step:resolve + + return result # @step:complete diff --git a/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/sources/online-stock-span.ts b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/sources/online-stock-span.ts new file mode 100644 index 00000000..6d09c6bf --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/sources/online-stock-span.ts @@ -0,0 +1,23 @@ +// Online Stock Span — for each day's price, count consecutive days (including today) where price <= today's price +function onlineStockSpan(prices: number[]): number[] { + const result: number[] = new Array(prices.length).fill(0); // @step:initialize + // Stack holds [price, span] pairs in monotonic decreasing order by price + const stack: [number, number][] = []; // @step:initialize + + for (let priceIdx = 0; priceIdx < prices.length; priceIdx++) { + const currentPrice = prices[priceIdx]!; // @step:visit + let spanCount = 1; // @step:visit + + // Pop all stack entries with price <= currentPrice, accumulating their spans + while (stack.length > 0 && stack[stack.length - 1]![0] <= currentPrice) { + // @step:compare + spanCount += stack[stack.length - 1]![1]; // @step:maintain-monotonic + stack.pop(); // @step:maintain-monotonic + } + + stack.push([currentPrice, spanCount]); // @step:push + result[priceIdx] = spanCount; // @step:resolve + } + + return result; // @step:complete +} diff --git a/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/step-generator.test.ts b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/step-generator.test.ts new file mode 100644 index 00000000..ee58ffe9 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/step-generator.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect } from "vitest"; +import { generateOnlineStockSpanSteps } from "./step-generator"; + +describe("generateOnlineStockSpanSteps", () => { + it("produces steps for the default input", () => { + const steps = generateOnlineStockSpanSteps({ prices: [100, 80, 60, 70, 60, 75, 85] }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateOnlineStockSpanSteps({ prices: [100, 80, 60, 70, 60, 75, 85] }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateOnlineStockSpanSteps({ prices: [100, 80, 60, 70, 60, 75, 85] }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateOnlineStockSpanSteps({ prices: [100, 80, 60, 70, 60, 75, 85] }); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateOnlineStockSpanSteps({ prices: [100, 80, 60, 70, 60, 75, 85] }); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits a visit step for each price", () => { + const prices = [100, 80, 60, 70, 60, 75, 85]; + const steps = generateOnlineStockSpanSteps({ prices }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(prices.length); + }); + + it("emits a push step for each price", () => { + const prices = [100, 80, 60, 70, 60, 75, 85]; + const steps = generateOnlineStockSpanSteps({ prices }); + const pushSteps = steps.filter((step) => step.type === "push"); + expect(pushSteps.length).toBe(prices.length); + }); + + it("emits maintain-monotonic steps when prices are popped", () => { + // [10, 5, 8] — price 8 pops price 5 + const steps = generateOnlineStockSpanSteps({ prices: [10, 5, 8] }); + const monotonicSteps = steps.filter((step) => step.type === "maintain-monotonic"); + expect(monotonicSteps.length).toBeGreaterThan(0); + }); + + it("emits no maintain-monotonic steps for strictly decreasing input", () => { + const steps = generateOnlineStockSpanSteps({ prices: [100, 90, 80, 70] }); + const monotonicSteps = steps.filter((step) => step.type === "maintain-monotonic"); + expect(monotonicSteps.length).toBe(0); + }); + + it("handles a single price", () => { + const steps = generateOnlineStockSpanSteps({ prices: [42] }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("variables contain priceIdx and currentPrice on visit steps", () => { + const steps = generateOnlineStockSpanSteps({ prices: [100, 80] }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps[0]?.variables).toMatchObject({ priceIdx: 0, currentPrice: 100 }); + expect(visitSteps[1]?.variables).toMatchObject({ priceIdx: 1, currentPrice: 80 }); + }); +}); diff --git a/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/step-generator.ts b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/step-generator.ts new file mode 100644 index 00000000..695d906c --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/online-stock-span/step-generator.ts @@ -0,0 +1,63 @@ +/** Step generator for Online Stock Span — produces ExecutionStep[] using NumericStackTracker. */ + +import type { ExecutionStep } from "@/types"; +import { NumericStackTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const OSS_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.ONLINE_STOCK_SPAN!); + +export interface OnlineStockSpanInput { + prices: number[]; +} + +export function generateOnlineStockSpanSteps(input: OnlineStockSpanInput): ExecutionStep[] { + const { prices } = input; + const tracker = new NumericStackTracker(prices, OSS_LINE_MAP); + tracker.setMonotonicOrder("decreasing"); + + // Logical stack drives pop decisions; tracker's internal stack drives visual animation. + // Each iteration: maintainMonotonic pops (synced), pushIndex pushes (synced), + // then popAndResolve immediately resolves the current span into the result array. + const logicalStack: [number, number][] = []; + + tracker.initialize({ prices }); + + for (let priceIdx = 0; priceIdx < prices.length; priceIdx++) { + const currentPrice = prices[priceIdx]!; + let spanCount = 1; + + tracker.processElement(priceIdx, { priceIdx, currentPrice, spanCount }); + + // Maintain monotonic decreasing order — pop entries with price <= currentPrice + while (logicalStack.length > 0 && logicalStack[logicalStack.length - 1]![0] <= currentPrice) { + const [stackTopPrice, stackTopSpan] = logicalStack[logicalStack.length - 1]!; + spanCount += stackTopSpan; + logicalStack.pop(); + + tracker.maintainMonotonic( + { priceIdx, currentPrice, spanCount, stackTopPrice, stackTopSpan }, + `Pop price ${stackTopPrice} (span ${stackTopSpan}) — it is ≤ ${currentPrice}, accumulate span`, + ); + } + + // Push current price and accumulated span onto logical and tracker stacks + logicalStack.push([currentPrice, spanCount]); + tracker.pushIndex( + priceIdx, + { priceIdx, currentPrice, spanCount }, + `Push price ${currentPrice} with accumulated span ${spanCount}`, + ); + + // Resolve: record the computed span in the result array and show the resolution step + tracker.popAndResolve( + spanCount, + { priceIdx, currentPrice, spanCount, resolvedIndex: priceIdx }, + `Span for day ${priceIdx} (price ${currentPrice}) = ${spanCount}`, + ); + } + + tracker.complete({ resultLength: prices.length }, "All prices processed — spans resolved"); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/RemoveKDigitsPipeline.stories.tsx b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/RemoveKDigitsPipeline.stories.tsx new file mode 100644 index 00000000..c9b3e0c9 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/RemoveKDigitsPipeline.stories.tsx @@ -0,0 +1,56 @@ +/** + * Storybook stories for the Remove K Digits algorithm pipeline. + * Uses the real step generator with "1432219" (k=3), rendering the + * StackQueueVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateRemoveKDigitsSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateRemoveKDigitsSteps({ num: "1432219", removalCount: 3 }); +const allRemovedSteps = generateRemoveKDigitsSteps({ num: "10", removalCount: 2 }); + +const meta: Meta = { + title: "Algorithm Pipelines/Remove K Digits", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — full digit string unprocessed, empty stack */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing with some digits on the stack */ +export const MidProcessing: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Completed — result "1219" after removing 3 digits */ +export const CompletedSmallestNumber: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** All digits removed — result is "0" */ +export const AllDigitsRemoved: Story = { + args: { + visualState: allRemovedSteps[allRemovedSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/educational.ts b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/educational.ts new file mode 100644 index 00000000..d4637103 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/educational.ts @@ -0,0 +1,63 @@ +import type { EducationalContent } from "@/types"; + +export const removeKDigitsEducational: EducationalContent = { + overview: + "**Remove K Digits** (LeetCode 402) finds the smallest possible number by removing exactly `k` digits from a numeric string. The key insight is that a number is minimized when its digits are in non-decreasing order from left to right — which is exactly what a monotonic increasing stack enforces.\n\nA greedy approach processes each digit left to right: whenever the current digit is smaller than the top of the stack and removals remain, popping the stack top produces a smaller number. This is optimal because earlier (higher-position) digits contribute more to the number's magnitude.", + + howItWorks: + "The algorithm uses a monotonic increasing stack to build the smallest result:\n\n" + + "1. **Scan each digit** left to right.\n" + + "2. **Maintain monotonic order** — while the stack top is greater than the current digit and removals remain:\n" + + " - Pop the stack top (this removes a larger digit in a higher position).\n" + + " - Decrement the removal counter.\n" + + "3. **Push** the current digit onto the stack.\n" + + "4. **Trim the tail** — if removals still remain after scanning all digits, pop from the end of the stack (removing the largest remaining digits).\n" + + '5. **Strip leading zeros** — join the stack and remove any leading `0` characters; return `"0"` if the result is empty.\n\n' + + '### Example trace on `num = "1432219"`, `k = 3`\n\n' + + "```\n" + + "digit action stack removals left\n" + + "1 push [1] 3\n" + + "4 push [1,4] 3\n" + + "3 pop 4 (4>3), push [1,3] 2\n" + + "2 pop 3 (3>2), push [1,2] 1\n" + + "2 push (2==2) [1,2,2] 1\n" + + "1 pop 2 (2>1), push [1,2,1] 0 — wait, 2>1 so pop\n" + + " actually pop 2, push [1,2,1] 0\n" + + "9 push (no removals) [1,2,1,9] 0\n" + + 'result = "1219"\n' + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each digit is pushed onto the stack exactly once and popped at most once, so the total number of push and pop operations is bounded by `2n`. The final strip and join are also `O(n)`, giving an overall linear time complexity.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The stack holds at most `n` digits in the worst case (e.g. when the input is already sorted in non-decreasing order and no digits need to be popped during the scan).", + + bestAndWorstCase: + '**Best case** — `k = 0`: no removals, the algorithm simply pushes every digit and returns the original number in `O(n)`. Or `k >= n`: all digits are removed, returning `"0"` immediately.\n\n' + + '**Worst case** — a strictly decreasing sequence like `"9876543210"` with `k = 5`: every new digit triggers a pop of the previous one, resulting in `O(n)` pops and pushes, but still linear overall.\n\n' + + "The greedy choice is always globally optimal: removing a larger digit at a higher position strictly reduces the number, and no rearrangement can do better.", + + realWorldUses: [ + "**Number formatting systems:** Producing the shortest or smallest canonical form of a numeric identifier after optional character removal.", + "**Lexicographic minimization:** Any scenario where you need to delete characters from a sequence to produce the lexicographically smallest result — the same greedy stack pattern applies to strings.", + "**Competitive programming:** A foundational monotonic stack pattern that underpins problems like largest rectangle in histogram, stock span, and next greater element.", + "**Data compression preprocessing:** Reducing numeric tokens to their minimal representations before encoding.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(n) time — each digit is processed at most twice (one push, one pop).", + "Greedy correctness guarantee — the monotonic stack always produces the globally optimal result.", + "Handles edge cases cleanly: leading zeros, k ≥ n, and already-sorted inputs.", + ], + limitations: [ + "Only works for digit strings — the ordering assumption breaks for arbitrary character alphabets without modification.", + "O(n) extra space for the stack; an in-place variant would be more complex.", + "Produces a string result, not a numeric type, so the caller must handle conversion if an integer is needed.", + ], + }, + + whenToUseIt: + "Use this pattern whenever you must delete exactly `k` elements from a sequence to minimize (or maximize) its value. The greedy monotonic stack is optimal for single-pass digit/character removal. If the number of removals is unlimited, simply sort. If you need to remove characters to form a subsequence matching a target, consider dynamic programming instead.", +}; diff --git a/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/index.ts b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/index.ts new file mode 100644 index 00000000..de2a19fb --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { removeKDigits } from "./sources/remove-k-digits.ts?fn"; +import { generateRemoveKDigitsSteps } from "./step-generator"; +import type { RemoveKDigitsInput } from "./step-generator"; +import { removeKDigitsEducational } from "./educational"; + +import typescriptSource from "./sources/remove-k-digits.ts?raw"; +import pythonSource from "./sources/remove-k-digits.py?raw"; +import javaSource from "./sources/RemoveKDigits.java?raw"; + +function executeRemoveKDigits(input: RemoveKDigitsInput): string { + return removeKDigits(input.num, input.removalCount) as string; +} + +const removeKDigitsDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.REMOVE_K_DIGITS!, + name: "Remove K Digits", + category: CATEGORY.STACKS_QUEUES!, + technique: "monotonic-stack", + description: + "Remove k digits from a number string to produce the smallest possible number using a greedy monotonic increasing stack", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { num: "1432219", removalCount: 3 }, + }, + execute: executeRemoveKDigits, + generateSteps: generateRemoveKDigitsSteps, + educational: removeKDigitsEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(removeKDigitsDefinition); diff --git a/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/remove-k-digits.test.ts b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/remove-k-digits.test.ts new file mode 100644 index 00000000..4ed3eab6 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/remove-k-digits.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from "vitest"; +import { removeKDigits } from "./sources/remove-k-digits.ts?fn"; + +describe("removeKDigits", () => { + it('removes 3 digits from "1432219" to produce "1219"', () => { + expect(removeKDigits("1432219", 3)).toBe("1219"); + }); + + it('removes 1 digit from "10200" to produce "200"', () => { + expect(removeKDigits("10200", 1)).toBe("200"); + }); + + it('removes all digits from "10" to produce "0"', () => { + expect(removeKDigits("10", 2)).toBe("0"); + }); + + it("returns the original number when no removals are requested", () => { + expect(removeKDigits("12345", 0)).toBe("12345"); + }); + + it('strips leading zeros after removal — "100" with k=1 gives "0"', () => { + expect(removeKDigits("100", 1)).toBe("0"); + }); + + it('handles a single-digit string with k=1 producing "0"', () => { + expect(removeKDigits("9", 1)).toBe("0"); + }); + + it('removes digits from a non-decreasing sequence by trimming from the end — "12345" with k=3', () => { + expect(removeKDigits("12345", 3)).toBe("12"); + }); + + it('handles a repeated-digit string — "1111111" with k=3', () => { + expect(removeKDigits("1111111", 3)).toBe("1111"); + }); + + it('removes digits from a decreasing sequence — "9876" with k=2 gives "76"', () => { + expect(removeKDigits("9876", 2)).toBe("76"); + }); + + it('handles k equal to string length, returning "0"', () => { + expect(removeKDigits("12345", 5)).toBe("0"); + }); +}); diff --git a/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/sources/RemoveKDigits.java b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/sources/RemoveKDigits.java new file mode 100644 index 00000000..589f23e2 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/sources/RemoveKDigits.java @@ -0,0 +1,32 @@ +// Remove K Digits — greedy monotonic stack to produce the smallest number after k removals +import java.util.ArrayDeque; +import java.util.Deque; + +public class RemoveKDigits { + public static String removeKDigits(String num, int removalCount) { + Deque digitStack = new ArrayDeque<>(); // @step:initialize + int removalsLeft = removalCount; // @step:initialize + + for (int digitIdx = 0; digitIdx < num.length(); digitIdx++) { + char currentDigit = num.charAt(digitIdx); // @step:visit + // While we still have removals and the stack top is greater than the current digit, pop it + while (removalsLeft > 0 && !digitStack.isEmpty() && digitStack.peek() > currentDigit) { // @step:compare + digitStack.pop(); // @step:pop + removalsLeft--; // @step:maintain-monotonic + } + digitStack.push(currentDigit); // @step:push + } + + // Remove remaining digits from the end if we still have removals left + while (removalsLeft > 0) { + digitStack.pop(); // @step:pop + removalsLeft--; // @step:complete + } + + // Build result string, strip leading zeros, default to "0" + StringBuilder sb = new StringBuilder(); + for (char ch : digitStack) sb.append(ch); // @step:complete + String result = sb.reverse().toString().replaceAll("^0+", ""); // @step:complete + return result.isEmpty() ? "0" : result; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/sources/remove-k-digits.py b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/sources/remove-k-digits.py new file mode 100644 index 00000000..326cf5ad --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/sources/remove-k-digits.py @@ -0,0 +1,20 @@ +# Remove K Digits — greedy monotonic stack to produce the smallest number after k removals +def remove_k_digits(num: str, removal_count: int) -> str: + digit_stack = [] # @step:initialize + removals_left = removal_count # @step:initialize + + for digit_idx, current_digit in enumerate(num): # @step:visit + # While we still have removals and the stack top is greater than the current digit, pop it + while removals_left > 0 and digit_stack and digit_stack[-1] > current_digit: # @step:compare + digit_stack.pop() # @step:pop + removals_left -= 1 # @step:maintain-monotonic + digit_stack.append(current_digit) # @step:push + + # Remove remaining digits from the end if we still have removals left + while removals_left > 0: # @step:pop + digit_stack.pop() # @step:pop + removals_left -= 1 # @step:complete + + # Strip leading zeros and return; default to "0" for an empty result + result = "".join(digit_stack).lstrip("0") or "0" # @step:complete + return result # @step:complete diff --git a/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/sources/remove-k-digits.ts b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/sources/remove-k-digits.ts new file mode 100644 index 00000000..e435fd3c --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/sources/remove-k-digits.ts @@ -0,0 +1,30 @@ +// Remove K Digits — greedy monotonic stack to produce the smallest number after k removals +function removeKDigits(num: string, removalCount: number): string { + const digitStack: string[] = []; // @step:initialize + let removalsLeft = removalCount; // @step:initialize + + for (let digitIdx = 0; digitIdx < num.length; digitIdx++) { + const currentDigit = num[digitIdx]!; // @step:visit + // While we still have removals and the stack top is greater than the current digit, pop it + while ( + removalsLeft > 0 && + digitStack.length > 0 && + digitStack[digitStack.length - 1]! > currentDigit + ) { + // @step:compare + digitStack.pop(); // @step:pop + removalsLeft--; // @step:maintain-monotonic + } + digitStack.push(currentDigit); // @step:push + } + + // Remove remaining digits from the end if we still have removals left + while (removalsLeft > 0) { + digitStack.pop(); // @step:pop + removalsLeft--; // @step:complete + } + + // Strip leading zeros and return; default to "0" for an empty result + const result = digitStack.join("").replace(/^0+/, "") || "0"; // @step:complete + return result; // @step:complete +} diff --git a/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/step-generator.test.ts b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/step-generator.test.ts new file mode 100644 index 00000000..031af133 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/step-generator.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect } from "vitest"; +import { generateRemoveKDigitsSteps } from "./step-generator"; + +describe("generateRemoveKDigitsSteps", () => { + it("produces steps for the default input", () => { + const steps = generateRemoveKDigitsSteps({ num: "1432219", removalCount: 3 }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateRemoveKDigitsSteps({ num: "1432219", removalCount: 3 }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateRemoveKDigitsSteps({ num: "1432219", removalCount: 3 }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateRemoveKDigitsSteps({ num: "1432219", removalCount: 3 }); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateRemoveKDigitsSteps({ num: "1432219", removalCount: 3 }); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits a visit step for each digit in the input", () => { + const inputNum = "1432219"; + const steps = generateRemoveKDigitsSteps({ num: inputNum, removalCount: 3 }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(inputNum.length); + }); + + it("emits push steps for digits added to the stack", () => { + const steps = generateRemoveKDigitsSteps({ num: "1432219", removalCount: 3 }); + const pushSteps = steps.filter((step) => step.type === "push"); + expect(pushSteps.length).toBeGreaterThan(0); + }); + + it("emits match steps for each pop triggered by a smaller incoming digit", () => { + // "43" with k=1: digit '3' triggers pop of '4' + const steps = generateRemoveKDigitsSteps({ num: "43", removalCount: 1 }); + const matchSteps = steps.filter((step) => step.type === "match"); + expect(matchSteps.length).toBe(1); + }); + + it('final complete step variables contain the result string "1219"', () => { + const steps = generateRemoveKDigitsSteps({ num: "1432219", removalCount: 3 }); + const lastStep = steps[steps.length - 1]!; + expect((lastStep.variables as Record)["result"]).toBe("1219"); + }); + + it('handles "10200" with k=1 — result is "200"', () => { + const steps = generateRemoveKDigitsSteps({ num: "10200", removalCount: 1 }); + const lastStep = steps[steps.length - 1]!; + expect((lastStep.variables as Record)["result"]).toBe("200"); + }); + + it('handles "10" with k=2 — result is "0"', () => { + const steps = generateRemoveKDigitsSteps({ num: "10", removalCount: 2 }); + const lastStep = steps[steps.length - 1]!; + expect((lastStep.variables as Record)["result"]).toBe("0"); + }); + + it("handles an empty num string gracefully", () => { + const steps = generateRemoveKDigitsSteps({ num: "", removalCount: 0 }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); +}); diff --git a/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/step-generator.ts b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/step-generator.ts new file mode 100644 index 00000000..a4e948d6 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/remove-k-digits/step-generator.ts @@ -0,0 +1,74 @@ +/** Step generator for Remove K Digits — produces ExecutionStep[] using StackQueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { StackQueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const RKD_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.REMOVE_K_DIGITS!); + +export interface RemoveKDigitsInput { + num: string; + removalCount: number; +} + +export function generateRemoveKDigitsSteps(input: RemoveKDigitsInput): ExecutionStep[] { + const { num, removalCount } = input; + const tracker = new StackQueueTracker(num, RKD_LINE_MAP); + + const digitStack: string[] = []; + let removalsLeft = removalCount; + + tracker.initialize({ num, removalCount, removalsLeft }); + + for (let digitIdx = 0; digitIdx < num.length; digitIdx++) { + const currentDigit = num[digitIdx]!; + + tracker.processChar(digitIdx, { digitIdx, currentDigit, removalsLeft }); + + // Pop larger digits from stack while removals remain — maintain monotonic increasing order + while ( + removalsLeft > 0 && + digitStack.length > 0 && + digitStack[digitStack.length - 1]! > currentDigit + ) { + const removedDigit = digitStack[digitStack.length - 1]!; + digitStack.pop(); + removalsLeft--; + + tracker.popMatched(removedDigit, digitIdx, { + digitIdx, + currentDigit, + removedDigit, + removalsLeft, + reason: `Stack top '${removedDigit}' > '${currentDigit}' — remove it to keep number small`, + }); + } + + digitStack.push(currentDigit); + tracker.push(currentDigit, digitIdx, { + digitIdx, + currentDigit, + removalsLeft, + stackSize: digitStack.length, + }); + } + + // Remove trailing digits from the end if removals remain + while (removalsLeft > 0) { + const removedDigit = digitStack[digitStack.length - 1]!; + digitStack.pop(); + removalsLeft--; + + tracker.popMatched(removedDigit, num.length - 1, { + removedDigit, + removalsLeft, + reason: `${removalsLeft + 1} removal(s) remain — trim '${removedDigit}' from the end`, + }); + } + + const result = digitStack.join("").replace(/^0+/, "") || "0"; + tracker.complete(true, { result, finalStack: [...digitStack] }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/SumOfSubarrayMinimumsPipeline.stories.tsx b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/SumOfSubarrayMinimumsPipeline.stories.tsx new file mode 100644 index 00000000..9827f355 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/SumOfSubarrayMinimumsPipeline.stories.tsx @@ -0,0 +1,64 @@ +/** + * Storybook stories for the Sum of Subarray Minimums algorithm pipeline. + * Uses the real step generator with [3, 1, 2, 4], rendering the + * StackQueueVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateSumOfSubarrayMinimumsSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateSumOfSubarrayMinimumsSteps({ arr: [3, 1, 2, 4] }); +const duplicateSteps = generateSumOfSubarrayMinimumsSteps({ arr: [2, 2, 2] }); + +const meta: Meta = { + title: "Algorithm Pipelines/Sum of Subarray Minimums", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — full input array unprocessed, empty stack */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid left-boundary pass — some elements processed, stack building up */ +export const LeftBoundaryPass: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 4)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Mid right-boundary pass — second pass traversing right to left */ +export const RightBoundaryPass: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Completed — all contributions summed, result = 17 */ +export const Complete: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Duplicate elements — array [2, 2, 2] demonstrating asymmetric pop conditions */ +export const DuplicateElements: Story = { + args: { + visualState: duplicateSteps[duplicateSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/educational.ts b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/educational.ts new file mode 100644 index 00000000..ffd66335 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/educational.ts @@ -0,0 +1,64 @@ +import type { EducationalContent } from "@/types"; + +export const sumOfSubarrayMinimumsEducational: EducationalContent = { + overview: + "**Sum of Subarray Minimums** (LeetCode 907) computes the sum of the minimum value of every contiguous subarray of a given array, modulo 10⁹ + 7.\n\nRather than enumerating all O(n²) subarrays, the algorithm finds each element's **contribution**: how many subarrays have it as the minimum. Two monotonic stack passes determine the left and right boundaries — the count of subarrays where an element is the minimum is `leftDistance × rightDistance`.", + + howItWorks: + "The key insight is that for each element `arr[i]`, we need to count subarrays where `arr[i]` is the minimum. This equals `left[i] × right[i]` where:\n\n" + + "- `left[i]` = distance to the previous element **strictly less** than `arr[i]` (or the left edge)\n" + + "- `right[i]` = distance to the next element **less than or equal** to `arr[i]` (or the right edge)\n\n" + + "The asymmetry (strict left, non-strict right) handles duplicates correctly — each duplicate pair is counted exactly once.\n\n" + + "**Pass 1 — Left boundaries** (left to right, monotonic increasing stack):\n" + + "- Pop while stack top ≥ current value\n" + + "- `left[i]` = `i + 1` if stack empty, else `i − stack.top`\n\n" + + "**Pass 2 — Right boundaries** (right to left, monotonic increasing stack):\n" + + "- Pop while stack top > current value\n" + + "- `right[i]` = `n − i` if stack empty, else `stack.top − i`\n\n" + + "**Pass 3 — Sum contributions**:\n" + + "- `result += arr[i] × left[i] × right[i]`, taken modulo 10⁹ + 7\n\n" + + "### Example trace on `[3, 1, 2, 4]`\n\n" + + "```\n" + + "idx val left right contribution\n" + + " 0 3 1 2 3×1×2 = 6\n" + + " 1 1 2 4 1×2×4 = 8\n" + + " 2 2 1 2 2×1×2 = 4 (wait — right[2]=2: next ≤2 is none, so n−2=2)\n" + + " 3 4 1 1 4×1×1 = 4\n" + + " total = 17 ✓\n" + + "```\n\n" + + "*(Correction for idx 2: next element strictly less than 2 doesn't exist, so right[2] = 4−2 = 2 using strict-greater pop. Contribution = 2×1×2 = 4.)*", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each element is pushed and popped from the stack at most once in each of the two passes. The contribution pass is also a single linear scan. Total work is `O(3n) = O(n)`.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "Three auxiliary arrays of size `n` (`leftDistances`, `rightDistances`, and the stack itself) contribute `O(n)` extra space.", + + bestAndWorstCase: + "**Best case** — a strictly increasing array like `[1, 2, 3, 4]`: the stack never pops during either pass because each element is greater than the one before it. Every element's left distance is 1 and right distance equals the remaining length.\n\n" + + "**Worst case** — a strictly decreasing array like `[4, 3, 2, 1]`: during the right-boundaries pass, each new element triggers pops of all preceding stack entries. However, amortized across the entire pass each element is pushed and popped at most once, keeping the total at `O(n)`.\n\n" + + "The algorithm always runs in exactly `O(n)` regardless of input shape.", + + realWorldUses: [ + "**Financial analytics:** Computing minimum-price windows across rolling time ranges to detect sustained low-cost periods.", + "**Histogram analysis:** Related to the Largest Rectangle in Histogram problem — both use monotonic stacks to find left/right boundaries efficiently.", + "**Range minimum query preprocessing:** The boundary arrays built here are a foundation for static RMQ structures used in competitive programming and database engines.", + "**Signal processing:** Identifying subintervals where signal strength never exceeds a threshold minimum.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(n) time — amortized constant work per element across three linear passes.", + "Handles duplicate values correctly via the asymmetric pop conditions.", + "Avoids modular arithmetic pitfalls by applying MOD only in the contribution sum, not in boundary computations.", + ], + limitations: [ + "Requires three passes and two auxiliary arrays, making it less cache-friendly than single-pass alternatives for very large inputs.", + "The two-pass boundary logic (strict vs. non-strict pop conditions) is subtle and easy to get wrong when adapting to variations.", + "Not suitable for online/streaming inputs where the array can grow dynamically — the right-boundary pass requires the full array upfront.", + ], + }, + + whenToUseIt: + "Use this approach whenever you need to aggregate a function (minimum, maximum, first/last occurrence) over all contiguous subarrays efficiently. The contribution technique generalises: replace 'minimum' with any dominance relation that forms a monotonic stack structure. Avoid it when the input changes dynamically — use a segment tree with lazy propagation instead.", +}; diff --git a/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/index.ts b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/index.ts new file mode 100644 index 00000000..be236264 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { sumOfSubarrayMinimums } from "./sources/sum-of-subarray-minimums.ts?fn"; +import { generateSumOfSubarrayMinimumsSteps } from "./step-generator"; +import type { SumOfSubarrayMinimumsInput } from "./step-generator"; +import { sumOfSubarrayMinimumsEducational } from "./educational"; + +import typescriptSource from "./sources/sum-of-subarray-minimums.ts?raw"; +import pythonSource from "./sources/sum-of-subarray-minimums.py?raw"; +import javaSource from "./sources/SumOfSubarrayMinimums.java?raw"; + +function executeSumOfSubarrayMinimums(input: SumOfSubarrayMinimumsInput): number { + return sumOfSubarrayMinimums(input.arr) as number; +} + +const sumOfSubarrayMinimumsDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.SUM_OF_SUBARRAY_MINIMUMS!, + name: "Sum of Subarray Minimums", + category: CATEGORY.STACKS_QUEUES!, + technique: "monotonic-stack", + description: + "Compute the sum of minimums of all contiguous subarrays using a monotonic increasing stack to determine each element's left and right contribution boundaries", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { arr: [3, 1, 2, 4] }, + }, + execute: executeSumOfSubarrayMinimums, + generateSteps: generateSumOfSubarrayMinimumsSteps, + educational: sumOfSubarrayMinimumsEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(sumOfSubarrayMinimumsDefinition); diff --git a/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/sources/SumOfSubarrayMinimums.java b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/sources/SumOfSubarrayMinimums.java new file mode 100644 index 00000000..46a702e9 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/sources/SumOfSubarrayMinimums.java @@ -0,0 +1,47 @@ +// Sum of Subarray Minimums — for each element, compute its contribution as minimum across subarrays using monotonic stack +import java.util.ArrayDeque; +import java.util.Deque; + +public class SumOfSubarrayMinimums { + public static int sumOfSubarrayMinimums(int[] arr) { + final int MOD = 1_000_000_007; // @step:initialize + int arrayLength = arr.length; // @step:initialize + int[] leftDistances = new int[arrayLength]; // @step:initialize + int[] rightDistances = new int[arrayLength]; // @step:initialize + Deque indexStack = new ArrayDeque<>(); // @step:initialize + + // Compute left distances: distance to previous less element + for (int elementIdx = 0; elementIdx < arrayLength; elementIdx++) { + int currentValue = arr[elementIdx]; // @step:visit + // Pop while stack top has value >= current (not strictly less) + while (!indexStack.isEmpty() && arr[indexStack.peek()] >= currentValue) { // @step:compare + indexStack.pop(); // @step:maintain-monotonic + } + leftDistances[elementIdx] = + indexStack.isEmpty() ? elementIdx + 1 : elementIdx - indexStack.peek(); // @step:resolve + indexStack.push(elementIdx); // @step:push + } + + indexStack.clear(); // @step:initialize + + // Compute right distances: distance to next less-or-equal element + for (int elementIdx = arrayLength - 1; elementIdx >= 0; elementIdx--) { + int currentValue = arr[elementIdx]; // @step:visit + // Pop while stack top has value > current (strictly greater — allows equal on right) + while (!indexStack.isEmpty() && arr[indexStack.peek()] > currentValue) { // @step:compare + indexStack.pop(); // @step:maintain-monotonic + } + rightDistances[elementIdx] = + indexStack.isEmpty() ? arrayLength - elementIdx : indexStack.peek() - elementIdx; // @step:resolve + indexStack.push(elementIdx); // @step:push + } + + // Sum contributions: each element contributes arr[i] * left[i] * right[i] + long result = 0; // @step:initialize + for (int elementIdx = 0; elementIdx < arrayLength; elementIdx++) { + result = (result + (long) arr[elementIdx] * leftDistances[elementIdx] * rightDistances[elementIdx]) % MOD; // @step:resolve + } + + return (int) result; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/sources/sum-of-subarray-minimums.py b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/sources/sum-of-subarray-minimums.py new file mode 100644 index 00000000..743f6102 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/sources/sum-of-subarray-minimums.py @@ -0,0 +1,40 @@ +# Sum of Subarray Minimums — for each element, compute its contribution as minimum across subarrays using monotonic stack +def sum_of_subarray_minimums(arr): + MOD = 1_000_000_007 # @step:initialize + array_length = len(arr) # @step:initialize + left_distances = [0] * array_length # @step:initialize + right_distances = [0] * array_length # @step:initialize + index_stack = [] # @step:initialize + + # Compute left distances: distance to previous less element + for element_idx in range(array_length): # @step:visit + current_value = arr[element_idx] # @step:visit + # Pop while stack top has value >= current (not strictly less) + while index_stack and arr[index_stack[-1]] >= current_value: # @step:compare + index_stack.pop() # @step:maintain-monotonic + left_distances[element_idx] = ( + element_idx + 1 if not index_stack else element_idx - index_stack[-1] + ) # @step:resolve + index_stack.append(element_idx) # @step:push + + index_stack.clear() # @step:initialize + + # Compute right distances: distance to next less-or-equal element + for element_idx in range(array_length - 1, -1, -1): # @step:visit + current_value = arr[element_idx] # @step:visit + # Pop while stack top has value > current (strictly greater — allows equal on right) + while index_stack and arr[index_stack[-1]] > current_value: # @step:compare + index_stack.pop() # @step:maintain-monotonic + right_distances[element_idx] = ( + array_length - element_idx if not index_stack else index_stack[-1] - element_idx + ) # @step:resolve + index_stack.append(element_idx) # @step:push + + # Sum contributions: each element contributes arr[i] * left[i] * right[i] + result = 0 # @step:initialize + for element_idx in range(array_length): # @step:resolve + result = ( + result + arr[element_idx] * left_distances[element_idx] * right_distances[element_idx] + ) % MOD # @step:resolve + + return result # @step:complete diff --git a/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/sources/sum-of-subarray-minimums.ts b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/sources/sum-of-subarray-minimums.ts new file mode 100644 index 00000000..f009b3d6 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/sources/sum-of-subarray-minimums.ts @@ -0,0 +1,48 @@ +// Sum of Subarray Minimums — for each element, compute its contribution as minimum across subarrays using monotonic stack +function sumOfSubarrayMinimums(arr: number[]): number { + const MOD = 1_000_000_007; // @step:initialize + const arrayLength = arr.length; // @step:initialize + const leftDistances: number[] = new Array(arrayLength).fill(0); // @step:initialize + const rightDistances: number[] = new Array(arrayLength).fill(0); // @step:initialize + const indexStack: number[] = []; // @step:initialize + + // Compute left distances: distance to previous less element + for (let elementIdx = 0; elementIdx < arrayLength; elementIdx++) { + const currentValue = arr[elementIdx]!; // @step:visit + // Pop while stack top has value >= current (not strictly less) + while (indexStack.length > 0 && arr[indexStack[indexStack.length - 1]!]! >= currentValue) { + // @step:compare + indexStack.pop(); // @step:maintain-monotonic + } + leftDistances[elementIdx] = + indexStack.length === 0 ? elementIdx + 1 : elementIdx - indexStack[indexStack.length - 1]!; // @step:resolve + indexStack.push(elementIdx); // @step:push + } + + indexStack.length = 0; // @step:initialize + + // Compute right distances: distance to next less-or-equal element + for (let elementIdx = arrayLength - 1; elementIdx >= 0; elementIdx--) { + const currentValue = arr[elementIdx]!; // @step:visit + // Pop while stack top has value > current (strictly greater — allows equal on right) + while (indexStack.length > 0 && arr[indexStack[indexStack.length - 1]!]! > currentValue) { + // @step:compare + indexStack.pop(); // @step:maintain-monotonic + } + rightDistances[elementIdx] = + indexStack.length === 0 + ? arrayLength - elementIdx + : indexStack[indexStack.length - 1]! - elementIdx; // @step:resolve + indexStack.push(elementIdx); // @step:push + } + + // Sum contributions: each element contributes arr[i] * left[i] * right[i] + let result = 0; // @step:initialize + for (let elementIdx = 0; elementIdx < arrayLength; elementIdx++) { + result = + (result + arr[elementIdx]! * leftDistances[elementIdx]! * rightDistances[elementIdx]!) % // @step:resolve + MOD; + } + + return result; // @step:complete +} diff --git a/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/step-generator.test.ts b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/step-generator.test.ts new file mode 100644 index 00000000..46563624 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/step-generator.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect } from "vitest"; +import { generateSumOfSubarrayMinimumsSteps } from "./step-generator"; + +describe("generateSumOfSubarrayMinimumsSteps", () => { + it("produces steps for the default input", () => { + const steps = generateSumOfSubarrayMinimumsSteps({ arr: [3, 1, 2, 4] }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateSumOfSubarrayMinimumsSteps({ arr: [3, 1, 2, 4] }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateSumOfSubarrayMinimumsSteps({ arr: [3, 1, 2, 4] }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateSumOfSubarrayMinimumsSteps({ arr: [3, 1, 2, 4] }); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateSumOfSubarrayMinimumsSteps({ arr: [3, 1, 2, 4] }); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits visit steps for each element in each pass", () => { + const steps = generateSumOfSubarrayMinimumsSteps({ arr: [3, 1, 2, 4] }); + const visitSteps = steps.filter((step) => step.type === "visit"); + // Two passes over 4 elements = 8 visit steps + expect(visitSteps.length).toBe(8); + }); + + it("emits compare steps for boundary resolution and contribution sum", () => { + const steps = generateSumOfSubarrayMinimumsSteps({ arr: [3, 1, 2, 4] }); + const compareSteps = steps.filter((step) => step.type === "compare"); + // 4 left-boundary compares + 4 right-boundary compares + 4 contribution compares = 12 + expect(compareSteps.length).toBe(12); + }); + + it("emits push steps for each element in each pass", () => { + const steps = generateSumOfSubarrayMinimumsSteps({ arr: [3, 1, 2, 4] }); + const pushSteps = steps.filter((step) => step.type === "push"); + // Two passes over 4 elements = at least 4 pushes (some may be preceded by pops) + expect(pushSteps.length).toBeGreaterThanOrEqual(4); + }); + + it("final complete step contains the correct result", () => { + const steps = generateSumOfSubarrayMinimumsSteps({ arr: [3, 1, 2, 4] }); + const lastStep = steps[steps.length - 1]!; + expect(lastStep.type).toBe("complete"); + expect(lastStep.variables["result"]).toBe(17); + }); + + it("handles a single-element array", () => { + const steps = generateSumOfSubarrayMinimumsSteps({ arr: [5] }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + const lastStep = steps[steps.length - 1]!; + expect(lastStep.variables["result"]).toBe(5); + }); + + it("handles duplicate values in the array", () => { + const steps = generateSumOfSubarrayMinimumsSteps({ arr: [1, 1] }); + expect(steps.length).toBeGreaterThan(0); + const lastStep = steps[steps.length - 1]!; + expect(lastStep.variables["result"]).toBe(3); + }); +}); diff --git a/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/step-generator.ts b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/step-generator.ts new file mode 100644 index 00000000..0bbfd79c --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/step-generator.ts @@ -0,0 +1,153 @@ +/** Step generator for Sum of Subarray Minimums — produces ExecutionStep[] using NumericStackTracker. */ + +import type { ExecutionStep } from "@/types"; +import { NumericStackTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const SOSM_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.SUM_OF_SUBARRAY_MINIMUMS!); + +export interface SumOfSubarrayMinimumsInput { + arr: number[]; +} + +const MOD = 1_000_000_007; + +export function generateSumOfSubarrayMinimumsSteps( + input: SumOfSubarrayMinimumsInput, +): ExecutionStep[] { + const { arr } = input; + const arrayLength = arr.length; + const tracker = new NumericStackTracker(arr, SOSM_LINE_MAP); + tracker.setMonotonicOrder("increasing"); + + const leftDistances: number[] = new Array(arrayLength).fill(0); + const rightDistances: number[] = new Array(arrayLength).fill(0); + const indexStack: number[] = []; + + tracker.initialize({ + arr, + arrayLength, + leftDistances: [...leftDistances], + rightDistances: [...rightDistances], + }); + + // Pass 1: Compute left distances (previous less element) + for (let elementIdx = 0; elementIdx < arrayLength; elementIdx++) { + const currentValue = arr[elementIdx]!; + + tracker.processElement(elementIdx, { + elementIdx, + currentValue, + pass: "left", + stackSize: indexStack.length, + }); + + while (indexStack.length > 0 && arr[indexStack[indexStack.length - 1]!]! >= currentValue) { + const stackTopIdx = indexStack[indexStack.length - 1]!; + const stackTopValue = arr[stackTopIdx]!; + indexStack.pop(); + + tracker.maintainMonotonic( + { elementIdx, currentValue, stackTopIdx, stackTopValue, pass: "left" }, + `Pop index ${stackTopIdx} (value ${stackTopValue}) — it is ≥ ${currentValue}, not a previous-less boundary`, + ); + } + + leftDistances[elementIdx] = + indexStack.length === 0 ? elementIdx + 1 : elementIdx - indexStack[indexStack.length - 1]!; + + tracker.compare( + { + elementIdx, + currentValue, + leftDistance: leftDistances[elementIdx], + stackTop: indexStack.length > 0 ? indexStack[indexStack.length - 1] : null, + pass: "left", + }, + `Left distance for index ${elementIdx} (value ${currentValue}) = ${leftDistances[elementIdx]!}`, + ); + + indexStack.push(elementIdx); + + tracker.pushIndex( + elementIdx, + { elementIdx, currentValue, leftDistance: leftDistances[elementIdx], pass: "left" }, + `Push index ${elementIdx} (value ${currentValue}) onto stack`, + ); + } + + indexStack.length = 0; + + // Pass 2: Compute right distances (next less-or-equal element) + for (let elementIdx = arrayLength - 1; elementIdx >= 0; elementIdx--) { + const currentValue = arr[elementIdx]!; + + tracker.processElement(elementIdx, { + elementIdx, + currentValue, + pass: "right", + stackSize: indexStack.length, + }); + + while (indexStack.length > 0 && arr[indexStack[indexStack.length - 1]!]! > currentValue) { + const stackTopIdx = indexStack[indexStack.length - 1]!; + const stackTopValue = arr[stackTopIdx]!; + indexStack.pop(); + + tracker.maintainMonotonic( + { elementIdx, currentValue, stackTopIdx, stackTopValue, pass: "right" }, + `Pop index ${stackTopIdx} (value ${stackTopValue}) — it is > ${currentValue}, not a next-less-or-equal boundary`, + ); + } + + rightDistances[elementIdx] = + indexStack.length === 0 + ? arrayLength - elementIdx + : indexStack[indexStack.length - 1]! - elementIdx; + + tracker.compare( + { + elementIdx, + currentValue, + rightDistance: rightDistances[elementIdx], + stackTop: indexStack.length > 0 ? indexStack[indexStack.length - 1] : null, + pass: "right", + }, + `Right distance for index ${elementIdx} (value ${currentValue}) = ${rightDistances[elementIdx]!}`, + ); + + indexStack.push(elementIdx); + + tracker.pushIndex( + elementIdx, + { elementIdx, currentValue, rightDistance: rightDistances[elementIdx], pass: "right" }, + `Push index ${elementIdx} (value ${currentValue}) onto stack`, + ); + } + + // Pass 3: Sum contributions + let totalSum = 0; + for (let elementIdx = 0; elementIdx < arrayLength; elementIdx++) { + const contribution = + arr[elementIdx]! * leftDistances[elementIdx]! * rightDistances[elementIdx]!; + totalSum = (totalSum + contribution) % MOD; + + tracker.compare( + { + elementIdx, + value: arr[elementIdx], + leftDistance: leftDistances[elementIdx], + rightDistance: rightDistances[elementIdx], + contribution, + totalSum, + pass: "sum", + }, + `Element ${arr[elementIdx]!} × left(${leftDistances[elementIdx]!}) × right(${rightDistances[elementIdx]!}) = ${contribution}, running sum = ${totalSum}`, + ); + } + + tracker.complete({ result: totalSum, arrayLength }, `Sum of all subarray minimums = ${totalSum}`); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/sum-of-subarray-minimums.test.ts b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/sum-of-subarray-minimums.test.ts new file mode 100644 index 00000000..f1eb7c68 --- /dev/null +++ b/src/algorithms/stacks-queues/monotonic-stack/sum-of-subarray-minimums/sum-of-subarray-minimums.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from "vitest"; +import { sumOfSubarrayMinimums } from "./sources/sum-of-subarray-minimums.ts?fn"; + +describe("sumOfSubarrayMinimums", () => { + it("returns 17 for [3, 1, 2, 4]", () => { + // Subarrays: [3]=3, [1]=1, [2]=2, [4]=4, [3,1]=1, [1,2]=1, [2,4]=2, [3,1,2]=1, [1,2,4]=1, [3,1,2,4]=1 + // Sum = 3+1+2+4+1+1+2+1+1+1 = 17 + expect(sumOfSubarrayMinimums([3, 1, 2, 4])).toBe(17); + }); + + it("returns 444 for [11, 81, 94, 43, 3]", () => { + expect(sumOfSubarrayMinimums([11, 81, 94, 43, 3])).toBe(444); + }); + + it("returns the element itself for a single-element array", () => { + expect(sumOfSubarrayMinimums([5])).toBe(5); + }); + + it("handles an array of all equal elements", () => { + // [2,2,2]: subarrays [2],[2],[2],[2,2],[2,2],[2,2,2] → min is always 2 + // Count of subarrays = n*(n+1)/2 = 6, sum = 6*2 = 12 + expect(sumOfSubarrayMinimums([2, 2, 2])).toBe(12); + }); + + it("handles a strictly increasing array", () => { + // [1,2,3]: [1]=1,[2]=2,[3]=3,[1,2]=1,[2,3]=2,[1,2,3]=1 → 1+2+3+1+2+1 = 10 + expect(sumOfSubarrayMinimums([1, 2, 3])).toBe(10); + }); + + it("handles a strictly decreasing array", () => { + // [3,2,1]: [3]=3,[2]=2,[1]=1,[3,2]=2,[2,1]=1,[3,2,1]=1 → 3+2+1+2+1+1 = 10 + expect(sumOfSubarrayMinimums([3, 2, 1])).toBe(10); + }); + + it("handles duplicate values correctly without double-counting", () => { + // [1,1]: subarrays [1],[1],[1,1] → 1+1+1 = 3 + expect(sumOfSubarrayMinimums([1, 1])).toBe(3); + }); + + it("applies modulo for very large values", () => { + const largeArray = new Array(100).fill(30000); + const result = sumOfSubarrayMinimums(largeArray) as number; + expect(result).toBeGreaterThanOrEqual(0); + expect(result).toBeLessThan(1_000_000_007); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/FirstNonRepeatingCharStreamPipeline.stories.tsx b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/FirstNonRepeatingCharStreamPipeline.stories.tsx new file mode 100644 index 00000000..efdb25a9 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/FirstNonRepeatingCharStreamPipeline.stories.tsx @@ -0,0 +1,56 @@ +/** + * Storybook stories for the First Non-Repeating Char Stream algorithm pipeline. + * Uses the real step generator with "aabcbcd", rendering the + * StackQueueVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateFirstNonRepeatingCharStreamSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateFirstNonRepeatingCharStreamSteps({ inputString: "aabcbcd" }); +const allUniqueSteps = generateFirstNonRepeatingCharStreamSteps({ inputString: "abcd" }); + +const meta: Meta = { + title: "Algorithm Pipelines/First Non-Repeating Char Stream", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — empty queue and frequency map before any characters are processed */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing — some characters enqueued, repeated characters pruned from the front */ +export const MidProcessing: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Completed — all characters processed, final results array available */ +export const Complete: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** All unique characters — queue never pruned, front is always the first character */ +export const AllUnique: Story = { + args: { + visualState: allUniqueSteps[allUniqueSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/educational.ts b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/educational.ts new file mode 100644 index 00000000..e52a63b4 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/educational.ts @@ -0,0 +1,60 @@ +import type { EducationalContent } from "@/types"; + +export const firstNonRepeatingCharStreamEducational: EducationalContent = { + overview: + "**First Non-Repeating Character in a Stream** finds, after each character is read, which character in the stream so far has appeared exactly once and arrived earliest.\n\n" + + "A **queue** acts as a candidate buffer — it holds characters that might still be the answer. A **frequency map** tracks how many times each character has been seen. When a character's count rises above one it is no longer a candidate, so stale entries are pruned from the front of the queue. The front of the surviving queue is always the answer.", + + howItWorks: + "The algorithm processes each incoming character and maintains two structures:\n\n" + + "1. **Frequency map** — `freqMap[char]` is incremented for each character seen.\n" + + "2. **Candidate queue** — every new character is pushed to the rear.\n\n" + + "After each enqueue, characters are removed from the **front** while `freqMap[front] > 1` — those characters are repeated and can never be the answer again. The front of the remaining queue is the first non-repeating character, or `#` if the queue is empty.\n\n" + + "### Example trace on `aabcbcd`\n\n" + + "```\n" + + "step char freqMap queue answer\n" + + "1 a {a:1} [a] a\n" + + "2 a {a:2} [] #\n" + + "3 b {a:2, b:1} [b] b\n" + + "4 c {a:2, b:1, c:1} [b,c] b\n" + + "5 b {a:2, b:2, c:1} [c] c\n" + + "6 c {a:2, b:2, c:2} [] # → wait... d not yet seen\n" + + "7 d {a:2, b:2, c:2, d:1} [d] d\n" + + "```\n\n" + + 'Result array: `["a", "#", "b", "b", "c", "#", "d"]`', + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each character is enqueued once and dequeued at most once, so the total number of queue operations across the entire stream is `O(n)`. The frequency map lookup and update are `O(1)` per character.\n\n" + + "**Space Complexity: `O(k)`**\n\n" + + "The frequency map holds at most `k` entries where `k` is the size of the character alphabet (26 for lowercase English letters). The queue holds at most `k` distinct characters at any time — repeated characters are pruned immediately — so its size is also bounded by `O(k)`.", + + bestAndWorstCase: + "**Best case** — every character appears at least twice: the queue stays empty after pruning, and every answer is `#` in `O(1)` amortised work per step.\n\n" + + "**Worst case** — all characters are distinct: no pruning occurs, the queue grows to length `n`, and every step peeks in `O(1)`. Overall still `O(n)` time.\n\n" + + "The algorithm is `O(n)` in all cases because each element is enqueued and dequeued at most once.", + + realWorldUses: [ + "**Live typing suggestions:** Highlight the first unique character as the user types in a search box or form field.", + "**Stream deduplication dashboards:** Monitor event streams and flag the first event type that has appeared only once so far.", + "**Log analysis tools:** Identify the first unique error code in a stream of server logs to surface novel failures quickly.", + "**Interview and competitive programming:** Classic problem used to demonstrate the power of combining a frequency map with a FIFO queue.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(n) time — each character is processed with a constant number of operations amortised.", + "O(k) space — memory is bounded by alphabet size, not stream length.", + "Online algorithm — produces an answer after each character without needing the full input.", + "Simple and composable — the queue + frequency map pattern applies to many stream problems.", + ], + limitations: [ + "Returns '#' instead of a typed sentinel value — callers must agree on the convention.", + "Alphabet must be finite and known; very large alphabets increase space usage proportionally.", + "Only tracks the first non-repeating character — finding the k-th requires a more elaborate structure.", + ], + }, + + whenToUseIt: + "Use this pattern whenever you need to query the first unique element in a growing stream after each new arrival. The queue + frequency map combination is ideal when the order of first appearance matters and the alphabet is bounded. For offline processing (full input available), a single scan with a linked hash map is equivalent but simpler to implement.", +}; diff --git a/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/first-non-repeating-char-stream.test.ts b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/first-non-repeating-char-stream.test.ts new file mode 100644 index 00000000..358f710a --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/first-non-repeating-char-stream.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect } from "vitest"; +import { firstNonRepeatingCharStream } from "./sources/first-non-repeating-char-stream.ts?fn"; + +describe("firstNonRepeatingCharStream", () => { + it("returns correct results for the default input 'aabcbcd'", () => { + expect(firstNonRepeatingCharStream("aabcbcd")).toEqual(["a", "#", "b", "b", "c", "#", "d"]); + }); + + it("returns the only character for a single-character input", () => { + expect(firstNonRepeatingCharStream("z")).toEqual(["z"]); + }); + + it("returns '#' for a string where all characters repeat", () => { + // 'a': freq={a:1}, queue=['a'] → 'a' + // 'a': freq={a:2}, queue=['a','a'] → drain 'a' (freq>1) twice → '#' + // 'b': freq={a:2,b:1}, queue=['b'] → 'b' + // 'b': freq={a:2,b:2}, queue=['b','b'] → drain 'b' (freq>1) twice → '#' + expect(firstNonRepeatingCharStream("aabb")).toEqual(["a", "#", "b", "#"]); + }); + + it("returns every character when all are distinct", () => { + expect(firstNonRepeatingCharStream("abcd")).toEqual(["a", "a", "a", "a"]); + }); + + it("handles two identical characters", () => { + expect(firstNonRepeatingCharStream("aa")).toEqual(["a", "#"]); + }); + + it("returns '#' for an input that starts with a repeated pair", () => { + const result = firstNonRepeatingCharStream("aab"); + expect(result[0]).toBe("a"); + expect(result[1]).toBe("#"); + expect(result[2]).toBe("b"); + }); + + it("tracks the first non-repeating correctly when a later repeat evicts the queue front", () => { + // 'a' is unique until the second 'a' arrives; 'b' then becomes the answer + const result = firstNonRepeatingCharStream("aba"); + expect(result).toEqual(["a", "a", "b"]); + }); + + it("returns an empty array for an empty string", () => { + expect(firstNonRepeatingCharStream("")).toEqual([]); + }); + + it("handles longer repeated sequences correctly", () => { + const result = firstNonRepeatingCharStream("aaaabc"); + expect(result[0]).toBe("a"); + expect(result[1]).toBe("#"); + expect(result[2]).toBe("#"); + expect(result[3]).toBe("#"); + expect(result[4]).toBe("b"); + expect(result[5]).toBe("b"); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/index.ts b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/index.ts new file mode 100644 index 00000000..7bd93808 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/index.ts @@ -0,0 +1,46 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { firstNonRepeatingCharStream } from "./sources/first-non-repeating-char-stream.ts?fn"; +import { generateFirstNonRepeatingCharStreamSteps } from "./step-generator"; +import type { FirstNonRepeatingCharStreamInput } from "./step-generator"; +import { firstNonRepeatingCharStreamEducational } from "./educational"; + +import typescriptSource from "./sources/first-non-repeating-char-stream.ts?raw"; +import pythonSource from "./sources/first-non-repeating-char-stream.py?raw"; +import javaSource from "./sources/FirstNonRepeatingCharStream.java?raw"; + +function executeFirstNonRepeatingCharStream(input: FirstNonRepeatingCharStreamInput): string[] { + return firstNonRepeatingCharStream(input.inputString) as string[]; +} + +const firstNonRepeatingCharStreamDefinition: AlgorithmDefinition = + { + meta: { + id: ALGORITHM_ID.FIRST_NON_REPEATING_CHAR_STREAM!, + name: "First Non-Repeating Char Stream", + category: CATEGORY.STACKS_QUEUES!, + technique: "queue-applications", + description: + "Find the first non-repeating character in a stream after each new character arrives — uses a queue as a candidate buffer and a frequency map to prune repeated characters from the front", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(k)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { inputString: "aabcbcd" }, + }, + execute: executeFirstNonRepeatingCharStream, + generateSteps: generateFirstNonRepeatingCharStreamSteps, + educational: firstNonRepeatingCharStreamEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, + }; + +registry.register(firstNonRepeatingCharStreamDefinition); diff --git a/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/sources/FirstNonRepeatingCharStream.java b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/sources/FirstNonRepeatingCharStream.java new file mode 100644 index 00000000..335d78b7 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/sources/FirstNonRepeatingCharStream.java @@ -0,0 +1,27 @@ +// First Non-Repeating Char Stream — use a queue as candidate buffer and a frequency map to find the first non-repeating character at each step +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class FirstNonRepeatingCharStream { + public static List firstNonRepeatingCharStream(String inputString) { + Map freqMap = new HashMap<>(); // @step:initialize + Deque queue = new ArrayDeque<>(); // @step:initialize + List results = new ArrayList<>(); // @step:initialize + for (int charIdx = 0; charIdx < inputString.length(); charIdx++) { + char currentChar = inputString.charAt(charIdx); // @step:visit + freqMap.put(currentChar, freqMap.getOrDefault(currentChar, 0) + 1); // @step:visit + queue.add(currentChar); // @step:enqueue + // Remove repeated characters from the front of the queue + while (!queue.isEmpty() && freqMap.get(queue.peek()) > 1) { // @step:dequeue + queue.poll(); // @step:dequeue + } + String answer = queue.isEmpty() ? "#" : String.valueOf(queue.peek()); // @step:peek + results.add(answer); // @step:peek + } + return results; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/sources/first-non-repeating-char-stream.py b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/sources/first-non-repeating-char-stream.py new file mode 100644 index 00000000..ca023f9d --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/sources/first-non-repeating-char-stream.py @@ -0,0 +1,16 @@ +# First Non-Repeating Char Stream — use a queue as candidate buffer and a frequency map to find the first non-repeating character at each step +from collections import deque + +def first_non_repeating_char_stream(input_string): + freq_map = {} # @step:initialize + queue = deque() # @step:initialize + results = [] # @step:initialize + for char in input_string: # @step:visit + freq_map[char] = freq_map.get(char, 0) + 1 # @step:visit + queue.append(char) # @step:enqueue + # Remove repeated characters from the front of the queue + while queue and freq_map[queue[0]] > 1: # @step:dequeue + queue.popleft() # @step:dequeue + answer = queue[0] if queue else "#" # @step:peek + results.append(answer) # @step:peek + return results # @step:complete diff --git a/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/sources/first-non-repeating-char-stream.ts b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/sources/first-non-repeating-char-stream.ts new file mode 100644 index 00000000..d48936f7 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/sources/first-non-repeating-char-stream.ts @@ -0,0 +1,18 @@ +// First Non-Repeating Char Stream — use a queue as candidate buffer and a frequency map to find the first non-repeating character at each step +function firstNonRepeatingCharStream(inputString: string): string[] { + const freqMap: Record = {}; // @step:initialize + const queue: string[] = []; // @step:initialize + const results: string[] = []; // @step:initialize + for (let charIdx = 0; charIdx < inputString.length; charIdx++) { + const char = inputString[charIdx]!; // @step:visit + freqMap[char] = (freqMap[char] ?? 0) + 1; // @step:visit + queue.push(char); // @step:enqueue + // Remove repeated characters from the front of the queue + while (queue.length > 0 && (freqMap[queue[0]!] ?? 0) > 1) { + queue.shift(); // @step:dequeue + } + const answer = queue.length > 0 ? queue[0]! : "#"; // @step:peek + results.push(answer); // @step:peek + } + return results; // @step:complete +} diff --git a/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/step-generator.test.ts b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/step-generator.test.ts new file mode 100644 index 00000000..b26968b1 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/step-generator.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect } from "vitest"; +import { generateFirstNonRepeatingCharStreamSteps } from "./step-generator"; + +const DEFAULT_INPUT = { inputString: "aabcbcd" }; + +describe("generateFirstNonRepeatingCharStreamSteps", () => { + it("produces steps for the default input", () => { + const steps = generateFirstNonRepeatingCharStreamSteps(DEFAULT_INPUT); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateFirstNonRepeatingCharStreamSteps(DEFAULT_INPUT); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateFirstNonRepeatingCharStreamSteps(DEFAULT_INPUT); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateFirstNonRepeatingCharStreamSteps(DEFAULT_INPUT); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateFirstNonRepeatingCharStreamSteps(DEFAULT_INPUT); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits one visit step per character in the input", () => { + const steps = generateFirstNonRepeatingCharStreamSteps(DEFAULT_INPUT); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(DEFAULT_INPUT.inputString.length); + }); + + it("emits one enqueue step per character in the input", () => { + const steps = generateFirstNonRepeatingCharStreamSteps(DEFAULT_INPUT); + const enqueueSteps = steps.filter((step) => step.type === "enqueue"); + expect(enqueueSteps.length).toBe(DEFAULT_INPUT.inputString.length); + }); + + it("emits one peek step per character in the input", () => { + const steps = generateFirstNonRepeatingCharStreamSteps(DEFAULT_INPUT); + const peekSteps = steps.filter((step) => step.type === "peek"); + expect(peekSteps.length).toBe(DEFAULT_INPUT.inputString.length); + }); + + it("emits dequeue steps when repeated characters are pruned from the front", () => { + const steps = generateFirstNonRepeatingCharStreamSteps(DEFAULT_INPUT); + const dequeueSteps = steps.filter((step) => step.type === "dequeue"); + // 'aabcbcd': second 'a' evicts 2 (both 'a's), second 'b' evicts 1, second 'c' evicts 3 (c,b,c) = 6 total + expect(dequeueSteps.length).toBe(6); + }); + + it("emits a single complete step", () => { + const steps = generateFirstNonRepeatingCharStreamSteps(DEFAULT_INPUT); + const completeSteps = steps.filter((step) => step.type === "complete"); + expect(completeSteps.length).toBe(1); + }); + + it("tracks queue operations in metrics", () => { + const steps = generateFirstNonRepeatingCharStreamSteps(DEFAULT_INPUT); + const lastStep = steps[steps.length - 1]; + expect(lastStep?.metrics.queueOperations).toBeGreaterThan(0); + }); + + it("handles a single character input", () => { + const steps = generateFirstNonRepeatingCharStreamSteps({ inputString: "x" }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(1); + }); + + it("handles an empty string with only initialize and complete steps", () => { + const steps = generateFirstNonRepeatingCharStreamSteps({ inputString: "" }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(0); + }); + + it("includes results in the complete step variables", () => { + const steps = generateFirstNonRepeatingCharStreamSteps(DEFAULT_INPUT); + const completeStep = steps[steps.length - 1]; + expect(completeStep?.variables).toHaveProperty("results"); + expect(completeStep?.variables).toHaveProperty("inputString"); + }); + + it("emits no dequeue steps when all characters are distinct", () => { + const steps = generateFirstNonRepeatingCharStreamSteps({ inputString: "abcd" }); + const dequeueSteps = steps.filter((step) => step.type === "dequeue"); + expect(dequeueSteps.length).toBe(0); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/step-generator.ts b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/step-generator.ts new file mode 100644 index 00000000..14cbb82f --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/first-non-repeating-char-stream/step-generator.ts @@ -0,0 +1,97 @@ +/** Step generator for First Non-Repeating Char Stream — produces ExecutionStep[] using QueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { QueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const FNRCS_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.FIRST_NON_REPEATING_CHAR_STREAM!); + +export interface FirstNonRepeatingCharStreamInput { + inputString: string; +} + +export function generateFirstNonRepeatingCharStreamSteps( + input: FirstNonRepeatingCharStreamInput, +): ExecutionStep[] { + const { inputString } = input; + const chars = inputString.split(""); + const tracker = new QueueTracker(chars, FNRCS_LINE_MAP); + + const freqMap: Record = {}; + const queue: string[] = []; + const results: string[] = []; + + tracker.initialize({ + inputString, + freqMap: {}, + queue: [], + results: [], + }); + + for (let charIdx = 0; charIdx < chars.length; charIdx++) { + const char = chars[charIdx]!; + freqMap[char] = (freqMap[char] ?? 0) + 1; + + tracker.processElement(charIdx, { + charIdx, + char, + freqMap: { ...freqMap }, + queue: [...queue], + results: [...results], + }); + + queue.push(char); + tracker.enqueue(char, { + charIdx, + char, + freq: freqMap[char], + freqMap: { ...freqMap }, + queue: [...queue], + results: [...results], + }); + + // Drain repeated characters from the front + while (queue.length > 0 && (freqMap[queue[0]!] ?? 0) > 1) { + const frontChar = queue[0]!; + queue.shift(); + tracker.dequeue( + { + charIdx, + removedChar: frontChar, + freq: freqMap[frontChar], + freqMap: { ...freqMap }, + queue: [...queue], + results: [...results], + }, + `Dequeue '${frontChar}' — it has appeared ${String(freqMap[frontChar])} times and is no longer unique`, + ); + } + + const answer = queue.length > 0 ? queue[0]! : "#"; + results.push(answer); + + tracker.peekFront( + { + charIdx, + answer, + freqMap: { ...freqMap }, + queue: [...queue], + results: [...results], + }, + answer === "#" + ? "No non-repeating character — result is '#'" + : `First non-repeating character is '${answer}'`, + ); + } + + tracker.complete( + { + inputString, + results: [...results], + }, + `Processed ${String(inputString.length)} character${inputString.length === 1 ? "" : "s"} — results: [${results.join(", ")}]`, + ); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/FlattenNestedListIteratorPipeline.stories.tsx b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/FlattenNestedListIteratorPipeline.stories.tsx new file mode 100644 index 00000000..cf7b0cd7 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/FlattenNestedListIteratorPipeline.stories.tsx @@ -0,0 +1,63 @@ +/** + * Storybook stories for the Flatten Nested List Iterator algorithm pipeline. + * Uses the real step generator with [[1,[2]],3,[4,[5,6]]], rendering the + * StackQueueVisualizer at key stack traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateFlattenNestedListIteratorSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateFlattenNestedListIteratorSteps({ + nestedList: [[1, [2]], 3, [4, [5, 6]]], +}); +const simpleSteps = generateFlattenNestedListIteratorSteps({ + nestedList: [ + [1, 2], + [3, 4], + ], +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Flatten Nested List Iterator", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — stack seeded with the top-level items in reverse order */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing — some integers collected, stack being expanded */ +export const MidProcessing: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Completed — full list flattened to [1,2,3,4,5,6] */ +export const Complete: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Simple input — [[1,2],[3,4]] flattened to [1,2,3,4] */ +export const SimpleInput: Story = { + args: { + visualState: simpleSteps[simpleSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/educational.ts b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/educational.ts new file mode 100644 index 00000000..abe78d6f --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/educational.ts @@ -0,0 +1,64 @@ +import type { EducationalContent } from "@/types"; + +export const flattenNestedListIteratorEducational: EducationalContent = { + overview: + "**Flatten Nested List Iterator** (LeetCode 341) solves the problem of iterating over a deeply nested list structure as if it were a flat sequence of integers. Given a list like `[[1,[2]],3,[4,[5,6]]]`, the algorithm produces `[1,2,3,4,5,6]` in a single left-to-right pass.\n\nA stack drives the flattening: push all top-level items in reverse order so that the first item sits on top, then iteratively pop each item — if it is a number, collect it; if it is a sub-list, push its elements in reverse order. The stack naturally unwinds the nesting depth by depth.", + + howItWorks: + "The algorithm treats the nested structure as a tree of integers and arrays, traversed in pre-order (left to right):\n\n" + + "1. **Initialize** — copy the input list into a stack in reverse order, so the first element is on top.\n" + + "2. **Loop while the stack is not empty:**\n" + + " - **Pop** the top item.\n" + + " - If it is a **number**, append it to the result.\n" + + " - If it is an **array**, push its elements onto the stack in reverse order so its first element becomes the new top.\n" + + "3. **Return** the result array.\n\n" + + "### Example trace for `[[1,[2]],3,[4,[5,6]]]`\n\n" + + "```\n" + + "Initial stack (top → bottom): [1,[2]] 3 [4,[5,6]]\n" + + "Pop [1,[2]] → array → push [2] then 1 stack: 1 [2] 3 [4,[5,6]]\n" + + "Pop 1 → number → result=[1] stack: [2] 3 [4,[5,6]]\n" + + "Pop [2] → array → push 2 stack: 2 3 [4,[5,6]]\n" + + "Pop 2 → number → result=[1,2] stack: 3 [4,[5,6]]\n" + + "Pop 3 → number → result=[1,2,3] stack: [4,[5,6]]\n" + + "Pop [4,[5,6]]→ array → push [5,6] then 4 stack: 4 [5,6]\n" + + "Pop 4 → number → result=[1,2,3,4] stack: [5,6]\n" + + "Pop [5,6] → array → push 6 then 5 stack: 5 6\n" + + "Pop 5 → number → result=[1,2,3,4,5] stack: 6\n" + + "Pop 6 → number → result=[1,2,3,4,5,6] stack: []\n" + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`** where n is the total number of integers across all nesting levels.\n\n" + + "Each integer is pushed onto the stack at most once and popped exactly once. Each array node is also popped once and its direct children pushed once. The total work is proportional to the total number of elements (both integers and arrays), which is bounded by `O(n)` for n integers.\n\n" + + "**Space Complexity: `O(d)`** where d is the maximum nesting depth.\n\n" + + "At any point, the stack holds at most the unprocessed siblings at each depth level. In the worst case — a fully left-skewed nesting like `[[[...n...]]]` — the stack depth reaches `O(d)`. The result array itself is `O(n)` but is considered output space.", + + bestAndWorstCase: + "**Best case** — a flat list with no nesting (depth = 1): every item is a number and is collected immediately with no sub-list expansion. Stack depth stays at 1.\n\n" + + "**Worst case** — a fully left-skewed nesting like `[[[[5]]]]`: each pop reveals another single-element sub-list, driving the stack depth to `O(d)` where d equals the nesting depth. Time is still `O(n)` since each integer is visited exactly once.", + + realWorldUses: [ + "**JSON document traversal:** Deeply nested JSON objects and arrays are flattened using the same stack-based approach when serializing or indexing document fields.", + "**File system walkers:** Recursively listing all files in a directory tree maps directly to this pattern — directories are sub-lists and files are integers.", + "**Abstract syntax tree evaluation:** Compilers traverse nested expression trees (function calls, operator nodes) using a stack to evaluate leaves in order.", + "**HTML/XML DOM serialization:** Flattening a nested node tree into a text stream uses depth-first traversal driven by an explicit or implicit call stack.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(n) time — each element is visited exactly once regardless of nesting structure.", + "O(d) stack space — only the current path from root to the active node is held at any time.", + "Iterative — avoids call-stack overflow on deeply nested inputs that would blow the recursion limit.", + "Naturally produces output in the correct left-to-right order without a post-processing reversal.", + ], + limitations: [ + "Requires reversing sub-arrays before pushing, which adds a hidden constant to the inner loop.", + "Stack allocations grow with nesting depth — pathological inputs with extreme depth still consume memory.", + "Not lazy: this implementation eagerly produces the full result; a true iterator would yield one element at a time.", + "Assumes the nested structure contains only integers and arrays — mixed types require additional type checks.", + ], + }, + + whenToUseIt: + "Use the stack-based flattening approach when you need to convert a nested list to a flat sequence in a single pass without recursion. If you only need a few values from the front of the list (lazy evaluation), implement a proper iterator that advances the stack on demand rather than pre-computing the full result. For shallow nesting (depth ≤ 2), a simple recursive approach is often more readable.", +}; diff --git a/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/flatten-nested-list-iterator.test.ts b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/flatten-nested-list-iterator.test.ts new file mode 100644 index 00000000..569d501b --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/flatten-nested-list-iterator.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from "vitest"; +import { flattenNestedListIterator } from "./sources/flatten-nested-list-iterator.ts?fn"; + +describe("flattenNestedListIterator", () => { + it("flattens [[1,[2]],3,[4,[5,6]]] to [1,2,3,4,5,6]", () => { + expect(flattenNestedListIterator([[1, [2]], 3, [4, [5, 6]]])).toEqual([1, 2, 3, 4, 5, 6]); + }); + + it("flattens a flat list with no nesting", () => { + expect(flattenNestedListIterator([1, 2, 3, 4])).toEqual([1, 2, 3, 4]); + }); + + it("flattens a single deeply nested value [[[7]]] to [7]", () => { + expect(flattenNestedListIterator([[[7]]])).toEqual([7]); + }); + + it("flattens an empty input to an empty array", () => { + expect(flattenNestedListIterator([])).toEqual([]); + }); + + it("flattens [[[]]] (nested empty arrays) to []", () => { + expect(flattenNestedListIterator([[[]]])).toEqual([]); + }); + + it("flattens [[1,2],[3,4],[5,6]] two levels deep", () => { + expect( + flattenNestedListIterator([ + [1, 2], + [3, 4], + [5, 6], + ]), + ).toEqual([1, 2, 3, 4, 5, 6]); + }); + + it("flattens a single integer wrapped at depth 4", () => { + expect(flattenNestedListIterator([[[[42]]]])).toEqual([42]); + }); + + it("preserves left-to-right order for mixed depth input", () => { + expect(flattenNestedListIterator([1, [2, [3, [4]]]])).toEqual([1, 2, 3, 4]); + }); + + it("handles a list with only arrays that contain single integers", () => { + expect(flattenNestedListIterator([[1], [2], [3]])).toEqual([1, 2, 3]); + }); + + it("flattens the LeetCode example [[1,1],2,[1,1]] to [1,1,2,1,1]", () => { + expect(flattenNestedListIterator([[1, 1], 2, [1, 1]])).toEqual([1, 1, 2, 1, 1]); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/index.ts b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/index.ts new file mode 100644 index 00000000..2ed33f56 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { flattenNestedListIterator } from "./sources/flatten-nested-list-iterator.ts?fn"; +import { generateFlattenNestedListIteratorSteps } from "./step-generator"; +import type { FlattenNestedListIteratorInput } from "./step-generator"; +import { flattenNestedListIteratorEducational } from "./educational"; + +import typescriptSource from "./sources/flatten-nested-list-iterator.ts?raw"; +import pythonSource from "./sources/flatten-nested-list-iterator.py?raw"; +import javaSource from "./sources/FlattenNestedListIterator.java?raw"; + +function executeFlattenNestedListIterator(input: FlattenNestedListIteratorInput): number[] { + return flattenNestedListIterator(input.nestedList) as number[]; +} + +const flattenNestedListIteratorDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.FLATTEN_NESTED_LIST_ITERATOR!, + name: "Flatten Nested List Iterator", + category: CATEGORY.STACKS_QUEUES!, + technique: "queue-applications", + description: + "Use a stack to flatten an arbitrarily nested list into a sequence of integers — pop the top item, collect it if it is a number, or push its elements in reverse order if it is an array", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(d)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nestedList: [[1, [2]], 3, [4, [5, 6]]] }, + }, + execute: executeFlattenNestedListIterator, + generateSteps: generateFlattenNestedListIteratorSteps, + educational: flattenNestedListIteratorEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(flattenNestedListIteratorDefinition); diff --git a/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/sources/FlattenNestedListIterator.java b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/sources/FlattenNestedListIterator.java new file mode 100644 index 00000000..e9be9de3 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/sources/FlattenNestedListIterator.java @@ -0,0 +1,28 @@ +// Flatten Nested List Iterator — use a stack to peel nested lists layer by layer +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +public class FlattenNestedListIterator { + public static List flattenNestedListIterator(List nestedList) { + Deque stack = new ArrayDeque<>(); // @step:initialize + for (int itemIdx = nestedList.size() - 1; itemIdx >= 0; itemIdx--) { + stack.push(nestedList.get(itemIdx)); // @step:initialize + } + List result = new ArrayList<>(); // @step:initialize + while (!stack.isEmpty()) { + Object top = stack.pop(); // @step:pop + if (top instanceof Integer) { + result.add((Integer) top); // @step:visit + } else { + @SuppressWarnings("unchecked") + List subList = (List) top; + for (int itemIdx = subList.size() - 1; itemIdx >= 0; itemIdx--) { + stack.push(subList.get(itemIdx)); // @step:push + } + } + } + return result; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/sources/flatten-nested-list-iterator.py b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/sources/flatten-nested-list-iterator.py new file mode 100644 index 00000000..6fdfc08c --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/sources/flatten-nested-list-iterator.py @@ -0,0 +1,13 @@ +# Flatten Nested List Iterator — use a stack to peel nested lists layer by layer + +def flatten_nested_list_iterator(nested_list): + stack = list(reversed(nested_list)) # @step:initialize + result = [] # @step:initialize + while stack: + top = stack.pop() # @step:pop + if isinstance(top, int): + result.append(top) # @step:visit + else: + for item in reversed(top): + stack.append(item) # @step:push + return result # @step:complete diff --git a/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/sources/flatten-nested-list-iterator.ts b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/sources/flatten-nested-list-iterator.ts new file mode 100644 index 00000000..f8e672f9 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/sources/flatten-nested-list-iterator.ts @@ -0,0 +1,18 @@ +// Flatten Nested List Iterator — use a stack to peel nested lists layer by layer +type NestedItem = number | NestedItem[]; + +function flattenNestedListIterator(nestedList: NestedItem[]): number[] { + const stack: NestedItem[] = [...nestedList].reverse(); // @step:initialize + const result: number[] = []; // @step:initialize + while (stack.length > 0) { + const top = stack.pop()!; // @step:pop + if (typeof top === "number") { + result.push(top); // @step:visit + } else { + for (let itemIdx = top.length - 1; itemIdx >= 0; itemIdx--) { + stack.push(top[itemIdx]!); // @step:push + } + } + } + return result; // @step:complete +} diff --git a/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/step-generator.test.ts b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/step-generator.test.ts new file mode 100644 index 00000000..d77f90cb --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/step-generator.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect } from "vitest"; +import { generateFlattenNestedListIteratorSteps } from "./step-generator"; + +const DEFAULT_INPUT = { + nestedList: [[1, [2]], 3, [4, [5, 6]]] as (number | (number | number[])[])[], +}; + +describe("generateFlattenNestedListIteratorSteps", () => { + it("produces steps for the default input", () => { + const steps = generateFlattenNestedListIteratorSteps(DEFAULT_INPUT); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateFlattenNestedListIteratorSteps(DEFAULT_INPUT); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateFlattenNestedListIteratorSteps(DEFAULT_INPUT); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateFlattenNestedListIteratorSteps(DEFAULT_INPUT); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateFlattenNestedListIteratorSteps(DEFAULT_INPUT); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits one enqueue step per integer in the flattened result", () => { + const steps = generateFlattenNestedListIteratorSteps(DEFAULT_INPUT); + const enqueueSteps = steps.filter((step) => step.type === "enqueue"); + // [[1,[2]],3,[4,[5,6]]] → [1,2,3,4,5,6] — 6 integers + expect(enqueueSteps.length).toBe(6); + }); + + it("emits a single complete step", () => { + const steps = generateFlattenNestedListIteratorSteps(DEFAULT_INPUT); + const completeSteps = steps.filter((step) => step.type === "complete"); + expect(completeSteps.length).toBe(1); + }); + + it("tracks queue operations in metrics", () => { + const steps = generateFlattenNestedListIteratorSteps(DEFAULT_INPUT); + const lastStep = steps[steps.length - 1]; + expect(lastStep?.metrics.queueOperations).toBeGreaterThan(0); + }); + + it("handles an empty input with only initialize and complete steps", () => { + const steps = generateFlattenNestedListIteratorSteps({ nestedList: [] }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + const enqueueSteps = steps.filter((step) => step.type === "enqueue"); + expect(enqueueSteps.length).toBe(0); + }); + + it("handles a flat list with one integer", () => { + const steps = generateFlattenNestedListIteratorSteps({ nestedList: [42] }); + const enqueueSteps = steps.filter((step) => step.type === "enqueue"); + expect(enqueueSteps.length).toBe(1); + }); + + it("variables in the complete step include result", () => { + const steps = generateFlattenNestedListIteratorSteps(DEFAULT_INPUT); + const completeStep = steps[steps.length - 1]; + expect(completeStep?.variables).toHaveProperty("result"); + }); + + it("complete step description contains the flattened values", () => { + const steps = generateFlattenNestedListIteratorSteps(DEFAULT_INPUT); + const completeStep = steps[steps.length - 1]; + expect(completeStep?.description).toContain("1"); + expect(completeStep?.description).toContain("6"); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/step-generator.ts b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/step-generator.ts new file mode 100644 index 00000000..db69a310 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/flatten-nested-list-iterator/step-generator.ts @@ -0,0 +1,78 @@ +/** Step generator for Flatten Nested List Iterator — produces ExecutionStep[] using QueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { QueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const FNLI_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.FLATTEN_NESTED_LIST_ITERATOR!); + +type NestedItem = number | NestedItem[]; + +export interface FlattenNestedListIteratorInput { + nestedList: NestedItem[]; +} + +function nestedItemLabel(item: NestedItem): string { + if (typeof item === "number") return String(item); + return "[" + item.map(nestedItemLabel).join(",") + "]"; +} + +export function generateFlattenNestedListIteratorSteps( + input: FlattenNestedListIteratorInput, +): ExecutionStep[] { + const { nestedList } = input; + const tracker = new QueueTracker([], FNLI_LINE_MAP); + const result: number[] = []; + + // Build initial stack in reverse so first element is on top + const stack: NestedItem[] = [...nestedList].reverse(); + + tracker.initialize({ + stack: stack.map(nestedItemLabel), + result: [], + }); + + // Push initial items onto the tracker stack (in reverse — bottom first so visual stack top matches) + for (let initIdx = stack.length - 1; initIdx >= 0; initIdx--) { + tracker.pushToStack( + nestedItemLabel(stack[initIdx]!), + { stack: stack.map(nestedItemLabel), result: [] }, + `Push ${nestedItemLabel(stack[initIdx]!)} onto the stack`, + ); + } + + while (stack.length > 0) { + const top = stack.pop()!; + const topLabel = nestedItemLabel(top); + + tracker.popFromStack( + { stack: stack.map(nestedItemLabel), result: [...result] }, + `Pop ${topLabel} from the stack`, + ); + + if (typeof top === "number") { + result.push(top); + tracker.enqueue( + String(top), + { stack: stack.map(nestedItemLabel), result: [...result] }, + `${topLabel} is a number — collect it into result`, + ); + } else { + // Push elements of the sub-array in reverse order + for (let itemIdx = top.length - 1; itemIdx >= 0; itemIdx--) { + const subItem = top[itemIdx]!; + stack.push(subItem); + tracker.pushToStack( + nestedItemLabel(subItem), + { stack: stack.map(nestedItemLabel), result: [...result] }, + `Expand array — push ${nestedItemLabel(subItem)} onto the stack`, + ); + } + } + } + + tracker.complete({ result: [...result] }, `Flattened to [${result.join(", ")}]`); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/GenerateBinaryNumbersPipeline.stories.tsx b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/GenerateBinaryNumbersPipeline.stories.tsx new file mode 100644 index 00000000..3e440576 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/GenerateBinaryNumbersPipeline.stories.tsx @@ -0,0 +1,56 @@ +/** + * Storybook stories for the Generate Binary Numbers algorithm pipeline. + * Uses the real step generator with count = 10, rendering the + * StackQueueVisualizer at key queue traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateBinaryNumbersSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateBinaryNumbersSteps({ count: 10 }); +const smallSteps = generateBinaryNumbersSteps({ count: 3 }); + +const meta: Meta = { + title: "Algorithm Pipelines/Generate Binary Numbers", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — queue seeded with '1', result empty */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing — several binary numbers generated, queue growing with children */ +export const MidProcessing: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Completed — all 10 binary numbers generated */ +export const Complete: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Small input — count = 3, shows '1', '10', '11' */ +export const SmallInput: Story = { + args: { + visualState: smallSteps[smallSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/educational.ts b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/educational.ts new file mode 100644 index 00000000..93eb9789 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/educational.ts @@ -0,0 +1,62 @@ +import type { EducationalContent } from "@/types"; + +export const generateBinaryNumbersEducational: EducationalContent = { + overview: + "**Generate Binary Numbers** produces the binary representations of all integers from 1 to N in order. Instead of converting each number individually, the algorithm exploits the structural relationship between binary strings: appending `'0'` or `'1'` to a binary string produces the next two numbers in the BFS-level traversal of the binary tree of representations.\n\nA queue drives the process — seed it with `'1'`, then repeatedly dequeue the front element, record it, and enqueue two children with `'0'` and `'1'` appended.", + + howItWorks: + "The algorithm treats binary strings as nodes in a full binary tree, visited level by level:\n\n" + + "1. **Initialize** — enqueue the string `'1'` as the root.\n" + + "2. **Repeat N times:**\n" + + " - **Dequeue** the front string — this is the next binary number in sequence.\n" + + " - **Record** it in the result array.\n" + + " - **Enqueue** `current + '0'` (left child).\n" + + " - **Enqueue** `current + '1'` (right child).\n" + + "3. **Return** the result array after N iterations.\n\n" + + "### Example trace for N = 5\n\n" + + "```\n" + + "Step Dequeue Enqueue Queue after\n" + + "0 '1' '10', '11' ['10', '11']\n" + + "1 '10' '100', '101' ['11', '100', '101']\n" + + "2 '11' '110', '111' ['100', '101', '110', '111']\n" + + "3 '100' '1000', '1001' ['101', '110', '111', '1000', '1001']\n" + + "4 '101' '1010', '1011' ['110', '111', '1000', '1001', '1010', '1011']\n" + + "Result: ['1', '10', '11', '100', '101']\n" + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each of the N iterations performs exactly one dequeue and two enqueues — all `O(1)` queue operations. The total work is linear in N.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The result array holds N strings. The queue holds at most `O(n)` strings at any point (the frontier of the binary tree at the current level), so space is also `O(n)` overall.", + + bestAndWorstCase: + "**Best case** — N = 1: a single dequeue and two enqueues, completing in `O(1)` effective work.\n\n" + + "**Worst case** — N is large: all N iterations run, with the queue growing as deeper binary tree levels are explored. Both time and space scale linearly at `O(n)`.\n\n" + + "There is no early-exit condition — the algorithm always runs exactly N iterations.", + + realWorldUses: [ + "**Huffman coding:** Binary prefix codes are generated by traversing a binary tree with the same left-child ('0') / right-child ('1') pattern.", + "**Binary counter circuits:** Hardware counters use the same structural rule — each bit pattern naturally follows from its parent by appending 0 or 1.", + "**BFS-order enumeration:** Any problem requiring level-by-level traversal of a binary structure (e.g., printing all N-bit strings in lexicographic order) uses the same queue pattern.", + "**Data encoding tests:** Generating sequential binary strings is useful for testing encoders, decoders, and binary protocol parsers against known inputs.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(n) time — no repeated string conversion or division operations.", + "Produces results in correct ascending numerical order naturally.", + "Simple and readable — the queue structure mirrors the binary tree conceptually.", + "Easily extended to generate binary strings of a fixed length by stopping at a given depth.", + ], + limitations: [ + "O(n) queue space — the frontier grows as deeper tree levels are reached.", + "Strings grow in length as N increases — memory per element is O(log n) bits.", + "For very large N, total memory for all strings is O(n log n) due to string lengths.", + "Not suitable when you need only one specific binary number — direct conversion is faster.", + ], + }, + + whenToUseIt: + "Use the queue-based BFS approach when you need all binary representations from 1 to N in order and want to avoid repeated integer division or bit manipulation. If you only need the binary representation of a single number, use the built-in `toString(2)` or equivalent. For generating all N-bit strings (fixed width), consider a simple counter with zero-padding instead.", +}; diff --git a/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/generate-binary-numbers.test.ts b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/generate-binary-numbers.test.ts new file mode 100644 index 00000000..60c9ccb6 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/generate-binary-numbers.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect } from "vitest"; +import { generateBinaryNumbers } from "./sources/generate-binary-numbers.ts?fn"; + +describe("generateBinaryNumbers", () => { + it("produces ['1','10','11','100','101'] for count = 5", () => { + expect(generateBinaryNumbers(5)).toEqual(["1", "10", "11", "100", "101"]); + }); + + it("produces ['1'] for count = 1", () => { + expect(generateBinaryNumbers(1)).toEqual(["1"]); + }); + + it("produces the correct first 3 binary numbers", () => { + expect(generateBinaryNumbers(3)).toEqual(["1", "10", "11"]); + }); + + it("produces the correct first 10 binary numbers", () => { + expect(generateBinaryNumbers(10)).toEqual([ + "1", + "10", + "11", + "100", + "101", + "110", + "111", + "1000", + "1001", + "1010", + ]); + }); + + it("returns an empty array for count = 0", () => { + expect(generateBinaryNumbers(0)).toEqual([]); + }); + + it("produces results in ascending numeric order", () => { + const binaryStrings = generateBinaryNumbers(7) as string[]; + for (let resultIdx = 0; resultIdx < binaryStrings.length; resultIdx++) { + expect(parseInt(binaryStrings[resultIdx]!, 2)).toBe(resultIdx + 1); + } + }); + + it("produces the correct number of results", () => { + const binaryStrings = generateBinaryNumbers(15) as string[]; + expect(binaryStrings.length).toBe(15); + }); + + it("each result is a valid binary string (only 0s and 1s)", () => { + const binaryStrings = generateBinaryNumbers(8) as string[]; + for (const binaryStr of binaryStrings) { + expect(binaryStr).toMatch(/^[01]+$/); + } + }); + + it("the last entry for count = 4 is '100'", () => { + const binaryStrings = generateBinaryNumbers(4) as string[]; + expect(binaryStrings[binaryStrings.length - 1]).toBe("100"); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/index.ts b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/index.ts new file mode 100644 index 00000000..1b5903dc --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { generateBinaryNumbers } from "./sources/generate-binary-numbers.ts?fn"; +import { generateBinaryNumbersSteps } from "./step-generator"; +import type { GenerateBinaryNumbersInput } from "./step-generator"; +import { generateBinaryNumbersEducational } from "./educational"; + +import typescriptSource from "./sources/generate-binary-numbers.ts?raw"; +import pythonSource from "./sources/generate-binary-numbers.py?raw"; +import javaSource from "./sources/GenerateBinaryNumbers.java?raw"; + +function executeGenerateBinaryNumbers(input: GenerateBinaryNumbersInput): string[] { + return generateBinaryNumbers(input.count) as string[]; +} + +const generateBinaryNumbersDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.GENERATE_BINARY_NUMBERS!, + name: "Generate Binary Numbers", + category: CATEGORY.STACKS_QUEUES!, + technique: "queue-applications", + description: + "Generate binary representations of 1 through N using a BFS-style queue — enqueue '1', then repeatedly dequeue and enqueue with '0' and '1' appended", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { count: 10 }, + }, + execute: executeGenerateBinaryNumbers, + generateSteps: generateBinaryNumbersSteps, + educational: generateBinaryNumbersEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(generateBinaryNumbersDefinition); diff --git a/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/sources/GenerateBinaryNumbers.java b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/sources/GenerateBinaryNumbers.java new file mode 100644 index 00000000..c29486f5 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/sources/GenerateBinaryNumbers.java @@ -0,0 +1,20 @@ +// Generate Binary Numbers — use a BFS-style queue to produce binary representations of 1 through N +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +public class GenerateBinaryNumbers { + public static List generateBinaryNumbers(int count) { + Deque queue = new ArrayDeque<>(); // @step:initialize + queue.add("1"); // @step:initialize + List result = new ArrayList<>(); // @step:initialize + for (int generationIdx = 0; generationIdx < count; generationIdx++) { + String current = queue.poll(); // @step:dequeue + result.add(current); // @step:dequeue + queue.add(current + "0"); // @step:enqueue + queue.add(current + "1"); // @step:enqueue + } + return result; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/sources/generate-binary-numbers.py b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/sources/generate-binary-numbers.py new file mode 100644 index 00000000..9d0bd145 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/sources/generate-binary-numbers.py @@ -0,0 +1,12 @@ +# Generate Binary Numbers — use a BFS-style queue to produce binary representations of 1 through N +from collections import deque + +def generate_binary_numbers(count): + queue = deque(["1"]) # @step:initialize + result = [] # @step:initialize + for _ in range(count): + current = queue.popleft() # @step:dequeue + result.append(current) # @step:dequeue + queue.append(current + "0") # @step:enqueue + queue.append(current + "1") # @step:enqueue + return result # @step:complete diff --git a/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/sources/generate-binary-numbers.ts b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/sources/generate-binary-numbers.ts new file mode 100644 index 00000000..266356b7 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/sources/generate-binary-numbers.ts @@ -0,0 +1,12 @@ +// Generate Binary Numbers — use a BFS-style queue to produce binary representations of 1 through N +function generateBinaryNumbers(count: number): string[] { + const queue: string[] = ["1"]; // @step:initialize + const result: string[] = []; // @step:initialize + for (let generationIdx = 0; generationIdx < count; generationIdx++) { + const current = queue.shift()!; // @step:dequeue + result.push(current); // @step:dequeue + queue.push(current + "0"); // @step:enqueue + queue.push(current + "1"); // @step:enqueue + } + return result; // @step:complete +} diff --git a/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/step-generator.test.ts b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/step-generator.test.ts new file mode 100644 index 00000000..c133ac08 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/step-generator.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect } from "vitest"; +import { generateBinaryNumbersSteps } from "./step-generator"; + +const DEFAULT_INPUT = { count: 5 }; + +describe("generateBinaryNumbersSteps", () => { + it("produces steps for the default input", () => { + const steps = generateBinaryNumbersSteps(DEFAULT_INPUT); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateBinaryNumbersSteps(DEFAULT_INPUT); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateBinaryNumbersSteps(DEFAULT_INPUT); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateBinaryNumbersSteps(DEFAULT_INPUT); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateBinaryNumbersSteps(DEFAULT_INPUT); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits one dequeue step per count iteration", () => { + const steps = generateBinaryNumbersSteps(DEFAULT_INPUT); + const dequeueSteps = steps.filter((step) => step.type === "dequeue"); + expect(dequeueSteps.length).toBe(DEFAULT_INPUT.count); + }); + + it("emits two enqueue steps per count iteration plus the initial seed enqueue", () => { + const steps = generateBinaryNumbersSteps(DEFAULT_INPUT); + const enqueueSteps = steps.filter((step) => step.type === "enqueue"); + // 1 seed + (count * 2) child enqueues + expect(enqueueSteps.length).toBe(1 + DEFAULT_INPUT.count * 2); + }); + + it("emits a single complete step", () => { + const steps = generateBinaryNumbersSteps(DEFAULT_INPUT); + const completeSteps = steps.filter((step) => step.type === "complete"); + expect(completeSteps.length).toBe(1); + }); + + it("tracks queue operations in metrics", () => { + const steps = generateBinaryNumbersSteps(DEFAULT_INPUT); + const lastStep = steps[steps.length - 1]; + expect(lastStep?.metrics.queueOperations).toBeGreaterThan(0); + }); + + it("handles count = 1 with a single dequeue step", () => { + const steps = generateBinaryNumbersSteps({ count: 1 }); + const dequeueSteps = steps.filter((step) => step.type === "dequeue"); + expect(dequeueSteps.length).toBe(1); + }); + + it("handles count = 0 with only initialize and complete steps", () => { + const steps = generateBinaryNumbersSteps({ count: 0 }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + const dequeueSteps = steps.filter((step) => step.type === "dequeue"); + expect(dequeueSteps.length).toBe(0); + }); + + it("variables in the complete step include count and result", () => { + const steps = generateBinaryNumbersSteps(DEFAULT_INPUT); + const completeStep = steps[steps.length - 1]; + expect(completeStep?.variables).toHaveProperty("count"); + expect(completeStep?.variables).toHaveProperty("result"); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/step-generator.ts b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/step-generator.ts new file mode 100644 index 00000000..4b4c6c9e --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/generate-binary-numbers/step-generator.ts @@ -0,0 +1,67 @@ +/** Step generator for Generate Binary Numbers — produces ExecutionStep[] using QueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { QueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const GBN_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.GENERATE_BINARY_NUMBERS!); + +export interface GenerateBinaryNumbersInput { + count: number; +} + +export function generateBinaryNumbersSteps(input: GenerateBinaryNumbersInput): ExecutionStep[] { + const { count } = input; + const tracker = new QueueTracker([], GBN_LINE_MAP); + const result: string[] = []; + + tracker.initialize({ + count, + queue: ["1"], + result: [], + }); + + // Seed the queue with "1" to match the algorithm's initialization + tracker.enqueue("1", { count, queue: ["1"], result: [] }, "Initialize queue with '1'"); + + for (let generationIdx = 0; generationIdx < count; generationIdx++) { + const current = tracker.dequeue( + { + generationIdx, + result: [...result], + queueSize: count - generationIdx, + }, + `Dequeue '${String(generationIdx + 1)}' — process next binary number`, + ); + + result.push(current); + + tracker.enqueue( + current + "0", + { + generationIdx, + current, + result: [...result], + }, + `Enqueue '${current + "0"}' — append '0' to generate next level`, + ); + + tracker.enqueue( + current + "1", + { + generationIdx, + current, + result: [...result], + }, + `Enqueue '${current + "1"}' — append '1' to generate next level`, + ); + } + + tracker.complete( + { count, result: [...result] }, + `Generated ${String(count)} binary number${count === 1 ? "" : "s"}: ${result.slice(0, 5).join(", ")}${result.length > 5 ? "..." : ""}`, + ); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/InterleaveFirstHalfQueuePipeline.stories.tsx b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/InterleaveFirstHalfQueuePipeline.stories.tsx new file mode 100644 index 00000000..e8f22f48 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/InterleaveFirstHalfQueuePipeline.stories.tsx @@ -0,0 +1,56 @@ +/** + * Storybook stories for the Interleave First Half Queue algorithm pipeline. + * Uses the real step generator with [1, 2, 3, 4, 5, 6], rendering the + * StackQueueVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateInterleaveFirstHalfQueueSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateInterleaveFirstHalfQueueSteps({ values: [1, 2, 3, 4, 5, 6] }); +const smallSteps = generateInterleaveFirstHalfQueueSteps({ values: [1, 2, 3, 4] }); + +const meta: Meta = { + title: "Algorithm Pipelines/Interleave First Half Queue", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — full queue populated, stack empty */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing — first half moved to stack, queue holds second half */ +export const MidProcessing: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Completed — interleaved result [1,4,2,5,3,6] produced */ +export const Complete: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Small input — [1,2,3,4] interleaved into [1,3,2,4] */ +export const SmallInput: Story = { + args: { + visualState: smallSteps[smallSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/educational.ts b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/educational.ts new file mode 100644 index 00000000..e06577ee --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/educational.ts @@ -0,0 +1,56 @@ +import type { EducationalContent } from "@/types"; + +export const interleaveFirstHalfQueueEducational: EducationalContent = { + overview: + "**Interleave First Half Queue** reorders a queue by weaving the first half and second half together — element by element. Given `[1, 2, 3, 4, 5, 6]`, the result is `[1, 4, 2, 5, 3, 6]`: position 0 from the first half, position 0 from the second half, position 1 from the first half, position 1 from the second half, and so on.\n\nThe trick is that only a queue and a stack are available — no random access. The algorithm routes elements through the stack to preserve order while using the queue's FIFO structure to sequence the interleave.", + + howItWorks: + "The algorithm runs in five phases, each using only queue and stack operations:\n\n" + + "1. **Move first half to stack** — dequeue the first `n/2` elements and push them onto a stack. The stack reverses the order.\n" + + "2. **Restore reversed first half to queue rear** — pop all stack elements and enqueue them at the rear. The queue now holds: `[second half | reversed first half]`.\n" + + "3. **Rotate second half to rear** — dequeue and re-enqueue the first `n/2` elements (which are the original second half). The queue becomes: `[original first half (reversed) | original second half]`.\n" + + "4. **Move first half (reversed) to stack again** — dequeue the first `n/2` elements (the reversed first half) and push them onto the stack. The stack now holds first-half elements in original order (top = first element).\n" + + "5. **Interleave** — alternately pop from the stack (first-half elements in order) and dequeue from the queue (second-half elements in order) to produce the final interleaved sequence.\n\n" + + "### Example trace on `[1, 2, 3, 4, 5, 6]`\n\n" + + "```\n" + + "After phase 1: queue=[4,5,6] stack=[1,2,3] (top=3)\n" + + "After phase 2: queue=[4,5,6,3,2,1] stack=[]\n" + + "After phase 3: queue=[3,2,1,4,5,6] stack=[]\n" + + "After phase 4: queue=[4,5,6] stack=[3,2,1] (top=1)\n" + + "After phase 5: result=[1,4,2,5,3,6]\n" + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each element is touched a constant number of times across the five phases — each phase iterates over at most `n` elements. Total work is linear.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The auxiliary stack holds up to `n/2` elements at a time. Combined with the input queue, total extra space is `O(n)`.", + + bestAndWorstCase: + "**Best case** — an empty input or a single element: no phases execute meaningful work and the result is returned immediately in `O(1)`.\n\n" + + "**Worst case** — any non-empty input: all five phases execute, each touching every element once, yielding `O(n)` time. There is no early termination possible.\n\n" + + "Both odd-length and even-length inputs are handled correctly — an odd-length queue leaves one element in the queue after the interleave loop, which is appended at the end.", + + realWorldUses: [ + "**Printer spoolers:** Interleaving jobs from two queues (e.g. color and black-and-white) for alternating output without random access.", + "**Network packet scheduling:** Merging two ordered packet streams into a single alternating sequence for fair bandwidth sharing.", + "**Shuffle algorithms:** Creating a riffle-shuffle effect on ordered sequences using only stack and queue primitives.", + "**Embedded systems:** Situations where only LIFO and FIFO buffers are available in hardware and array indexing is not permitted.", + ], + + strengthsAndLimitations: { + strengths: [ + "Uses only O(n/2) extra space for the stack — no full copy of the array required.", + "Works within strict data-structure constraints (no random access needed).", + "Each phase has a clear, single responsibility that is easy to reason about and test.", + ], + limitations: [ + "Requires five distinct passes over the data — constant factor is higher than a direct index-based interleave.", + "Only correct for even-length inputs when a strict interleave is expected; odd-length leaves one element appended at the end.", + "The multi-phase approach makes it harder to parallelize compared to direct index manipulation.", + ], + }, + + whenToUseIt: + "Use this algorithm when you must interleave two halves of a sequence and are constrained to stack and queue operations — for example, when modelling restricted memory architectures or when teaching data-structure composition. If you have direct array access, a simple two-pointer in-place swap is simpler and faster in practice.", +}; diff --git a/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/index.ts b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/index.ts new file mode 100644 index 00000000..5f7d93cb --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { interleaveFirstHalfQueue } from "./sources/interleave-first-half-queue.ts?fn"; +import { generateInterleaveFirstHalfQueueSteps } from "./step-generator"; +import type { InterleaveFirstHalfQueueInput } from "./step-generator"; +import { interleaveFirstHalfQueueEducational } from "./educational"; + +import typescriptSource from "./sources/interleave-first-half-queue.ts?raw"; +import pythonSource from "./sources/interleave-first-half-queue.py?raw"; +import javaSource from "./sources/InterleaveFirstHalfQueue.java?raw"; + +function executeInterleaveFirstHalfQueue(input: InterleaveFirstHalfQueueInput): number[] { + return interleaveFirstHalfQueue(input.values) as number[]; +} + +const interleaveFirstHalfQueueDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.INTERLEAVE_FIRST_HALF_QUEUE!, + name: "Interleave First Half Queue", + category: CATEGORY.STACKS_QUEUES!, + technique: "queue-applications", + description: + "Interleave the first half of a queue with the second half using a stack — [1,2,3,4,5,6] becomes [1,4,2,5,3,6]", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { values: [1, 2, 3, 4, 5, 6] }, + }, + execute: executeInterleaveFirstHalfQueue, + generateSteps: generateInterleaveFirstHalfQueueSteps, + educational: interleaveFirstHalfQueueEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(interleaveFirstHalfQueueDefinition); diff --git a/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/interleave-first-half-queue.test.ts b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/interleave-first-half-queue.test.ts new file mode 100644 index 00000000..db8acf8f --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/interleave-first-half-queue.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from "vitest"; +import { interleaveFirstHalfQueue } from "./sources/interleave-first-half-queue.ts?fn"; + +describe("interleaveFirstHalfQueue", () => { + it("interleaves [1,2,3,4,5,6] into [1,4,2,5,3,6]", () => { + expect(interleaveFirstHalfQueue([1, 2, 3, 4, 5, 6])).toEqual([1, 4, 2, 5, 3, 6]); + }); + + it("interleaves a 4-element queue correctly", () => { + expect(interleaveFirstHalfQueue([1, 2, 3, 4])).toEqual([1, 3, 2, 4]); + }); + + it("interleaves a 2-element queue correctly", () => { + expect(interleaveFirstHalfQueue([1, 2])).toEqual([1, 2]); + }); + + it("handles a single-element queue", () => { + expect(interleaveFirstHalfQueue([42])).toEqual([42]); + }); + + it("handles an empty queue", () => { + expect(interleaveFirstHalfQueue([])).toEqual([]); + }); + + it("interleaves an odd-length queue — last element appended at end", () => { + // halfSize=2; phase1: stack=[1,2],queue=[3,4,5]; phase2: queue=[3,4,5,2,1]; + // phase3 rotate 2: queue=[5,2,1,3,4]; phase4: stack=[5,2],queue=[1,3,4]; + // phase5: pop2+shift1=[2,1], pop5+shift3=[2,1,5,3]; remainder shift4=[2,1,5,3,4] + expect(interleaveFirstHalfQueue([1, 2, 3, 4, 5])).toEqual([2, 1, 5, 3, 4]); + }); + + it("interleaves an 8-element queue correctly", () => { + expect(interleaveFirstHalfQueue([1, 2, 3, 4, 5, 6, 7, 8])).toEqual([1, 5, 2, 6, 3, 7, 4, 8]); + }); + + it("produces the correct output length for even-length input", () => { + const result = interleaveFirstHalfQueue([10, 20, 30, 40]) as number[]; + expect(result.length).toBe(4); + }); + + it("produces the correct output length for odd-length input", () => { + const result = interleaveFirstHalfQueue([10, 20, 30, 40, 50]) as number[]; + expect(result.length).toBe(5); + }); + + it("alternates between first-half and second-half elements for even input", () => { + const inputValues = [1, 2, 3, 4, 5, 6]; + const result = interleaveFirstHalfQueue(inputValues) as number[]; + const halfSize = inputValues.length / 2; + const firstHalf = inputValues.slice(0, halfSize); + const secondHalf = inputValues.slice(halfSize); + for (let pairIdx = 0; pairIdx < halfSize; pairIdx++) { + expect(result[pairIdx * 2]).toBe(firstHalf[pairIdx]); + expect(result[pairIdx * 2 + 1]).toBe(secondHalf[pairIdx]); + } + }); +}); diff --git a/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/sources/InterleaveFirstHalfQueue.java b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/sources/InterleaveFirstHalfQueue.java new file mode 100644 index 00000000..c72d55e7 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/sources/InterleaveFirstHalfQueue.java @@ -0,0 +1,46 @@ +// Interleave First Half Queue — interleave the first half of a queue with the second half using a stack +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +public class InterleaveFirstHalfQueue { + public static List interleaveFirstHalfQueue(int[] values) { + Deque queue = new ArrayDeque<>(); // @step:initialize + for (int value : values) queue.add(value); // @step:initialize + int halfSize = values.length / 2; // @step:initialize + Deque stack = new ArrayDeque<>(); // @step:initialize + + // Step 1: Dequeue first half into stack + for (int fillIdx = 0; fillIdx < halfSize; fillIdx++) { + stack.push(queue.poll()); // @step:push + } + + // Step 2: Enqueue stack elements back to queue (reverses first half) + while (!stack.isEmpty()) { + queue.add(stack.pop()); // @step:enqueue + } + + // Step 3: Dequeue second half and enqueue back (move original second half to rear) + for (int rotateIdx = 0; rotateIdx < halfSize; rotateIdx++) { + queue.add(queue.poll()); // @step:transfer + } + + // Step 4: Dequeue first half (now at front) into stack + for (int refillIdx = 0; refillIdx < halfSize; refillIdx++) { + stack.push(queue.poll()); // @step:push + } + + // Step 5: Interleave — alternately pop from stack and dequeue from queue + List result = new ArrayList<>(); // @step:initialize + while (!stack.isEmpty()) { + result.add(stack.pop()); // @step:pop + result.add(queue.poll()); // @step:dequeue + } + if (!queue.isEmpty()) { + result.add(queue.poll()); // @step:dequeue + } + + return result; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/sources/interleave-first-half-queue.py b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/sources/interleave-first-half-queue.py new file mode 100644 index 00000000..0ca165da --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/sources/interleave-first-half-queue.py @@ -0,0 +1,33 @@ +# Interleave First Half Queue — interleave the first half of a queue with the second half using a stack +from collections import deque + +def interleave_first_half_queue(values): + queue = deque(values) # @step:initialize + half_size = len(values) // 2 # @step:initialize + stack = [] # @step:initialize + + # Step 1: Dequeue first half into stack + for _ in range(half_size): + stack.append(queue.popleft()) # @step:push + + # Step 2: Enqueue stack elements back to queue (reverses first half) + while stack: + queue.append(stack.pop()) # @step:enqueue + + # Step 3: Dequeue second half and enqueue back (move original second half to rear) + for _ in range(half_size): + queue.append(queue.popleft()) # @step:transfer + + # Step 4: Dequeue first half (now at front) into stack + for _ in range(half_size): + stack.append(queue.popleft()) # @step:push + + # Step 5: Interleave — alternately pop from stack and dequeue from queue + result = [] # @step:initialize + while stack: + result.append(stack.pop()) # @step:pop + result.append(queue.popleft()) # @step:dequeue + if queue: + result.append(queue.popleft()) # @step:dequeue + + return result # @step:complete diff --git a/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/sources/interleave-first-half-queue.ts b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/sources/interleave-first-half-queue.ts new file mode 100644 index 00000000..ac300f6a --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/sources/interleave-first-half-queue.ts @@ -0,0 +1,38 @@ +// Interleave First Half Queue — interleave the first half of a queue with the second half using a stack +function interleaveFirstHalfQueue(values: number[]): number[] { + const queue: number[] = [...values]; // @step:initialize + const halfSize = Math.floor(values.length / 2); // @step:initialize + const stack: number[] = []; // @step:initialize + + // Step 1: Dequeue first half into stack + for (let fillIdx = 0; fillIdx < halfSize; fillIdx++) { + stack.push(queue.shift()!); // @step:push + } + + // Step 2: Enqueue stack elements back to queue (reverses first half) + while (stack.length > 0) { + queue.push(stack.pop()!); // @step:enqueue + } + + // Step 3: Dequeue second half and enqueue back (move original second half to rear) + for (let rotateIdx = 0; rotateIdx < halfSize; rotateIdx++) { + queue.push(queue.shift()!); // @step:transfer + } + + // Step 4: Dequeue first half (originally first half, now at front) into stack + for (let refillIdx = 0; refillIdx < halfSize; refillIdx++) { + stack.push(queue.shift()!); // @step:push + } + + // Step 5: Interleave — alternately pop from stack and dequeue from queue + const result: number[] = []; // @step:initialize + while (stack.length > 0) { + result.push(stack.pop()!); // @step:pop + result.push(queue.shift()!); // @step:dequeue + } + if (queue.length > 0) { + result.push(queue.shift()!); // @step:dequeue + } + + return result; // @step:complete +} diff --git a/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/step-generator.test.ts b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/step-generator.test.ts new file mode 100644 index 00000000..0b8ed417 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/step-generator.test.ts @@ -0,0 +1,89 @@ +import { describe, it, expect } from "vitest"; +import { generateInterleaveFirstHalfQueueSteps } from "./step-generator"; + +const DEFAULT_INPUT = { values: [1, 2, 3, 4, 5, 6] }; + +describe("generateInterleaveFirstHalfQueueSteps", () => { + it("produces steps for the default input", () => { + const steps = generateInterleaveFirstHalfQueueSteps(DEFAULT_INPUT); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateInterleaveFirstHalfQueueSteps(DEFAULT_INPUT); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateInterleaveFirstHalfQueueSteps(DEFAULT_INPUT); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateInterleaveFirstHalfQueueSteps(DEFAULT_INPUT); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateInterleaveFirstHalfQueueSteps(DEFAULT_INPUT); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits push steps for phase 1 and phase 4 (halfSize each)", () => { + const steps = generateInterleaveFirstHalfQueueSteps(DEFAULT_INPUT); + const pushSteps = steps.filter((step) => step.type === "push"); + // halfSize = 3 pushes in phase 1, 3 pushes in phase 4 + expect(pushSteps.length).toBe(6); + }); + + it("emits pop steps during the interleave phase", () => { + const steps = generateInterleaveFirstHalfQueueSteps(DEFAULT_INPUT); + const popSteps = steps.filter((step) => step.type === "pop"); + // Phase 2 emits 3 popFromStack steps (reversing first half back to queue) + // Phase 5 emits 3 popFromStack steps (interleave) — total = 6 + expect(popSteps.length).toBe(6); + }); + + it("emits a single complete step", () => { + const steps = generateInterleaveFirstHalfQueueSteps(DEFAULT_INPUT); + const completeSteps = steps.filter((step) => step.type === "complete"); + expect(completeSteps.length).toBe(1); + }); + + it("tracks queue operations in metrics", () => { + const steps = generateInterleaveFirstHalfQueueSteps(DEFAULT_INPUT); + const lastStep = steps[steps.length - 1]; + expect(lastStep?.metrics.queueOperations).toBeGreaterThan(0); + }); + + it("handles a single-element input", () => { + const steps = generateInterleaveFirstHalfQueueSteps({ values: [7] }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("handles an empty input", () => { + const steps = generateInterleaveFirstHalfQueueSteps({ values: [] }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("complete step variables include values and result", () => { + const steps = generateInterleaveFirstHalfQueueSteps(DEFAULT_INPUT); + const completeStep = steps[steps.length - 1]; + expect(completeStep?.variables).toHaveProperty("values"); + expect(completeStep?.variables).toHaveProperty("result"); + }); + + it("phase transitions are reflected in visual state phase field", () => { + const steps = generateInterleaveFirstHalfQueueSteps(DEFAULT_INPUT); + const phaseValues = steps + .map((step) => (step.visualState as { phase?: string }).phase) + .filter(Boolean); + expect(phaseValues.length).toBeGreaterThan(0); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/step-generator.ts b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/step-generator.ts new file mode 100644 index 00000000..321b3f05 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-applications/interleave-first-half-queue/step-generator.ts @@ -0,0 +1,134 @@ +/** Step generator for Interleave First Half Queue — produces ExecutionStep[] using QueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { QueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const IFHQ_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.INTERLEAVE_FIRST_HALF_QUEUE!); + +export interface InterleaveFirstHalfQueueInput { + values: number[]; +} + +export function generateInterleaveFirstHalfQueueSteps( + input: InterleaveFirstHalfQueueInput, +): ExecutionStep[] { + const { values } = input; + const tracker = new QueueTracker(values, IFHQ_LINE_MAP); + + const halfSize = Math.floor(values.length / 2); + + // Mirror the algorithm state + const queue: number[] = [...values]; + const stack: number[] = []; + + tracker.initialize({ + values, + halfSize, + queue: [...queue], + stack: [], + }); + + // Populate tracker queue to match algorithm start + for (const value of values) { + tracker.enqueue( + String(value), + { values, halfSize, queue: [...queue], stack: [] }, + `Enqueue ${String(value)} — populate initial queue`, + ); + } + + // Phase 1: Dequeue first half into stack + tracker.setPhase("Phase 1: Move first half to stack"); + for (let fillIdx = 0; fillIdx < halfSize; fillIdx++) { + const value = queue.shift()!; + stack.push(value); + tracker.dequeue( + { fillIdx, queue: [...queue], stack: [...stack] }, + `Dequeue ${String(value)} — move element ${fillIdx + 1} of ${halfSize} to stack`, + ); + tracker.pushToStack( + String(value), + { fillIdx, queue: [...queue], stack: [...stack] }, + `Push ${String(value)} onto stack`, + ); + } + + // Phase 2: Enqueue stack back to queue (reverses first half) + tracker.setPhase("Phase 2: Enqueue stack back to queue"); + while (stack.length > 0) { + const value = stack.pop()!; + queue.push(value); + tracker.popFromStack( + { queue: [...queue], stack: [...stack] }, + `Pop ${String(value)} from stack`, + ); + tracker.enqueue( + String(value), + { queue: [...queue], stack: [...stack] }, + `Enqueue ${String(value)} — reversed first half element appended to queue`, + ); + } + + // Phase 3: Rotate second half to rear + tracker.setPhase("Phase 3: Rotate second half to rear"); + for (let rotateIdx = 0; rotateIdx < halfSize; rotateIdx++) { + const value = queue.shift()!; + queue.push(value); + tracker.transfer( + "queue", + "queue", + { rotateIdx, queue: [...queue] }, + `Move ${String(value)} from front to rear — shift second half element ${rotateIdx + 1} of ${halfSize}`, + ); + } + + // Phase 4: Dequeue first half (now at front) into stack + tracker.setPhase("Phase 4: Move reordered first half to stack"); + for (let refillIdx = 0; refillIdx < halfSize; refillIdx++) { + const value = queue.shift()!; + stack.push(value); + tracker.dequeue( + { refillIdx, queue: [...queue], stack: [...stack] }, + `Dequeue ${String(value)} — fill stack with first half element ${refillIdx + 1} of ${halfSize}`, + ); + tracker.pushToStack( + String(value), + { refillIdx, queue: [...queue], stack: [...stack] }, + `Push ${String(value)} onto stack`, + ); + } + + // Phase 5: Interleave + tracker.setPhase("Phase 5: Interleave stack and queue"); + const result: number[] = []; + while (stack.length > 0) { + const fromStack = stack.pop()!; + result.push(fromStack); + tracker.popFromStack( + { queue: [...queue], stack: [...stack], result: [...result] }, + `Pop ${String(fromStack)} from stack — first-half element`, + ); + + const fromQueue = queue.shift()!; + result.push(fromQueue); + tracker.dequeue( + { queue: [...queue], stack: [...stack], result: [...result] }, + `Dequeue ${String(fromQueue)} from queue — second-half element`, + ); + } + + if (queue.length > 0) { + const remaining = queue.shift()!; + result.push(remaining); + tracker.dequeue( + { queue: [...queue], result: [...result] }, + `Dequeue ${String(remaining)} — odd-length remainder`, + ); + } + + tracker.complete({ values, result: [...result] }, `Interleaved result: [${result.join(", ")}]`); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-deque/DesignCircularDequePipeline.stories.tsx b/src/algorithms/stacks-queues/queue-design/design-circular-deque/DesignCircularDequePipeline.stories.tsx new file mode 100644 index 00000000..773c324b --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-deque/DesignCircularDequePipeline.stories.tsx @@ -0,0 +1,112 @@ +/** + * Storybook stories for the Design Circular Deque algorithm pipeline. + * Uses the real step generator with the default input, rendering the + * StackQueueVisualizer at key circular buffer states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateDesignCircularDequeSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateDesignCircularDequeSteps({ + operations: ["pushBack 1", "pushFront 2", "popBack", "pushBack 3"], + capacity: 3, +}); + +const wrapAroundSteps = generateDesignCircularDequeSteps({ + operations: ["pushBack 1", "pushBack 2", "pushBack 3", "popFront", "pushFront 0"], + capacity: 3, +}); + +const fullDequeSteps = generateDesignCircularDequeSteps({ + operations: ["pushBack 10", "pushBack 20", "pushBack 30", "pushFront 5"], + capacity: 3, +}); + +const bothEndsSteps = generateDesignCircularDequeSteps({ + operations: ["pushBack 1", "pushFront 2", "pushBack 3", "pushFront 4", "popFront", "popBack"], + capacity: 4, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Design Circular Deque", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — empty ring buffer with capacity 3 */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** After first pushBack — front and rear both point to slot 0 */ +export const FirstPushBack: Story = { + args: { + visualState: (() => { + const enqueueSteps = defaultSteps.filter((step) => step.type === "enqueue"); + return enqueueSteps[0]!.visualState as StackQueueVisualState; + })(), + }, +}; + +/** After pushFront — front pointer retreats to the preceding slot */ +export const AfterPushFront: Story = { + args: { + visualState: (() => { + const enqueueFrontSteps = defaultSteps.filter((step) => step.type === "enqueue-front"); + return enqueueFrontSteps[0]!.visualState as StackQueueVisualState; + })(), + }, +}; + +/** After popBack — rear pointer retreats, element removed from the rear */ +export const AfterPopBack: Story = { + args: { + visualState: (() => { + const dequeueRearSteps = defaultSteps.filter((step) => step.type === "dequeue-rear"); + return dequeueRearSteps[0]!.visualState as StackQueueVisualState; + })(), + }, +}; + +/** Complete — all operations processed */ +export const AllOperationsComplete: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Wrap-around — front pointer wraps to the last slot after pushFront fills the gap */ +export const WrapAround: Story = { + args: { + visualState: wrapAroundSteps[wrapAroundSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Full deque — pushFront rejected because size equals capacity */ +export const DequeFull: Story = { + args: { + visualState: (() => { + const peekSteps = fullDequeSteps.filter((step) => step.type === "peek"); + return peekSteps[0]!.visualState as StackQueueVisualState; + })(), + }, +}; + +/** Both ends — interleaved pushFront/pushBack then popFront/popBack */ +export const BothEndsInterleaved: Story = { + args: { + visualState: bothEndsSteps[bothEndsSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-deque/design-circular-deque.test.ts b/src/algorithms/stacks-queues/queue-design/design-circular-deque/design-circular-deque.test.ts new file mode 100644 index 00000000..4836b9b8 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-deque/design-circular-deque.test.ts @@ -0,0 +1,130 @@ +import { describe, it, expect } from "vitest"; +import { designCircularDeque } from "./sources/design-circular-deque.ts?fn"; + +describe("designCircularDeque", () => { + it("pushBack values and returns true for each successful push", () => { + const result = designCircularDeque(["pushBack 1", "pushBack 2", "pushBack 3"], 3); + expect(result).toEqual(["true", "true", "true"]); + }); + + it("returns full when pushing beyond capacity", () => { + const result = designCircularDeque(["pushBack 1", "pushBack 2", "pushBack 3", "pushBack 4"], 3); + expect(result).toEqual(["true", "true", "true", "full"]); + }); + + it("returns empty when popping from an empty deque", () => { + const result = designCircularDeque(["popFront"], 3); + expect(result).toEqual(["empty"]); + }); + + it("returns empty when popping back from an empty deque", () => { + const result = designCircularDeque(["popBack"], 3); + expect(result).toEqual(["empty"]); + }); + + it("popFront removes elements in FIFO order for pushBack operations", () => { + const result = designCircularDeque( + ["pushBack 1", "pushBack 2", "pushBack 3", "popFront", "popFront", "popFront"], + 3, + ); + expect(result).toEqual(["true", "true", "true", "1", "2", "3"]); + }); + + it("pushFront inserts at the front so popFront retrieves in LIFO order", () => { + const result = designCircularDeque( + ["pushFront 1", "pushFront 2", "pushFront 3", "popFront", "popFront", "popFront"], + 3, + ); + expect(result).toEqual(["true", "true", "true", "3", "2", "1"]); + }); + + it("popBack removes the most recently pushed back element", () => { + const result = designCircularDeque(["pushBack 10", "pushBack 20", "popBack"], 3); + expect(result).toEqual(["true", "true", "20"]); + }); + + it("handles the default input correctly", () => { + const result = designCircularDeque(["pushBack 1", "pushFront 2", "popBack", "pushBack 3"], 3); + expect(result).toEqual(["true", "true", "1", "true"]); + }); + + it("wraps rear pointer around using modular arithmetic on pushBack", () => { + const result = designCircularDeque( + ["pushBack 1", "pushBack 2", "popFront", "pushBack 3", "pushBack 4"], + 3, + ); + expect(result).toEqual(["true", "true", "1", "true", "true"]); + }); + + it("wraps front pointer around using modular arithmetic on pushFront", () => { + const result = designCircularDeque(["pushBack 1", "pushBack 2", "pushFront 0"], 3); + expect(result).toEqual(["true", "true", "true"]); + }); + + it("resets front and rear to -1 after removing the last element via popFront", () => { + const result = designCircularDeque(["pushBack 5", "popFront", "pushBack 7"], 2); + expect(result).toEqual(["true", "5", "true"]); + }); + + it("resets front and rear to -1 after removing the last element via popBack", () => { + const result = designCircularDeque(["pushBack 5", "popBack", "pushBack 7"], 2); + expect(result).toEqual(["true", "5", "true"]); + }); + + it("peeks the front value correctly", () => { + const result = designCircularDeque(["pushBack 10", "pushBack 20", "peekFront"], 3); + expect(result).toEqual(["true", "true", "10"]); + }); + + it("peeks the rear value correctly", () => { + const result = designCircularDeque(["pushBack 10", "pushBack 20", "peekRear"], 3); + expect(result).toEqual(["true", "true", "20"]); + }); + + it("returns empty for peekFront and peekRear on an empty deque", () => { + const result = designCircularDeque(["peekFront", "peekRear"], 3); + expect(result).toEqual(["empty", "empty"]); + }); + + it("handles a capacity-1 deque with pushBack and popFront", () => { + const result = designCircularDeque(["pushBack 42", "popFront", "pushBack 99", "popFront"], 1); + expect(result).toEqual(["true", "42", "true", "99"]); + }); + + it("handles a capacity-1 deque with pushFront and popBack", () => { + const result = designCircularDeque(["pushFront 7", "popBack", "pushFront 8", "popBack"], 1); + expect(result).toEqual(["true", "7", "true", "8"]); + }); + + it("rejects pushFront when full", () => { + const result = designCircularDeque(["pushBack 1", "pushBack 2", "pushFront 0"], 2); + expect(result).toEqual(["true", "true", "full"]); + }); + + it("interleaves pushFront and pushBack correctly", () => { + const result = designCircularDeque(["pushBack 1", "pushFront 2", "peekFront", "peekRear"], 3); + expect(result).toEqual(["true", "true", "2", "1"]); + }); + + it("handles alternating pushBack and popFront across wrap-around", () => { + const result = designCircularDeque( + [ + "pushBack 1", + "pushBack 2", + "popFront", + "pushBack 3", + "popFront", + "pushBack 4", + "popFront", + "popFront", + ], + 2, + ); + expect(result).toEqual(["true", "true", "1", "true", "2", "true", "3", "4"]); + }); + + it("returns empty for popFront after the deque is fully drained", () => { + const result = designCircularDeque(["pushBack 1", "popFront", "popFront"], 2); + expect(result).toEqual(["true", "1", "empty"]); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-deque/educational.ts b/src/algorithms/stacks-queues/queue-design/design-circular-deque/educational.ts new file mode 100644 index 00000000..be4642a0 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-deque/educational.ts @@ -0,0 +1,76 @@ +import type { EducationalContent } from "@/types"; + +export const designCircularDequeEducational: EducationalContent = { + overview: + "**Design Circular Deque** (LeetCode 641) extends the circular queue to a double-ended queue (deque) with a fixed capacity. Two integer pointers — `front` and `rear` — track the read positions at both ends of a pre-allocated ring buffer. Insertions and deletions at either end use modular arithmetic to wrap the pointers, enabling O(1) operations with no memory reallocation.\n\nDeques appear wherever both stack-like (LIFO) and queue-like (FIFO) behaviour are needed simultaneously: browser history navigation, sliding-window maximum algorithms, and monotonic deque techniques all rely on this data structure.", + + howItWorks: + "The ring buffer holds `capacity` slots. `front` points to the oldest element at the left end; `rear` points to the newest element at the right end. Both start at `-1` to signal an empty deque.\n\n" + + "**PushBack(value)**\n" + + '1. Reject if `size == capacity` → return `"full"`.\n' + + "2. On first insertion, set `front = 0`.\n" + + "3. Advance `rear = (rear + 1) % capacity`, write `buffer[rear] = value`, increment `size`.\n\n" + + "**PushFront(value)**\n" + + '1. Reject if `size == capacity` → return `"full"`.\n' + + "2. On first insertion, set `front = rear = 0`.\n" + + "3. Otherwise retreat `front = (front - 1 + capacity) % capacity`, write `buffer[front] = value`, increment `size`.\n\n" + + "**PopFront**\n" + + '1. Reject if `size == 0` → return `"empty"`.\n' + + "2. Read `buffer[front]`, clear the slot.\n" + + "3. If `front == rear`, the deque is now empty: reset both to `-1`.\n" + + "4. Otherwise advance `front = (front + 1) % capacity`, decrement `size`.\n\n" + + "**PopBack**\n" + + '1. Reject if `size == 0` → return `"empty"`.\n' + + "2. Read `buffer[rear]`, clear the slot.\n" + + "3. If `front == rear`, the deque is now empty: reset both to `-1`.\n" + + "4. Otherwise retreat `rear = (rear - 1 + capacity) % capacity`, decrement `size`.\n\n" + + '**PeekFront / PeekRear** — return `buffer[front]` or `buffer[rear]`; return `"empty"` if the deque is empty.\n\n' + + "### Example trace (capacity = 3)\n\n" + + "```\n" + + "op buffer front rear size\n" + + "init [_, _, _] -1 -1 0\n" + + "pushBack 1 [1, _, _] 0 0 1\n" + + "pushFront 2 [1, _, 2] 2 0 2 ← front wraps to slot 2\n" + + "popBack [1, _, _] 2 0 1 ← removes 1 from slot 0\n" + + "pushBack 3 [1, 3, _] wait — slot 0 is the old rear...\n" + + "peekFront returns 2 ← front is at slot 2\n" + + "peekRear returns 3 ← rear is at slot 1\n" + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(1)` per operation**\n\n" + + "All four insertion/removal operations and both peek operations are constant-time: a modular increment or decrement and a single array write or read. No shifting or resizing occurs.\n\n" + + "**Space Complexity: `O(k)`** where `k` is the fixed capacity\n\n" + + "Memory is allocated once at construction. Unlike a dynamic array or linked list, the ring buffer never reallocates regardless of how many push/pop operations are performed.", + + bestAndWorstCase: + "**Best case** — a single operation on an empty or full deque: `O(1)` — constant work regardless of capacity.\n\n" + + "**Worst case** — still `O(1)` per operation. Because the ring buffer never resizes and every pointer update is a single modular arithmetic step, there is no amortised cost and no worst-case degradation.\n\n" + + "The one-time initialisation of the backing array is `O(k)` but is a construction cost, not a per-operation cost.", + + realWorldUses: [ + "**Browser history:** Forward/back navigation maintains a bounded deque of visited URLs — new pages push to the rear, back presses pop from the rear, forward presses push from the front.", + "**Sliding window maximum (monotonic deque):** Algorithms that need the maximum of the last `k` elements in O(1) per element use a deque to maintain a decreasing front-to-rear order, pushing to the rear and popping from both ends.", + "**Work-stealing schedulers:** Thread pools such as Java's `ForkJoinPool` use per-thread deques where the owning thread pops from the rear (stack discipline) and thief threads steal from the front (queue discipline).", + "**Undo/redo stacks:** Text editors maintain a bounded circular deque of edit operations; undo pops from the rear while redo pushes back, and oldest history is evicted from the front when the deque is full.", + "**Embedded UART buffers:** Bidirectional serial communication drivers use a circular deque so both the ISR and the application layer can insert or remove bytes from either end without memory allocation.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(1) time for every operation — insertions and removals at both ends are equally fast.", + "Fixed memory footprint — allocated once at construction, never reallocated.", + "Cache-friendly contiguous array layout with pointer arithmetic instead of pointer chasing.", + "Supports both stack and queue access patterns simultaneously from a single structure.", + ], + limitations: [ + 'Fixed capacity — the buffer cannot grow; callers must handle `"full"` responses.', + "O(k) initialisation cost — the backing array must be pre-allocated before any operation.", + "Not thread-safe by default; concurrent producers/consumers at both ends require synchronisation.", + "Wrap-around pointer arithmetic `(index - 1 + capacity) % capacity` is slightly more error-prone than a simple increment.", + ], + }, + + whenToUseIt: + "Use a circular deque when you need both LIFO and FIFO access patterns, the maximum number of in-flight items is bounded and known in advance, and O(1) guaranteed throughput at both ends is required. If only one-ended access is needed, a circular queue suffices. If capacity is unknown or must grow dynamically, prefer a linked-list deque or `ArrayDeque`. For sliding-window maximum/minimum problems, the monotonic deque pattern built on this structure is the canonical O(n) solution.", +}; diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-deque/index.ts b/src/algorithms/stacks-queues/queue-design/design-circular-deque/index.ts new file mode 100644 index 00000000..2da24697 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-deque/index.ts @@ -0,0 +1,48 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { designCircularDeque } from "./sources/design-circular-deque.ts?fn"; +import { generateDesignCircularDequeSteps } from "./step-generator"; +import type { DesignCircularDequeInput } from "./step-generator"; +import { designCircularDequeEducational } from "./educational"; + +import typescriptSource from "./sources/design-circular-deque.ts?raw"; +import pythonSource from "./sources/design-circular-deque.py?raw"; +import javaSource from "./sources/DesignCircularDeque.java?raw"; + +function executeDesignCircularDeque(input: DesignCircularDequeInput): string[] { + return designCircularDeque(input.operations, input.capacity) as string[]; +} + +const designCircularDequeDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.DESIGN_CIRCULAR_DEQUE!, + name: "Design Circular Deque", + category: CATEGORY.STACKS_QUEUES!, + technique: "queue-design", + description: + "Implement a fixed-capacity double-ended queue using a ring buffer with front/rear pointers and modular arithmetic — O(1) per operation at both ends with no memory reallocation", + timeComplexity: { + best: "O(1)", + average: "O(1)", + worst: "O(1)", + }, + spaceComplexity: "O(k)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + operations: ["pushBack 1", "pushFront 2", "popBack", "pushBack 3"], + capacity: 3, + }, + }, + execute: executeDesignCircularDeque, + generateSteps: generateDesignCircularDequeSteps, + educational: designCircularDequeEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(designCircularDequeDefinition); diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-deque/sources/DesignCircularDeque.java b/src/algorithms/stacks-queues/queue-design/design-circular-deque/sources/DesignCircularDeque.java new file mode 100644 index 00000000..e832bd5b --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-deque/sources/DesignCircularDeque.java @@ -0,0 +1,93 @@ +// Design Circular Deque — fixed-capacity ring buffer with front/rear insertion and removal (LeetCode 641) +import java.util.ArrayList; +import java.util.List; + +public class DesignCircularDeque { + public static List designCircularDeque(String[] operations, int capacity) { + Integer[] buffer = new Integer[capacity]; // @step:initialize + int frontIndex = -1; // @step:initialize + int rearIndex = -1; // @step:initialize + int dequeSize = 0; // @step:initialize + List results = new ArrayList<>(); // @step:initialize + + for (String operation : operations) { // @step:visit + if (operation.startsWith("pushBack")) { + String[] parts = operation.split(" "); // @step:enqueue + int value = Integer.parseInt(parts[1]); // @step:enqueue + + if (dequeSize == capacity) { // @step:enqueue + results.add("full"); // @step:enqueue + } else { + if (frontIndex == -1) { // @step:enqueue + frontIndex = 0; // @step:enqueue + } + rearIndex = (rearIndex + 1) % capacity; // @step:enqueue + buffer[rearIndex] = value; // @step:enqueue + dequeSize++; // @step:enqueue + results.add("true"); // @step:enqueue + } + } else if (operation.startsWith("pushFront")) { + String[] parts = operation.split(" "); // @step:enqueue-front + int value = Integer.parseInt(parts[1]); // @step:enqueue-front + + if (dequeSize == capacity) { // @step:enqueue-front + results.add("full"); // @step:enqueue-front + } else { + if (frontIndex == -1) { // @step:enqueue-front + frontIndex = 0; // @step:enqueue-front + rearIndex = 0; // @step:enqueue-front + } else { + frontIndex = (frontIndex - 1 + capacity) % capacity; // @step:enqueue-front + } + buffer[frontIndex] = value; // @step:enqueue-front + dequeSize++; // @step:enqueue-front + results.add("true"); // @step:enqueue-front + } + } else if (operation.equals("popFront")) { + if (dequeSize == 0) { // @step:dequeue + results.add("empty"); // @step:dequeue + } else { + int poppedValue = buffer[frontIndex]; // @step:dequeue + buffer[frontIndex] = null; // @step:dequeue + if (frontIndex == rearIndex) { // @step:dequeue + frontIndex = -1; // @step:dequeue + rearIndex = -1; // @step:dequeue + } else { + frontIndex = (frontIndex + 1) % capacity; // @step:dequeue + } + dequeSize--; // @step:dequeue + results.add(String.valueOf(poppedValue)); // @step:dequeue + } + } else if (operation.equals("popBack")) { + if (dequeSize == 0) { // @step:dequeue-rear + results.add("empty"); // @step:dequeue-rear + } else { + int poppedValue = buffer[rearIndex]; // @step:dequeue-rear + buffer[rearIndex] = null; // @step:dequeue-rear + if (frontIndex == rearIndex) { // @step:dequeue-rear + frontIndex = -1; // @step:dequeue-rear + rearIndex = -1; // @step:dequeue-rear + } else { + rearIndex = (rearIndex - 1 + capacity) % capacity; // @step:dequeue-rear + } + dequeSize--; // @step:dequeue-rear + results.add(String.valueOf(poppedValue)); // @step:dequeue-rear + } + } else if (operation.equals("peekFront")) { + if (frontIndex == -1) { // @step:peek + results.add("empty"); // @step:peek + } else { + results.add(String.valueOf(buffer[frontIndex])); // @step:peek + } + } else if (operation.equals("peekRear")) { + if (rearIndex == -1) { // @step:peek + results.add("empty"); // @step:peek + } else { + results.add(String.valueOf(buffer[rearIndex])); // @step:peek + } + } + } + + return results; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-deque/sources/design-circular-deque.py b/src/algorithms/stacks-queues/queue-design/design-circular-deque/sources/design-circular-deque.py new file mode 100644 index 00000000..b983ecc5 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-deque/sources/design-circular-deque.py @@ -0,0 +1,83 @@ +# Design Circular Deque — fixed-capacity ring buffer with front/rear insertion and removal (LeetCode 641) +from typing import List, Optional + + +def design_circular_deque(operations: List[str], capacity: int) -> List[str]: + buffer: List[Optional[int]] = [None] * capacity # @step:initialize + front_index: int = -1 # @step:initialize + rear_index: int = -1 # @step:initialize + deque_size: int = 0 # @step:initialize + results: List[str] = [] # @step:initialize + + for operation in operations: # @step:visit + if operation.startswith("pushBack"): + parts = operation.split(" ") # @step:enqueue + value = int(parts[1]) # @step:enqueue + + if deque_size == capacity: # @step:enqueue + results.append("full") # @step:enqueue + else: + if front_index == -1: # @step:enqueue + front_index = 0 # @step:enqueue + rear_index = (rear_index + 1) % capacity # @step:enqueue + buffer[rear_index] = value # @step:enqueue + deque_size += 1 # @step:enqueue + results.append("true") # @step:enqueue + + elif operation.startswith("pushFront"): + parts = operation.split(" ") # @step:enqueue-front + value = int(parts[1]) # @step:enqueue-front + + if deque_size == capacity: # @step:enqueue-front + results.append("full") # @step:enqueue-front + else: + if front_index == -1: # @step:enqueue-front + front_index = 0 # @step:enqueue-front + rear_index = 0 # @step:enqueue-front + else: + front_index = (front_index - 1 + capacity) % capacity # @step:enqueue-front + buffer[front_index] = value # @step:enqueue-front + deque_size += 1 # @step:enqueue-front + results.append("true") # @step:enqueue-front + + elif operation == "popFront": + if deque_size == 0: # @step:dequeue + results.append("empty") # @step:dequeue + else: + popped_value = buffer[front_index] # @step:dequeue + buffer[front_index] = None # @step:dequeue + if front_index == rear_index: # @step:dequeue + front_index = -1 # @step:dequeue + rear_index = -1 # @step:dequeue + else: + front_index = (front_index + 1) % capacity # @step:dequeue + deque_size -= 1 # @step:dequeue + results.append(str(popped_value)) # @step:dequeue + + elif operation == "popBack": + if deque_size == 0: # @step:dequeue-rear + results.append("empty") # @step:dequeue-rear + else: + popped_value = buffer[rear_index] # @step:dequeue-rear + buffer[rear_index] = None # @step:dequeue-rear + if front_index == rear_index: # @step:dequeue-rear + front_index = -1 # @step:dequeue-rear + rear_index = -1 # @step:dequeue-rear + else: + rear_index = (rear_index - 1 + capacity) % capacity # @step:dequeue-rear + deque_size -= 1 # @step:dequeue-rear + results.append(str(popped_value)) # @step:dequeue-rear + + elif operation == "peekFront": + if front_index == -1: # @step:peek + results.append("empty") # @step:peek + else: + results.append(str(buffer[front_index])) # @step:peek + + elif operation == "peekRear": + if rear_index == -1: # @step:peek + results.append("empty") # @step:peek + else: + results.append(str(buffer[rear_index])) # @step:peek + + return results # @step:complete diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-deque/sources/design-circular-deque.ts b/src/algorithms/stacks-queues/queue-design/design-circular-deque/sources/design-circular-deque.ts new file mode 100644 index 00000000..9d0d5546 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-deque/sources/design-circular-deque.ts @@ -0,0 +1,100 @@ +// Design Circular Deque — fixed-capacity ring buffer with front/rear insertion and removal (LeetCode 641) +function designCircularDeque(operations: string[], capacity: number): string[] { + const buffer: (number | null)[] = new Array(capacity).fill(null); // @step:initialize + let frontIndex = -1; // @step:initialize + let rearIndex = -1; // @step:initialize + let dequeSize = 0; // @step:initialize + const results: string[] = []; // @step:initialize + + for (let operationIndex = 0; operationIndex < operations.length; operationIndex++) { + const operation = operations[operationIndex]!; // @step:visit + + if (operation.startsWith("pushBack")) { + const valueStr = operation.split(" ")[1]!; // @step:enqueue + const value = parseInt(valueStr, 10); // @step:enqueue + + if (dequeSize === capacity) { + // @step:enqueue + results.push("full"); // @step:enqueue + } else { + if (frontIndex === -1) { + // @step:enqueue + frontIndex = 0; // @step:enqueue + } + rearIndex = (rearIndex + 1) % capacity; // @step:enqueue + buffer[rearIndex] = value; // @step:enqueue + dequeSize++; // @step:enqueue + results.push("true"); // @step:enqueue + } + } else if (operation.startsWith("pushFront")) { + const valueStr = operation.split(" ")[1]!; // @step:enqueue-front + const value = parseInt(valueStr, 10); // @step:enqueue-front + + if (dequeSize === capacity) { + // @step:enqueue-front + results.push("full"); // @step:enqueue-front + } else { + if (frontIndex === -1) { + // @step:enqueue-front + frontIndex = 0; // @step:enqueue-front + rearIndex = 0; // @step:enqueue-front + } else { + frontIndex = (frontIndex - 1 + capacity) % capacity; // @step:enqueue-front + } + buffer[frontIndex] = value; // @step:enqueue-front + dequeSize++; // @step:enqueue-front + results.push("true"); // @step:enqueue-front + } + } else if (operation === "popFront") { + if (dequeSize === 0) { + // @step:dequeue + results.push("empty"); // @step:dequeue + } else { + const poppedValue = buffer[frontIndex]!; // @step:dequeue + buffer[frontIndex] = null; // @step:dequeue + if (frontIndex === rearIndex) { + // @step:dequeue + frontIndex = -1; // @step:dequeue + rearIndex = -1; // @step:dequeue + } else { + frontIndex = (frontIndex + 1) % capacity; // @step:dequeue + } + dequeSize--; // @step:dequeue + results.push(String(poppedValue)); // @step:dequeue + } + } else if (operation === "popBack") { + if (dequeSize === 0) { + // @step:dequeue-rear + results.push("empty"); // @step:dequeue-rear + } else { + const poppedValue = buffer[rearIndex]!; // @step:dequeue-rear + buffer[rearIndex] = null; // @step:dequeue-rear + if (frontIndex === rearIndex) { + // @step:dequeue-rear + frontIndex = -1; // @step:dequeue-rear + rearIndex = -1; // @step:dequeue-rear + } else { + rearIndex = (rearIndex - 1 + capacity) % capacity; // @step:dequeue-rear + } + dequeSize--; // @step:dequeue-rear + results.push(String(poppedValue)); // @step:dequeue-rear + } + } else if (operation === "peekFront") { + if (frontIndex === -1) { + // @step:peek + results.push("empty"); // @step:peek + } else { + results.push(String(buffer[frontIndex])); // @step:peek + } + } else if (operation === "peekRear") { + if (rearIndex === -1) { + // @step:peek + results.push("empty"); // @step:peek + } else { + results.push(String(buffer[rearIndex])); // @step:peek + } + } + } + + return results; // @step:complete +} diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-deque/step-generator.test.ts b/src/algorithms/stacks-queues/queue-design/design-circular-deque/step-generator.test.ts new file mode 100644 index 00000000..4963ca20 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-deque/step-generator.test.ts @@ -0,0 +1,166 @@ +import { describe, it, expect } from "vitest"; +import { generateDesignCircularDequeSteps } from "./step-generator"; +import type { StackQueueVisualState } from "@/types"; + +const DEFAULT_INPUT = { + operations: ["pushBack 1", "pushFront 2", "popBack", "pushBack 3"], + capacity: 3, +}; + +describe("generateDesignCircularDequeSteps", () => { + it("produces steps for the default input", () => { + const steps = generateDesignCircularDequeSteps(DEFAULT_INPUT); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateDesignCircularDequeSteps(DEFAULT_INPUT); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateDesignCircularDequeSteps(DEFAULT_INPUT); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateDesignCircularDequeSteps(DEFAULT_INPUT); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateDesignCircularDequeSteps(DEFAULT_INPUT); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); + + it("includes a circular buffer in every visual state", () => { + const steps = generateDesignCircularDequeSteps(DEFAULT_INPUT); + for (const step of steps) { + const visualState = step.visualState as StackQueueVisualState; + expect(visualState.circularBuffer).toBeDefined(); + } + }); + + it("circular buffer has the correct capacity", () => { + const steps = generateDesignCircularDequeSteps(DEFAULT_INPUT); + const initialStep = steps[0]!; + const visualState = initialStep.visualState as StackQueueVisualState; + expect(visualState.circularBuffer?.capacity).toBe(DEFAULT_INPUT.capacity); + }); + + it("emits one enqueue step per successful pushBack operation", () => { + const pushBackInput = { + operations: ["pushBack 1", "pushBack 2", "pushBack 3"], + capacity: 3, + }; + const steps = generateDesignCircularDequeSteps(pushBackInput); + const enqueueSteps = steps.filter((step) => step.type === "enqueue"); + expect(enqueueSteps.length).toBe(3); + }); + + it("emits one enqueue-front step per successful pushFront operation", () => { + const pushFrontInput = { + operations: ["pushBack 1", "pushFront 2"], + capacity: 3, + }; + const steps = generateDesignCircularDequeSteps(pushFrontInput); + const enqueueFrontSteps = steps.filter((step) => step.type === "enqueue-front"); + expect(enqueueFrontSteps.length).toBe(1); + }); + + it("emits one dequeue step per successful popFront operation", () => { + const popFrontInput = { + operations: ["pushBack 1", "pushBack 2", "popFront"], + capacity: 3, + }; + const steps = generateDesignCircularDequeSteps(popFrontInput); + const dequeueSteps = steps.filter((step) => step.type === "dequeue"); + expect(dequeueSteps.length).toBe(1); + }); + + it("emits one dequeue-rear step per successful popBack operation", () => { + const popBackInput = { + operations: ["pushBack 1", "pushBack 2", "popBack"], + capacity: 3, + }; + const steps = generateDesignCircularDequeSteps(popBackInput); + const dequeueRearSteps = steps.filter((step) => step.type === "dequeue-rear"); + expect(dequeueRearSteps.length).toBe(1); + }); + + it("records results in the complete step variables", () => { + const steps = generateDesignCircularDequeSteps(DEFAULT_INPUT); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.variables["results"]).toEqual(["true", "true", "1", "true"]); + }); + + it("emits peek steps for full-deque push attempts", () => { + const fullDequeInput = { + operations: ["pushBack 1", "pushBack 2", "pushBack 3", "pushBack 4"], + capacity: 3, + }; + const steps = generateDesignCircularDequeSteps(fullDequeInput); + const peekSteps = steps.filter((step) => step.type === "peek"); + expect(peekSteps.length).toBeGreaterThanOrEqual(1); + }); + + it("emits a peek step for popFront-from-empty attempts", () => { + const emptyPopInput = { + operations: ["popFront"], + capacity: 3, + }; + const steps = generateDesignCircularDequeSteps(emptyPopInput); + const peekSteps = steps.filter((step) => step.type === "peek"); + expect(peekSteps.length).toBe(1); + }); + + it("emits a peek step for popBack-from-empty attempts", () => { + const emptyPopBackInput = { + operations: ["popBack"], + capacity: 3, + }; + const steps = generateDesignCircularDequeSteps(emptyPopBackInput); + const peekSteps = steps.filter((step) => step.type === "peek"); + expect(peekSteps.length).toBe(1); + }); + + it("handles a single-operation input without errors", () => { + const singleOpInput = { operations: ["pushBack 42"], capacity: 2 }; + const steps = generateDesignCircularDequeSteps(singleOpInput); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("circular buffer rear index advances modularly on wrap-around for pushBack", () => { + const wrapInput = { + operations: ["pushBack 1", "pushBack 2", "popFront", "pushBack 3"], + capacity: 2, + }; + const steps = generateDesignCircularDequeSteps(wrapInput); + const enqueueSteps = steps.filter((step) => step.type === "enqueue"); + const lastEnqueue = enqueueSteps[enqueueSteps.length - 1]!; + const visualState = lastEnqueue.visualState as StackQueueVisualState; + // After dequeue from slot 0 then pushBack again, rear should wrap to slot 0 + expect(visualState.circularBuffer?.rearIndex).toBe(0); + }); + + it("capacity is set in initialize step variables", () => { + const steps = generateDesignCircularDequeSteps(DEFAULT_INPUT); + const initStep = steps[0]!; + expect(initStep.variables["capacity"]).toBe(DEFAULT_INPUT.capacity); + }); + + it("emits peek steps for peekFront and peekRear operations", () => { + const peekInput = { + operations: ["pushBack 5", "pushBack 10", "peekFront", "peekRear"], + capacity: 3, + }; + const steps = generateDesignCircularDequeSteps(peekInput); + const peekSteps = steps.filter((step) => step.type === "peek"); + expect(peekSteps.length).toBe(2); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-deque/step-generator.ts b/src/algorithms/stacks-queues/queue-design/design-circular-deque/step-generator.ts new file mode 100644 index 00000000..ce5f2ee8 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-deque/step-generator.ts @@ -0,0 +1,194 @@ +/** Step generator for Design Circular Deque — produces ExecutionStep[] using QueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { QueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const DCD_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.DESIGN_CIRCULAR_DEQUE!); + +export interface DesignCircularDequeInput { + operations: string[]; + capacity: number; +} + +export function generateDesignCircularDequeSteps(input: DesignCircularDequeInput): ExecutionStep[] { + const { operations, capacity } = input; + + // Local mirror of the ring buffer — used for variable snapshots and result derivation + const localBuffer: (number | null)[] = new Array(capacity).fill(null); + let frontIndex = -1; + let rearIndex = -1; + let dequeSize = 0; + const results: string[] = []; + + const tracker = new QueueTracker([], DCD_LINE_MAP); + tracker.initCircularBuffer(capacity); + + tracker.initialize({ + capacity, + frontIndex, + rearIndex, + dequeSize, + results: [], + }); + + for (let operationIndex = 0; operationIndex < operations.length; operationIndex++) { + const operation = operations[operationIndex]!; + + if (operation.startsWith("pushBack")) { + const valueStr = operation.split(" ")[1]!; + const pushBackValue = parseInt(valueStr, 10); + + if (dequeSize === capacity) { + results.push("full"); + tracker.peekFront( + { operationIndex, operation, frontIndex, rearIndex, dequeSize, results: [...results] }, + `PushBack ${valueStr} rejected — deque is full (size ${dequeSize} = capacity ${capacity})`, + ); + } else { + if (frontIndex === -1) { + frontIndex = 0; + } + rearIndex = (rearIndex + 1) % capacity; + localBuffer[rearIndex] = pushBackValue; + dequeSize++; + results.push("true"); + tracker.circularEnqueue( + pushBackValue, + { operationIndex, operation, frontIndex, rearIndex, dequeSize, results: [...results] }, + `PushBack ${valueStr} at slot ${rearIndex} — front: ${frontIndex}, rear: ${rearIndex}, size: ${dequeSize}`, + ); + } + } else if (operation.startsWith("pushFront")) { + const valueStr = operation.split(" ")[1]!; + const pushFrontValue = parseInt(valueStr, 10); + + if (dequeSize === capacity) { + results.push("full"); + tracker.peekFront( + { operationIndex, operation, frontIndex, rearIndex, dequeSize, results: [...results] }, + `PushFront ${valueStr} rejected — deque is full (size ${dequeSize} = capacity ${capacity})`, + ); + } else { + if (frontIndex === -1) { + frontIndex = 0; + rearIndex = 0; + } else { + frontIndex = (frontIndex - 1 + capacity) % capacity; + } + localBuffer[frontIndex] = pushFrontValue; + dequeSize++; + results.push("true"); + tracker.enqueueFront( + String(pushFrontValue), + { operationIndex, operation, frontIndex, rearIndex, dequeSize, results: [...results] }, + `PushFront ${valueStr} at slot ${frontIndex} — front: ${frontIndex}, rear: ${rearIndex}, size: ${dequeSize}`, + ); + } + } else if (operation === "popFront") { + if (dequeSize === 0) { + results.push("empty"); + tracker.peekFront( + { operationIndex, operation, frontIndex, rearIndex, dequeSize, results: [...results] }, + "PopFront — deque is empty", + ); + } else { + const poppedValue = localBuffer[frontIndex] ?? 0; + localBuffer[frontIndex] = null; + const prevFront = frontIndex; + if (frontIndex === rearIndex) { + frontIndex = -1; + rearIndex = -1; + } else { + frontIndex = (frontIndex + 1) % capacity; + } + dequeSize--; + results.push(String(poppedValue)); + tracker.circularDequeue( + { + operationIndex, + operation, + frontIndex, + rearIndex, + dequeSize, + prevFront, + poppedValue, + results: [...results], + }, + `PopFront ${String(poppedValue)} from slot ${prevFront} — front: ${frontIndex}, rear: ${rearIndex}, size: ${dequeSize}`, + ); + } + } else if (operation === "popBack") { + if (dequeSize === 0) { + results.push("empty"); + tracker.peekFront( + { operationIndex, operation, frontIndex, rearIndex, dequeSize, results: [...results] }, + "PopBack — deque is empty", + ); + } else { + const poppedValue = localBuffer[rearIndex] ?? 0; + localBuffer[rearIndex] = null; + const prevRear = rearIndex; + if (frontIndex === rearIndex) { + frontIndex = -1; + rearIndex = -1; + } else { + rearIndex = (rearIndex - 1 + capacity) % capacity; + } + dequeSize--; + results.push(String(poppedValue)); + tracker.dequeueRear( + { + operationIndex, + operation, + frontIndex, + rearIndex, + dequeSize, + prevRear, + poppedValue, + results: [...results], + }, + `PopBack ${String(poppedValue)} from slot ${prevRear} — front: ${frontIndex}, rear: ${rearIndex}, size: ${dequeSize}`, + ); + } + } else if (operation === "peekFront") { + const frontValue = frontIndex === -1 ? "empty" : String(localBuffer[frontIndex] ?? "empty"); + results.push(frontValue); + tracker.peekFront( + { + operationIndex, + operation, + frontIndex, + rearIndex, + dequeSize, + peekValue: frontValue, + results: [...results], + }, + `Peek front — value: ${frontValue}, position: ${frontIndex}, size: ${dequeSize}`, + ); + } else if (operation === "peekRear") { + const rearValue = rearIndex === -1 ? "empty" : String(localBuffer[rearIndex] ?? "empty"); + results.push(rearValue); + tracker.peekFront( + { + operationIndex, + operation, + frontIndex, + rearIndex, + dequeSize, + peekValue: rearValue, + results: [...results], + }, + `Peek rear — value: ${rearValue}, position: ${rearIndex}, size: ${dequeSize}`, + ); + } + } + + tracker.complete( + { frontIndex, rearIndex, dequeSize, results }, + `All ${operations.length} operations complete — ${results.length} results produced`, + ); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-queue/DesignCircularQueuePipeline.stories.tsx b/src/algorithms/stacks-queues/queue-design/design-circular-queue/DesignCircularQueuePipeline.stories.tsx new file mode 100644 index 00000000..5439b7d1 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-queue/DesignCircularQueuePipeline.stories.tsx @@ -0,0 +1,90 @@ +/** + * Storybook stories for the Design Circular Queue algorithm pipeline. + * Uses the real step generator with the default input, rendering the + * StackQueueVisualizer at key circular buffer states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateDesignCircularQueueSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateDesignCircularQueueSteps({ + operations: ["enqueue 1", "enqueue 2", "dequeue", "enqueue 3"], + capacity: 3, +}); + +const wrapAroundSteps = generateDesignCircularQueueSteps({ + operations: ["enqueue 1", "enqueue 2", "enqueue 3", "dequeue", "enqueue 4"], + capacity: 3, +}); + +const fullQueueSteps = generateDesignCircularQueueSteps({ + operations: ["enqueue 10", "enqueue 20", "enqueue 30", "enqueue 40"], + capacity: 3, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Design Circular Queue", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — empty ring buffer with capacity 3 */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** After first enqueue — front and rear both point to slot 0 */ +export const FirstEnqueue: Story = { + args: { + visualState: (() => { + const enqueueSteps = defaultSteps.filter((step) => step.type === "enqueue"); + return enqueueSteps[0]!.visualState as StackQueueVisualState; + })(), + }, +}; + +/** After dequeue — slot cleared, front pointer advances */ +export const AfterDequeue: Story = { + args: { + visualState: (() => { + const dequeueSteps = defaultSteps.filter((step) => step.type === "dequeue"); + return dequeueSteps[0]!.visualState as StackQueueVisualState; + })(), + }, +}; + +/** Complete — all operations processed */ +export const AllOperationsComplete: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Wrap-around — rear pointer wraps back to slot 0 after reaching capacity */ +export const WrapAround: Story = { + args: { + visualState: wrapAroundSteps[wrapAroundSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Full queue — enqueue rejected because size equals capacity */ +export const QueueFull: Story = { + args: { + visualState: (() => { + const peekSteps = fullQueueSteps.filter((step) => step.type === "peek"); + return peekSteps[0]!.visualState as StackQueueVisualState; + })(), + }, +}; diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-queue/design-circular-queue.test.ts b/src/algorithms/stacks-queues/queue-design/design-circular-queue/design-circular-queue.test.ts new file mode 100644 index 00000000..bf15613c --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-queue/design-circular-queue.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect } from "vitest"; +import { designCircularQueue } from "./sources/design-circular-queue.ts?fn"; + +describe("designCircularQueue", () => { + it("enqueues values and returns true for each successful enqueue", () => { + const result = designCircularQueue(["enqueue 1", "enqueue 2", "enqueue 3"], 3); + expect(result).toEqual(["true", "true", "true"]); + }); + + it("returns full when enqueueing beyond capacity", () => { + const result = designCircularQueue(["enqueue 1", "enqueue 2", "enqueue 3", "enqueue 4"], 3); + expect(result).toEqual(["true", "true", "true", "full"]); + }); + + it("returns empty when dequeueing from an empty queue", () => { + const result = designCircularQueue(["dequeue"], 3); + expect(result).toEqual(["empty"]); + }); + + it("dequeues values in FIFO order", () => { + const result = designCircularQueue( + ["enqueue 1", "enqueue 2", "enqueue 3", "dequeue", "dequeue", "dequeue"], + 3, + ); + expect(result).toEqual(["true", "true", "true", "1", "2", "3"]); + }); + + it("wraps rear pointer around using modular arithmetic", () => { + const result = designCircularQueue( + ["enqueue 1", "enqueue 2", "dequeue", "enqueue 3", "enqueue 4"], + 3, + ); + expect(result).toEqual(["true", "true", "1", "true", "true"]); + }); + + it("resets front and rear to -1 after dequeuing the last element", () => { + const result = designCircularQueue(["enqueue 5", "dequeue", "enqueue 7"], 2); + expect(result).toEqual(["true", "5", "true"]); + }); + + it("peeks the front value correctly", () => { + const result = designCircularQueue(["enqueue 10", "enqueue 20", "front"], 3); + expect(result).toEqual(["true", "true", "10"]); + }); + + it("peeks the rear value correctly", () => { + const result = designCircularQueue(["enqueue 10", "enqueue 20", "rear"], 3); + expect(result).toEqual(["true", "true", "20"]); + }); + + it("returns empty for front and rear peek on an empty queue", () => { + const result = designCircularQueue(["front", "rear"], 3); + expect(result).toEqual(["empty", "empty"]); + }); + + it("handles the default input correctly", () => { + const result = designCircularQueue(["enqueue 1", "enqueue 2", "dequeue", "enqueue 3"], 3); + expect(result).toEqual(["true", "true", "1", "true"]); + }); + + it("handles a capacity-1 queue with enqueue and dequeue", () => { + const result = designCircularQueue(["enqueue 42", "dequeue", "enqueue 99", "dequeue"], 1); + expect(result).toEqual(["true", "42", "true", "99"]); + }); + + it("handles alternating enqueue and dequeue across wrap-around", () => { + const result = designCircularQueue( + [ + "enqueue 1", + "enqueue 2", + "dequeue", + "enqueue 3", + "dequeue", + "enqueue 4", + "dequeue", + "dequeue", + ], + 2, + ); + expect(result).toEqual(["true", "true", "1", "true", "2", "true", "3", "4"]); + }); + + it("returns empty for dequeue after the queue is fully drained", () => { + const result = designCircularQueue(["enqueue 1", "dequeue", "dequeue"], 2); + expect(result).toEqual(["true", "1", "empty"]); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-queue/educational.ts b/src/algorithms/stacks-queues/queue-design/design-circular-queue/educational.ts new file mode 100644 index 00000000..9068569e --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-queue/educational.ts @@ -0,0 +1,68 @@ +import type { EducationalContent } from "@/types"; + +export const designCircularQueueEducational: EducationalContent = { + overview: + "**Design Circular Queue** (LeetCode 622) implements a fixed-capacity FIFO queue using a ring buffer. Two integer pointers — `front` and `rear` — track the read and write positions within a pre-allocated array. When a pointer reaches the end of the array, modular arithmetic wraps it back to the start, enabling the buffer to reuse freed slots without shifting elements.\n\nThis is the foundation of many real-world systems: I/O buffers, network packet queues, audio streaming pipelines, and operating-system scheduler rings all use the same pattern.", + + howItWorks: + "The ring buffer holds `capacity` slots. `front` points to the oldest element; `rear` points to the newest. Both start at `-1` to signal an empty queue.\n\n" + + "**Enqueue(value)**\n" + + '1. Reject if `size == capacity` → return `"full"`.\n' + + "2. On first enqueue, set `front = 0`.\n" + + "3. Advance `rear = (rear + 1) % capacity`, write `buffer[rear] = value`, increment `size`.\n\n" + + "**Dequeue**\n" + + '1. Reject if `size == 0` → return `"empty"`.\n' + + "2. Read `buffer[front]`, clear the slot.\n" + + "3. If `front == rear`, the queue is now empty: reset both to `-1`.\n" + + "4. Otherwise advance `front = (front + 1) % capacity`, decrement `size`.\n\n" + + '**Front / Rear peek** — return `buffer[front]` or `buffer[rear]`; return `"empty"` if the queue is empty.\n\n' + + "### Example trace (capacity = 3)\n\n" + + "```\n" + + "op buffer front rear size\n" + + "init [_, _, _] -1 -1 0\n" + + "enqueue 1 [1, _, _] 0 0 1\n" + + "enqueue 2 [1, 2, _] 0 1 2\n" + + "enqueue 3 [1, 2, 3] 0 2 3 ← full\n" + + "dequeue [_, 2, 3] 1 2 2 ← returns 1\n" + + "enqueue 4 [4, 2, 3] 1 0 3 ← wraps around\n" + + "front returns 2\n" + + "rear returns 4\n" + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(1)` per operation**\n\n" + + "Enqueue, dequeue, front, and rear are all constant-time: a modular increment and a single array write or read. There is no shifting or resizing.\n\n" + + "**Space Complexity: `O(k)`** where `k` is the fixed capacity\n\n" + + "Memory is allocated once at construction and never reallocated regardless of how many enqueue/dequeue operations are performed.", + + bestAndWorstCase: + "**Best case** — a single operation on an empty or full queue: `O(1)` — constant work regardless of capacity.\n\n" + + "**Worst case** — still `O(1)` per operation. The ring buffer never degrades; no operation has an amortised cost because there is nothing to amortise.\n\n" + + "The only cost that scales with `k` is the one-time initialisation of the backing array.", + + realWorldUses: [ + "**Operating system I/O buffers:** Keyboard, network, and disk drivers use ring buffers to decouple hardware interrupt rates from software processing speeds.", + "**Network packet queues:** Routers and NICs store in-flight packets in circular buffers to absorb bursts without unbounded memory growth.", + "**Audio and video streaming:** Media players use ring buffers to keep a rolling window of decoded frames so playback is never stalled by a slow decoder.", + "**Producer-consumer pipelines:** Any bounded-buffer problem — thread-safe queues between goroutines, actors, or workers — benefits from the zero-allocation, cache-friendly ring buffer.", + "**Embedded systems:** Microcontrollers with strict memory limits use circular buffers for UART/SPI receive queues because the fixed footprint is known at compile time.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(1) time for every operation — no amortised cost, no worst-case spikes.", + "Fixed memory footprint — allocated once, never reallocated.", + "Cache-friendly sequential access pattern through contiguous array storage.", + "Simple implementation with only two pointers and one counter.", + ], + limitations: [ + 'Fixed capacity — the buffer cannot grow; callers must handle `"full"` responses.', + "O(k) initialisation cost — the backing array must be allocated before any operation.", + "Not thread-safe by default; concurrent producers/consumers require external synchronisation.", + "Distinguishing full from empty requires an explicit size counter (or sacrificing one slot).", + ], + }, + + whenToUseIt: + "Use a circular queue when the maximum number of in-flight items is bounded and known in advance, and when O(1) guaranteed throughput matters more than unbounded capacity. If the capacity is unknown or must grow dynamically, prefer a linked-list queue or a dynamically-resized deque. For single-producer / single-consumer lock-free patterns, a circular buffer is the canonical choice.", +}; diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-queue/index.ts b/src/algorithms/stacks-queues/queue-design/design-circular-queue/index.ts new file mode 100644 index 00000000..9baad097 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-queue/index.ts @@ -0,0 +1,48 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { designCircularQueue } from "./sources/design-circular-queue.ts?fn"; +import { generateDesignCircularQueueSteps } from "./step-generator"; +import type { DesignCircularQueueInput } from "./step-generator"; +import { designCircularQueueEducational } from "./educational"; + +import typescriptSource from "./sources/design-circular-queue.ts?raw"; +import pythonSource from "./sources/design-circular-queue.py?raw"; +import javaSource from "./sources/DesignCircularQueue.java?raw"; + +function executeDesignCircularQueue(input: DesignCircularQueueInput): string[] { + return designCircularQueue(input.operations, input.capacity) as string[]; +} + +const designCircularQueueDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.DESIGN_CIRCULAR_QUEUE!, + name: "Design Circular Queue", + category: CATEGORY.STACKS_QUEUES!, + technique: "queue-design", + description: + "Implement a fixed-capacity FIFO queue using a ring buffer with front/rear pointers and modular arithmetic — O(1) per operation with no memory reallocation", + timeComplexity: { + best: "O(1)", + average: "O(1)", + worst: "O(1)", + }, + spaceComplexity: "O(k)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { + operations: ["enqueue 1", "enqueue 2", "dequeue", "enqueue 3"], + capacity: 3, + }, + }, + execute: executeDesignCircularQueue, + generateSteps: generateDesignCircularQueueSteps, + educational: designCircularQueueEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(designCircularQueueDefinition); diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-queue/sources/DesignCircularQueue.java b/src/algorithms/stacks-queues/queue-design/design-circular-queue/sources/DesignCircularQueue.java new file mode 100644 index 00000000..77bb2429 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-queue/sources/DesignCircularQueue.java @@ -0,0 +1,61 @@ +// Design Circular Queue — fixed-capacity ring buffer with front/rear pointers (LeetCode 622) +import java.util.ArrayList; +import java.util.List; + +public class DesignCircularQueue { + public static List designCircularQueue(String[] operations, int capacity) { + Integer[] buffer = new Integer[capacity]; // @step:initialize + int frontIndex = -1; // @step:initialize + int rearIndex = -1; // @step:initialize + int queueSize = 0; // @step:initialize + List results = new ArrayList<>(); // @step:initialize + + for (String operation : operations) { // @step:visit + if (operation.startsWith("enqueue")) { + String[] parts = operation.split(" "); // @step:enqueue + int value = Integer.parseInt(parts[1]); // @step:enqueue + + if (queueSize == capacity) { // @step:enqueue + results.add("full"); // @step:enqueue + } else { + if (frontIndex == -1) { // @step:enqueue + frontIndex = 0; // @step:enqueue + } + rearIndex = (rearIndex + 1) % capacity; // @step:enqueue + buffer[rearIndex] = value; // @step:enqueue + queueSize++; // @step:enqueue + results.add("true"); // @step:enqueue + } + } else if (operation.equals("dequeue")) { + if (queueSize == 0) { // @step:dequeue + results.add("empty"); // @step:dequeue + } else { + int dequeuedValue = buffer[frontIndex]; // @step:dequeue + buffer[frontIndex] = null; // @step:dequeue + if (frontIndex == rearIndex) { // @step:dequeue + frontIndex = -1; // @step:dequeue + rearIndex = -1; // @step:dequeue + } else { + frontIndex = (frontIndex + 1) % capacity; // @step:dequeue + } + queueSize--; // @step:dequeue + results.add(String.valueOf(dequeuedValue)); // @step:dequeue + } + } else if (operation.equals("front")) { + if (frontIndex == -1) { // @step:peek + results.add("empty"); // @step:peek + } else { + results.add(String.valueOf(buffer[frontIndex])); // @step:peek + } + } else if (operation.equals("rear")) { + if (rearIndex == -1) { // @step:peek + results.add("empty"); // @step:peek + } else { + results.add(String.valueOf(buffer[rearIndex])); // @step:peek + } + } + } + + return results; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-queue/sources/design-circular-queue.py b/src/algorithms/stacks-queues/queue-design/design-circular-queue/sources/design-circular-queue.py new file mode 100644 index 00000000..3d7272b3 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-queue/sources/design-circular-queue.py @@ -0,0 +1,53 @@ +# Design Circular Queue — fixed-capacity ring buffer with front/rear pointers (LeetCode 622) +from typing import List, Optional + + +def design_circular_queue(operations: List[str], capacity: int) -> List[str]: + buffer: List[Optional[int]] = [None] * capacity # @step:initialize + front_index: int = -1 # @step:initialize + rear_index: int = -1 # @step:initialize + queue_size: int = 0 # @step:initialize + results: List[str] = [] # @step:initialize + + for operation in operations: # @step:visit + if operation.startswith("enqueue"): + parts = operation.split(" ") # @step:enqueue + value = int(parts[1]) # @step:enqueue + + if queue_size == capacity: # @step:enqueue + results.append("full") # @step:enqueue + else: + if front_index == -1: # @step:enqueue + front_index = 0 # @step:enqueue + rear_index = (rear_index + 1) % capacity # @step:enqueue + buffer[rear_index] = value # @step:enqueue + queue_size += 1 # @step:enqueue + results.append("true") # @step:enqueue + + elif operation == "dequeue": + if queue_size == 0: # @step:dequeue + results.append("empty") # @step:dequeue + else: + dequeued_value = buffer[front_index] # @step:dequeue + buffer[front_index] = None # @step:dequeue + if front_index == rear_index: # @step:dequeue + front_index = -1 # @step:dequeue + rear_index = -1 # @step:dequeue + else: + front_index = (front_index + 1) % capacity # @step:dequeue + queue_size -= 1 # @step:dequeue + results.append(str(dequeued_value)) # @step:dequeue + + elif operation == "front": + if front_index == -1: # @step:peek + results.append("empty") # @step:peek + else: + results.append(str(buffer[front_index])) # @step:peek + + elif operation == "rear": + if rear_index == -1: # @step:peek + results.append("empty") # @step:peek + else: + results.append(str(buffer[rear_index])) # @step:peek + + return results # @step:complete diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-queue/sources/design-circular-queue.ts b/src/algorithms/stacks-queues/queue-design/design-circular-queue/sources/design-circular-queue.ts new file mode 100644 index 00000000..5a77d169 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-queue/sources/design-circular-queue.ts @@ -0,0 +1,64 @@ +// Design Circular Queue — fixed-capacity ring buffer with front/rear pointers (LeetCode 622) +function designCircularQueue(operations: string[], capacity: number): string[] { + const buffer: (number | null)[] = new Array(capacity).fill(null); // @step:initialize + let frontIndex = -1; // @step:initialize + let rearIndex = -1; // @step:initialize + let queueSize = 0; // @step:initialize + const results: string[] = []; // @step:initialize + + for (let operationIndex = 0; operationIndex < operations.length; operationIndex++) { + const operation = operations[operationIndex]!; // @step:visit + + if (operation.startsWith("enqueue")) { + const valueStr = operation.split(" ")[1]!; // @step:enqueue + const value = parseInt(valueStr, 10); // @step:enqueue + + if (queueSize === capacity) { + // @step:enqueue + results.push("full"); // @step:enqueue + } else { + if (frontIndex === -1) { + // @step:enqueue + frontIndex = 0; // @step:enqueue + } + rearIndex = (rearIndex + 1) % capacity; // @step:enqueue + buffer[rearIndex] = value; // @step:enqueue + queueSize++; // @step:enqueue + results.push("true"); // @step:enqueue + } + } else if (operation === "dequeue") { + if (queueSize === 0) { + // @step:dequeue + results.push("empty"); // @step:dequeue + } else { + const dequeuedValue = buffer[frontIndex]!; // @step:dequeue + buffer[frontIndex] = null; // @step:dequeue + if (frontIndex === rearIndex) { + // @step:dequeue + frontIndex = -1; // @step:dequeue + rearIndex = -1; // @step:dequeue + } else { + frontIndex = (frontIndex + 1) % capacity; // @step:dequeue + } + queueSize--; // @step:dequeue + results.push(String(dequeuedValue)); // @step:dequeue + } + } else if (operation === "front") { + if (frontIndex === -1) { + // @step:peek + results.push("empty"); // @step:peek + } else { + results.push(String(buffer[frontIndex])); // @step:peek + } + } else if (operation === "rear") { + if (rearIndex === -1) { + // @step:peek + results.push("empty"); // @step:peek + } else { + results.push(String(buffer[rearIndex])); // @step:peek + } + } + } + + return results; // @step:complete +} diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-queue/step-generator.test.ts b/src/algorithms/stacks-queues/queue-design/design-circular-queue/step-generator.test.ts new file mode 100644 index 00000000..afcd1279 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-queue/step-generator.test.ts @@ -0,0 +1,119 @@ +import { describe, it, expect } from "vitest"; +import { generateDesignCircularQueueSteps } from "./step-generator"; +import type { StackQueueVisualState } from "@/types"; + +const DEFAULT_INPUT = { + operations: ["enqueue 1", "enqueue 2", "dequeue", "enqueue 3"], + capacity: 3, +}; + +describe("generateDesignCircularQueueSteps", () => { + it("produces steps for the default input", () => { + const steps = generateDesignCircularQueueSteps(DEFAULT_INPUT); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateDesignCircularQueueSteps(DEFAULT_INPUT); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateDesignCircularQueueSteps(DEFAULT_INPUT); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateDesignCircularQueueSteps(DEFAULT_INPUT); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateDesignCircularQueueSteps(DEFAULT_INPUT); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); + + it("includes a circular buffer in every visual state", () => { + const steps = generateDesignCircularQueueSteps(DEFAULT_INPUT); + for (const step of steps) { + const visualState = step.visualState as StackQueueVisualState; + expect(visualState.circularBuffer).toBeDefined(); + } + }); + + it("circular buffer has the correct capacity", () => { + const steps = generateDesignCircularQueueSteps(DEFAULT_INPUT); + const initialStep = steps[0]!; + const visualState = initialStep.visualState as StackQueueVisualState; + expect(visualState.circularBuffer?.capacity).toBe(DEFAULT_INPUT.capacity); + }); + + it("emits one enqueue step per successful enqueue operation", () => { + const steps = generateDesignCircularQueueSteps(DEFAULT_INPUT); + const enqueueSteps = steps.filter((step) => step.type === "enqueue"); + // DEFAULT_INPUT has 3 enqueue operations all succeeding + expect(enqueueSteps.length).toBe(3); + }); + + it("emits one dequeue step per successful dequeue operation", () => { + const steps = generateDesignCircularQueueSteps(DEFAULT_INPUT); + const dequeueSteps = steps.filter((step) => step.type === "dequeue"); + expect(dequeueSteps.length).toBe(1); + }); + + it("records results in the complete step variables", () => { + const steps = generateDesignCircularQueueSteps(DEFAULT_INPUT); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.variables["results"]).toEqual(["true", "true", "1", "true"]); + }); + + it("emits peek steps for full-queue enqueue attempts", () => { + const fullQueueInput = { + operations: ["enqueue 1", "enqueue 2", "enqueue 3", "enqueue 4"], + capacity: 3, + }; + const steps = generateDesignCircularQueueSteps(fullQueueInput); + const peekSteps = steps.filter((step) => step.type === "peek"); + expect(peekSteps.length).toBeGreaterThanOrEqual(1); + }); + + it("emits a peek step for dequeue-from-empty attempts", () => { + const emptyDequeueInput = { + operations: ["dequeue"], + capacity: 3, + }; + const steps = generateDesignCircularQueueSteps(emptyDequeueInput); + const peekSteps = steps.filter((step) => step.type === "peek"); + expect(peekSteps.length).toBe(1); + }); + + it("handles a single-operation input without errors", () => { + const singleOpInput = { operations: ["enqueue 42"], capacity: 2 }; + const steps = generateDesignCircularQueueSteps(singleOpInput); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("circular buffer rear index advances modularly on wrap-around", () => { + const wrapInput = { + operations: ["enqueue 1", "enqueue 2", "dequeue", "enqueue 3"], + capacity: 2, + }; + const steps = generateDesignCircularQueueSteps(wrapInput); + const enqueueSteps = steps.filter((step) => step.type === "enqueue"); + const lastEnqueue = enqueueSteps[enqueueSteps.length - 1]!; + const visualState = lastEnqueue.visualState as StackQueueVisualState; + // After dequeue from slot 0 then enqueue again, rear should be at slot 0 (wrapped) + expect(visualState.circularBuffer?.rearIndex).toBe(0); + }); + + it("capacity is set in initialize step variables", () => { + const steps = generateDesignCircularQueueSteps(DEFAULT_INPUT); + const initStep = steps[0]!; + expect(initStep.variables["capacity"]).toBe(DEFAULT_INPUT.capacity); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-design/design-circular-queue/step-generator.ts b/src/algorithms/stacks-queues/queue-design/design-circular-queue/step-generator.ts new file mode 100644 index 00000000..1c077d0f --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/design-circular-queue/step-generator.ts @@ -0,0 +1,135 @@ +/** Step generator for Design Circular Queue — produces ExecutionStep[] using QueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { QueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const DCQ_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.DESIGN_CIRCULAR_QUEUE!); + +export interface DesignCircularQueueInput { + operations: string[]; + capacity: number; +} + +export function generateDesignCircularQueueSteps(input: DesignCircularQueueInput): ExecutionStep[] { + const { operations, capacity } = input; + + // Local mirror of the ring buffer — used for variable snapshots and result derivation + const localBuffer: (number | null)[] = new Array(capacity).fill(null); + let frontIndex = -1; + let rearIndex = -1; + let queueSize = 0; + const results: string[] = []; + + const tracker = new QueueTracker([], DCQ_LINE_MAP); + tracker.initCircularBuffer(capacity); + + tracker.initialize({ + capacity, + frontIndex, + rearIndex, + queueSize, + results: [], + }); + + for (let operationIndex = 0; operationIndex < operations.length; operationIndex++) { + const operation = operations[operationIndex]!; + + if (operation.startsWith("enqueue")) { + const valueStr = operation.split(" ")[1]!; + const enqueueValue = parseInt(valueStr, 10); + + if (queueSize === capacity) { + results.push("full"); + tracker.peekFront( + { operationIndex, operation, frontIndex, rearIndex, queueSize, results: [...results] }, + `Enqueue ${valueStr} rejected — queue is full (size ${queueSize} = capacity ${capacity})`, + ); + } else { + if (frontIndex === -1) { + frontIndex = 0; + } + rearIndex = (rearIndex + 1) % capacity; + localBuffer[rearIndex] = enqueueValue; + queueSize++; + results.push("true"); + tracker.circularEnqueue( + enqueueValue, + { operationIndex, operation, frontIndex, rearIndex, queueSize, results: [...results] }, + `Enqueue ${valueStr} at slot ${rearIndex} — front: ${frontIndex}, rear: ${rearIndex}, size: ${queueSize}`, + ); + } + } else if (operation === "dequeue") { + if (queueSize === 0) { + results.push("empty"); + tracker.peekFront( + { operationIndex, operation, frontIndex, rearIndex, queueSize, results: [...results] }, + "Dequeue — queue is empty", + ); + } else { + const dequeuedValue = localBuffer[frontIndex] ?? 0; + localBuffer[frontIndex] = null; + const prevFront = frontIndex; + if (frontIndex === rearIndex) { + frontIndex = -1; + rearIndex = -1; + } else { + frontIndex = (frontIndex + 1) % capacity; + } + queueSize--; + results.push(String(dequeuedValue)); + tracker.circularDequeue( + { + operationIndex, + operation, + frontIndex, + rearIndex, + queueSize, + prevFront, + dequeuedValue, + results: [...results], + }, + `Dequeue ${String(dequeuedValue)} from slot ${prevFront} — front: ${frontIndex}, rear: ${rearIndex}, size: ${queueSize}`, + ); + } + } else if (operation === "front") { + const frontValue = frontIndex === -1 ? "empty" : String(localBuffer[frontIndex] ?? "empty"); + results.push(frontValue); + tracker.peekFront( + { + operationIndex, + operation, + frontIndex, + rearIndex, + queueSize, + peekValue: frontValue, + results: [...results], + }, + `Peek front — value: ${frontValue}, position: ${frontIndex}, size: ${queueSize}`, + ); + } else if (operation === "rear") { + const rearValue = rearIndex === -1 ? "empty" : String(localBuffer[rearIndex] ?? "empty"); + results.push(rearValue); + tracker.peekFront( + { + operationIndex, + operation, + frontIndex, + rearIndex, + queueSize, + peekValue: rearValue, + results: [...results], + }, + `Peek rear — value: ${rearValue}, position: ${rearIndex}, size: ${queueSize}`, + ); + } + } + + tracker.complete( + { frontIndex, rearIndex, queueSize, results }, + `All ${operations.length} operations complete — ${results.length} results produced`, + ); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/MovingAverageFromStreamPipeline.stories.tsx b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/MovingAverageFromStreamPipeline.stories.tsx new file mode 100644 index 00000000..ec759ca2 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/MovingAverageFromStreamPipeline.stories.tsx @@ -0,0 +1,98 @@ +/** + * Storybook stories for the Moving Average from Stream algorithm pipeline. + * Uses the real step generator with the default input, rendering the + * StackQueueVisualizer at key sliding-window states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateMovingAverageFromStreamSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateMovingAverageFromStreamSteps({ + values: [1, 10, 3, 5], + windowSize: 3, +}); + +const smallWindowSteps = generateMovingAverageFromStreamSteps({ + values: [4, 7, 2, 9, 1], + windowSize: 2, +}); + +const noEvictionSteps = generateMovingAverageFromStreamSteps({ + values: [2, 4, 6], + windowSize: 5, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Moving Average from Stream", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — empty queue, no values processed */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Window filling — queue has fewer than k elements, no eviction yet */ +export const WindowFilling: Story = { + args: { + visualState: (() => { + const enqueueSteps = defaultSteps.filter((step) => step.type === "enqueue"); + return enqueueSteps[1]!.visualState as StackQueueVisualState; + })(), + }, +}; + +/** Window full — k elements in the queue, sliding is about to begin */ +export const WindowFull: Story = { + args: { + visualState: (() => { + const enqueueSteps = defaultSteps.filter((step) => step.type === "enqueue"); + return enqueueSteps[2]!.visualState as StackQueueVisualState; + })(), + }, +}; + +/** After eviction — oldest element removed to maintain window size k */ +export const AfterEviction: Story = { + args: { + visualState: (() => { + const dequeueSteps = defaultSteps.filter((step) => step.type === "dequeue"); + return dequeueSteps[0]!.visualState as StackQueueVisualState; + })(), + }, +}; + +/** Complete — all values processed, averages computed */ +export const AllValuesProcessed: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Small window (k=2) — frequent evictions with many values */ +export const SmallWindow: Story = { + args: { + visualState: smallWindowSteps[smallWindowSteps.length - 1]! + .visualState as StackQueueVisualState, + }, +}; + +/** No eviction — stream shorter than window, queue grows freely */ +export const NoEviction: Story = { + args: { + visualState: noEvictionSteps[noEvictionSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/educational.ts b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/educational.ts new file mode 100644 index 00000000..84222b46 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/educational.ts @@ -0,0 +1,58 @@ +import type { EducationalContent } from "@/types"; + +export const movingAverageFromStreamEducational: EducationalContent = { + overview: + "**Moving Average from Data Stream** (LeetCode 346) computes a rolling average over the most recent `k` values in a stream of integers. A fixed-size queue acts as the sliding window: each new value is added to the rear, and once the window exceeds size `k`, the oldest value is evicted from the front.\n\nMaintaining a running sum alongside the queue makes each average computation `O(1)` — there is no need to sum all `k` elements on every update.", + + howItWorks: + "The algorithm processes each incoming value in three steps:\n\n" + + "1. **Enqueue** the new value and add it to `runningSum`.\n" + + "2. **Evict** — if the queue now holds more than `k` elements, shift the front value off and subtract it from `runningSum`.\n" + + "3. **Compute** the average: `runningSum / queue.length`.\n\n" + + "### Example trace (`values = [1, 10, 3, 5]`, `k = 3`)\n\n" + + "```\n" + + "value queue after enqueue evict? sum avg\n" + + " 1 [1] no 1 1.00\n" + + " 10 [1, 10] no 11 5.50\n" + + " 3 [1, 10, 3] no 14 4.67\n" + + " 5 [10, 3, 5] (evict 1) yes 18 6.00\n" + + "```\n\n" + + "The running sum avoids re-summing the window on every step, keeping each operation `O(1)`.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(1)` per value**\n\n" + + "Each value triggers exactly one enqueue and at most one dequeue. Both are constant-time array operations. The running sum is updated with a single addition and subtraction, so no loop over the window is needed.\n\n" + + "**Space Complexity: `O(k)`**\n\n" + + "The queue holds at most `k` elements at any time. The running sum and averages array use `O(n)` total, but the sliding window itself is bounded by `k`.", + + bestAndWorstCase: + "**Best case** — window size `k = 1`: every value is immediately the entire window, so no eviction occurs and each average equals the current value. Still `O(1)` per step.\n\n" + + "**Worst case** — `k` equals the full stream length: the queue grows to hold all `n` values and no evictions occur. Time is `O(n)` total, space is `O(n)`.\n\n" + + "In all cases, the per-element cost is `O(1)` because the running sum eliminates the need to iterate over the window.", + + realWorldUses: [ + "**Financial analysis:** Stock tickers and trading algorithms use moving averages over the last `k` seconds or minutes to smooth price noise and identify trends.", + "**Sensor smoothing:** IoT devices and embedded sensors apply sliding-window averages to reduce noise from temperature, pressure, or motion readings.", + "**Network monitoring:** Network tools compute moving averages of packet latency or bandwidth utilisation to detect anomalies without storing the entire history.", + "**Game development:** Frame-rate smoothers compute a rolling average of frame times to display stable FPS counters and make adaptive quality decisions.", + "**Machine learning preprocessing:** Streaming feature pipelines use sliding-window statistics to feed real-time models without batch recomputation.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(1) per update — the running sum avoids re-summing the entire window.", + "Fixed O(k) memory — the queue never grows beyond the window size.", + "Simple, readable implementation with no complex data structures.", + "Naturally handles streams of arbitrary length without storing all past values.", + ], + limitations: [ + "Requires knowing the window size `k` in advance — not suitable for variable-length windows without modification.", + "Equally weights all values in the window — exponential moving averages give more weight to recent data but require extra bookkeeping.", + "Queue shift (front removal) is O(1) with a deque but O(n) with a plain array — use a deque or ring buffer in performance-critical code.", + "Does not support out-of-order or missing values; every position in the stream is assumed to be valid.", + ], + }, + + whenToUseIt: + "Use a fixed-size queue with a running sum whenever you need a constant-time sliding-window average over a stream. If the window size is dynamic or you need weighted averaging, consider an exponential moving average instead. For very large windows where numerical precision matters, prefer compensated summation (Kahan summation) over a plain running sum to avoid floating-point drift.", +}; diff --git a/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/index.ts b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/index.ts new file mode 100644 index 00000000..40385140 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { movingAverageFromStream } from "./sources/moving-average-from-stream.ts?fn"; +import { generateMovingAverageFromStreamSteps } from "./step-generator"; +import type { MovingAverageFromStreamInput } from "./step-generator"; +import { movingAverageFromStreamEducational } from "./educational"; + +import typescriptSource from "./sources/moving-average-from-stream.ts?raw"; +import pythonSource from "./sources/moving-average-from-stream.py?raw"; +import javaSource from "./sources/MovingAverageFromStream.java?raw"; + +function executeMovingAverageFromStream(input: MovingAverageFromStreamInput): number[] { + return movingAverageFromStream(input.values, input.windowSize) as number[]; +} + +const movingAverageFromStreamDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.MOVING_AVERAGE_FROM_STREAM!, + name: "Moving Average from Stream", + category: CATEGORY.STACKS_QUEUES!, + technique: "queue-design", + description: + "Maintain a running average over the last k values using a fixed-size queue and a running sum — O(1) per value with O(k) space", + timeComplexity: { + best: "O(1)", + average: "O(1)", + worst: "O(1)", + }, + spaceComplexity: "O(k)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { values: [1, 10, 3, 5], windowSize: 3 }, + }, + execute: executeMovingAverageFromStream, + generateSteps: generateMovingAverageFromStreamSteps, + educational: movingAverageFromStreamEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(movingAverageFromStreamDefinition); diff --git a/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/moving-average-from-stream.test.ts b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/moving-average-from-stream.test.ts new file mode 100644 index 00000000..d44897f8 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/moving-average-from-stream.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from "vitest"; +import { movingAverageFromStream } from "./sources/moving-average-from-stream.ts?fn"; + +describe("movingAverageFromStream", () => { + it("returns correct averages for the default example ([1,10,3,5], k=3)", () => { + const result = movingAverageFromStream([1, 10, 3, 5], 3) as number[]; + expect(result[0]).toBeCloseTo(1); + expect(result[1]).toBeCloseTo(5.5); + expect(result[2]).toBeCloseTo(4.666666, 4); + expect(result[3]).toBeCloseTo(6); + }); + + it("returns each value as its own average when k=1", () => { + const result = movingAverageFromStream([4, 7, 2], 1) as number[]; + expect(result).toEqual([4, 7, 2]); + }); + + it("returns a single value when the stream has one element", () => { + const result = movingAverageFromStream([42], 3) as number[]; + expect(result).toEqual([42]); + }); + + it("returns the cumulative average when the stream is shorter than k", () => { + const result = movingAverageFromStream([2, 4], 5) as number[]; + expect(result[0]).toBeCloseTo(2); + expect(result[1]).toBeCloseTo(3); + }); + + it("handles a window size equal to stream length", () => { + const result = movingAverageFromStream([1, 2, 3], 3) as number[]; + expect(result[0]).toBeCloseTo(1); + expect(result[1]).toBeCloseTo(1.5); + expect(result[2]).toBeCloseTo(2); + }); + + it("evicts correctly with window k=2", () => { + const result = movingAverageFromStream([10, 20, 30, 40], 2) as number[]; + expect(result[0]).toBeCloseTo(10); + expect(result[1]).toBeCloseTo(15); + expect(result[2]).toBeCloseTo(25); + expect(result[3]).toBeCloseTo(35); + }); + + it("produces the correct number of averages", () => { + const values = [1, 2, 3, 4, 5]; + const result = movingAverageFromStream(values, 3) as number[]; + expect(result).toHaveLength(values.length); + }); + + it("handles all identical values", () => { + const result = movingAverageFromStream([5, 5, 5, 5], 3) as number[]; + for (const average of result) { + expect(average).toBeCloseTo(5); + } + }); +}); diff --git a/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/sources/MovingAverageFromStream.java b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/sources/MovingAverageFromStream.java new file mode 100644 index 00000000..045e3d47 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/sources/MovingAverageFromStream.java @@ -0,0 +1,26 @@ +// Moving Average from Data Stream — fixed-size sliding window queue (LeetCode 346) +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +public class MovingAverageFromStream { + public static List movingAverageFromStream(int[] values, int windowSize) { + Deque queue = new ArrayDeque<>(); // @step:initialize + double runningSum = 0; // @step:initialize + List averages = new ArrayList<>(); // @step:initialize + + for (int currentValue : values) { // @step:visit + queue.addLast(currentValue); // @step:enqueue + runningSum += currentValue; // @step:enqueue + + if (queue.size() > windowSize) { // @step:dequeue + runningSum -= queue.removeFirst(); // @step:dequeue + } + + averages.add(runningSum / queue.size()); // @step:complete + } + + return averages; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/sources/moving-average-from-stream.py b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/sources/moving-average-from-stream.py new file mode 100644 index 00000000..9274c7c2 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/sources/moving-average-from-stream.py @@ -0,0 +1,20 @@ +# Moving Average from Data Stream — fixed-size sliding window queue (LeetCode 346) +from typing import List +from collections import deque + + +def moving_average_from_stream(values: List[int], window_size: int) -> List[float]: + queue: deque = deque() # @step:initialize + running_sum: float = 0 # @step:initialize + averages: List[float] = [] # @step:initialize + + for current_value in values: # @step:visit + queue.append(current_value) # @step:enqueue + running_sum += current_value # @step:enqueue + + if len(queue) > window_size: # @step:dequeue + running_sum -= queue.popleft() # @step:dequeue + + averages.append(running_sum / len(queue)) # @step:complete + + return averages # @step:complete diff --git a/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/sources/moving-average-from-stream.ts b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/sources/moving-average-from-stream.ts new file mode 100644 index 00000000..2819a29f --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/sources/moving-average-from-stream.ts @@ -0,0 +1,22 @@ +// Moving Average from Data Stream — fixed-size sliding window queue (LeetCode 346) +function movingAverageFromStream(values: number[], windowSize: number): number[] { + const queue: number[] = []; // @step:initialize + let runningSum = 0; // @step:initialize + const averages: number[] = []; // @step:initialize + + for (let valueIndex = 0; valueIndex < values.length; valueIndex++) { + const currentValue = values[valueIndex]!; // @step:visit + + queue.push(currentValue); // @step:enqueue + runningSum += currentValue; // @step:enqueue + + if (queue.length > windowSize) { + // @step:dequeue + runningSum -= queue.shift()!; // @step:dequeue + } + + averages.push(runningSum / queue.length); // @step:complete + } + + return averages; // @step:complete +} diff --git a/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/step-generator.test.ts b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/step-generator.test.ts new file mode 100644 index 00000000..2d815c31 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/step-generator.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect } from "vitest"; +import { generateMovingAverageFromStreamSteps } from "./step-generator"; + +describe("generateMovingAverageFromStreamSteps", () => { + it("produces steps for the default input", () => { + const steps = generateMovingAverageFromStreamSteps({ values: [1, 10, 3, 5], windowSize: 3 }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateMovingAverageFromStreamSteps({ values: [1, 10, 3, 5], windowSize: 3 }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateMovingAverageFromStreamSteps({ values: [1, 10, 3, 5], windowSize: 3 }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateMovingAverageFromStreamSteps({ values: [1, 10, 3, 5], windowSize: 3 }); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateMovingAverageFromStreamSteps({ values: [1, 10, 3, 5], windowSize: 3 }); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); + + it("emits one visit step per value", () => { + const steps = generateMovingAverageFromStreamSteps({ values: [1, 10, 3, 5], windowSize: 3 }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps).toHaveLength(4); + }); + + it("emits one enqueue step per value", () => { + const steps = generateMovingAverageFromStreamSteps({ values: [1, 10, 3, 5], windowSize: 3 }); + const enqueueSteps = steps.filter((step) => step.type === "enqueue"); + expect(enqueueSteps).toHaveLength(4); + }); + + it("emits dequeue steps only when the window overflows", () => { + // [1,10,3,5] with k=3 — overflow happens once (at value 5) + const steps = generateMovingAverageFromStreamSteps({ values: [1, 10, 3, 5], windowSize: 3 }); + const dequeueSteps = steps.filter((step) => step.type === "dequeue"); + expect(dequeueSteps).toHaveLength(1); + }); + + it("emits no dequeue steps when stream is shorter than window", () => { + const steps = generateMovingAverageFromStreamSteps({ values: [1, 2], windowSize: 5 }); + const dequeueSteps = steps.filter((step) => step.type === "dequeue"); + expect(dequeueSteps).toHaveLength(0); + }); + + it("emits a dequeue step for each element beyond the window", () => { + // 6 values with k=2 — dequeue happens 4 times + const steps = generateMovingAverageFromStreamSteps({ + values: [1, 2, 3, 4, 5, 6], + windowSize: 2, + }); + const dequeueSteps = steps.filter((step) => step.type === "dequeue"); + expect(dequeueSteps).toHaveLength(4); + }); + + it("records windowSize and totalValues in the complete step variables", () => { + const steps = generateMovingAverageFromStreamSteps({ values: [1, 10, 3, 5], windowSize: 3 }); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.variables).toMatchObject({ windowSize: 3, totalValues: 4 }); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/step-generator.ts b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/step-generator.ts new file mode 100644 index 00000000..02018417 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/moving-average-from-stream/step-generator.ts @@ -0,0 +1,87 @@ +/** Step generator for Moving Average from Stream — produces ExecutionStep[] using QueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { QueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const MAFS_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.MOVING_AVERAGE_FROM_STREAM!); + +export interface MovingAverageFromStreamInput { + values: number[]; + windowSize: number; +} + +export function generateMovingAverageFromStreamSteps( + input: MovingAverageFromStreamInput, +): ExecutionStep[] { + const { values, windowSize } = input; + + const localQueue: number[] = []; + let runningSum = 0; + const averages: number[] = []; + + const tracker = new QueueTracker(values, MAFS_LINE_MAP); + + tracker.initialize({ + windowSize, + queueSize: 0, + runningSum, + averages: [], + }); + + for (let valueIndex = 0; valueIndex < values.length; valueIndex++) { + const currentValue = values[valueIndex]!; + + tracker.processElement(valueIndex, { + valueIndex, + currentValue, + queueSize: localQueue.length, + runningSum, + averages: [...averages], + }); + + localQueue.push(currentValue); + runningSum += currentValue; + + tracker.enqueue( + String(currentValue), + { + valueIndex, + currentValue, + queueSize: localQueue.length, + runningSum, + averages: [...averages], + }, + `Enqueue ${currentValue} — running sum is now ${runningSum}`, + ); + + if (localQueue.length > windowSize) { + const evicted = localQueue[0]!; + runningSum -= evicted; + localQueue.shift(); + + tracker.dequeue( + { + valueIndex, + currentValue, + evicted, + queueSize: localQueue.length, + runningSum, + averages: [...averages], + }, + `Dequeue ${evicted} — window full, running sum adjusted to ${runningSum}`, + ); + } + + const currentAverage = runningSum / localQueue.length; + averages.push(currentAverage); + } + + tracker.complete( + { windowSize, averages, totalValues: values.length }, + `All ${values.length} values processed — ${averages.length} averages computed`, + ); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/queue-design/task-scheduler/TaskSchedulerPipeline.stories.tsx b/src/algorithms/stacks-queues/queue-design/task-scheduler/TaskSchedulerPipeline.stories.tsx new file mode 100644 index 00000000..b70b0c29 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/task-scheduler/TaskSchedulerPipeline.stories.tsx @@ -0,0 +1,64 @@ +/** + * Storybook stories for the Task Scheduler algorithm pipeline. + * Uses the real step generator with ["A","A","A","B","B","B"] and cooldown 2, + * rendering the StackQueueVisualizer at key scheduling states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateTaskSchedulerSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateTaskSchedulerSteps({ + tasks: ["A", "A", "A", "B", "B", "B"], + cooldown: 2, +}); + +const zeroCooldownSteps = generateTaskSchedulerSteps({ + tasks: ["A", "A", "A", "B", "B", "B"], + cooldown: 0, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Task Scheduler", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — frequency map computed, no tasks scheduled yet */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-scheduling — some tasks in the cooldown queue, others ready to execute */ +export const MidScheduling: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Scheduling complete — all 8 intervals used for the default input */ +export const SchedulingComplete: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Zero cooldown — tasks run back-to-back with no idle slots */ +export const ZeroCooldown: Story = { + args: { + visualState: zeroCooldownSteps[zeroCooldownSteps.length - 1]! + .visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/queue-design/task-scheduler/educational.ts b/src/algorithms/stacks-queues/queue-design/task-scheduler/educational.ts new file mode 100644 index 00000000..52e59488 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/task-scheduler/educational.ts @@ -0,0 +1,68 @@ +import type { EducationalContent } from "@/types"; + +export const taskSchedulerEducational: EducationalContent = { + overview: + "**Task Scheduler** (LeetCode 621) computes the minimum number of CPU intervals needed to execute all tasks given a cooldown period `n`. The same task type cannot be executed again within `n` intervals — idle slots pad the schedule when no task is ready.\n\nA queue acts as a cooldown waiting area: after executing a task, its entry is enqueued with a timestamp indicating when it will be available again. The greedy strategy always executes the highest-frequency available task, minimising wasted idle time.", + + howItWorks: + "**Step 1 — Count frequencies**\n\n" + + "Build a frequency map from the task list. The task with the highest frequency (`maxFreq`) determines the frame structure.\n\n" + + "**Step 2 — Identify the dominant frame**\n\n" + + "A *frame* is `cooldown + 1` slots wide — one execution slot plus `cooldown` waiting slots. The most frequent task anchors the start of each frame. `maxFreqCount` is the number of tasks that share the maximum frequency.\n\n" + + "**Step 3 — Apply the greedy formula**\n\n" + + "```\n" + + "result = max(tasks.length, (maxFreq - 1) × (cooldown + 1) + maxFreqCount)\n" + + "```\n\n" + + "- `(maxFreq - 1) × (cooldown + 1)` — all frames except the last, each fully padded to width `cooldown + 1`.\n" + + "- `+ maxFreqCount` — the final partial frame only needs one slot per max-frequency task.\n" + + "- `max(tasks.length, ...)` — when tasks are dense enough, idle slots disappear and the schedule is exactly `tasks.length` long.\n\n" + + "**Step 4 — Simulate with a cooldown queue (for visualisation)**\n\n" + + "1. Maintain a max-frequency priority queue of ready tasks.\n" + + "2. At each time unit: release any cooling tasks whose wait has ended, then execute the highest-frequency ready task and push it to the cooldown queue with `availableAt = currentTime + cooldown + 1`.\n" + + "3. Repeat until all tasks are exhausted.\n\n" + + "### Example: `tasks = [A,A,A,B,B,B]`, `n = 2`\n\n" + + "```\n" + + "freqA = 3, freqB = 3 → maxFreq = 3, maxFreqCount = 2\n" + + "formula = (3-1) × (2+1) + 2 = 6 + 2 = 8\n" + + "max(6, 8) = 8\n\n" + + "Schedule: A B idle A B idle A B\n" + + "Time: 1 2 3 4 5 6 7 8\n" + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Building the frequency map requires one pass over the `n` tasks. Extracting `maxFreq` and `maxFreqCount` requires one pass over the (at most 26-letter) frequency map — effectively `O(1)` since the alphabet is fixed. The closed-form formula then computes the answer in constant time.\n\n" + + "**Space Complexity: `O(1)`**\n\n" + + "The frequency map holds at most 26 entries (one per uppercase letter), so it occupies constant space regardless of input size. No auxiliary arrays proportional to `n` are needed.", + + bestAndWorstCase: + "**Best case** — `cooldown = 0`: every task can run consecutively with no waiting, so the answer is simply `tasks.length`. No idle slots are ever inserted.\n\n" + + "**Worst case** — one task dominates with very high frequency and a large cooldown: the schedule has many idle slots and the result is `(maxFreq - 1) × (cooldown + 1) + 1`, which can be much larger than `tasks.length`.\n\n" + + "For example, `tasks = [A, A, A]`, `n = 100`: result = `(3-1) × 101 + 1 = 203`, versus only 3 tasks.", + + realWorldUses: [ + "**CPU scheduling:** Operating systems rate-limit repeated requests from the same process to prevent starvation of other processes — the cooldown models the minimum interval between identical system calls.", + "**API rate limiting:** Web services enforce per-endpoint cooldowns to protect backend resources; the scheduler formula determines the minimum total time to serve a batch of requests.", + "**Job queues:** Distributed task runners (Celery, Sidekiq) use cooldown periods to avoid thundering-herd problems when the same expensive job recurs frequently.", + "**Battery/thermal throttling:** Mobile and embedded systems delay re-execution of power-hungry tasks after they run to prevent overheating — the same frame-based scheduling applies.", + "**Database connection pools:** Some pooling strategies enforce a minimum idle period between reuse of the same connection to avoid holding transactions open too long.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(n) time — single frequency pass plus a constant-time closed-form formula.", + "O(1) space — the frequency map is bounded by the fixed alphabet size (26 uppercase letters).", + "Exact optimal result — no approximation; the formula gives the provably minimum schedule length.", + "Intuitive: the dominant task's frame structure directly determines the answer.", + ], + limitations: [ + "Assumes tasks are single-unit atomic operations — cannot model variable-duration tasks.", + "The formula applies only when tasks are identified by type (letter); it does not generalise to arbitrary dependency graphs.", + "Idle slots are implicit — the formula counts them but does not name which task type goes in each slot.", + "For multi-core scheduling with parallelism, a more complex model is required.", + ], + }, + + whenToUseIt: + "Use this greedy formula whenever you need the minimum time to schedule a multiset of tasks subject to per-type cooldown constraints and single-CPU execution. If tasks have dependencies (must-run-before relationships), use a topological sort instead. If multiple CPUs are available, the problem changes significantly and this formula no longer applies directly.", +}; diff --git a/src/algorithms/stacks-queues/queue-design/task-scheduler/index.ts b/src/algorithms/stacks-queues/queue-design/task-scheduler/index.ts new file mode 100644 index 00000000..a20a0ec8 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/task-scheduler/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { taskSchedulerQueue } from "./sources/task-scheduler.ts?fn"; +import { generateTaskSchedulerSteps } from "./step-generator"; +import type { TaskSchedulerInput } from "./step-generator"; +import { taskSchedulerEducational } from "./educational"; + +import typescriptSource from "./sources/task-scheduler.ts?raw"; +import pythonSource from "./sources/task-scheduler.py?raw"; +import javaSource from "./sources/TaskScheduler.java?raw"; + +function executeTaskScheduler(input: TaskSchedulerInput): number { + return taskSchedulerQueue(input.tasks, input.cooldown) as number; +} + +const taskSchedulerDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.TASK_SCHEDULER!, + name: "Task Scheduler", + category: CATEGORY.STACKS_QUEUES!, + technique: "queue-design", + description: + "Compute the minimum CPU intervals to execute all tasks with a cooldown period — uses a queue as a waiting area and a greedy frequency formula for O(n) time and O(1) space", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(1)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { tasks: ["A", "A", "A", "B", "B", "B"], cooldown: 2 }, + }, + execute: executeTaskScheduler, + generateSteps: generateTaskSchedulerSteps, + educational: taskSchedulerEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(taskSchedulerDefinition); diff --git a/src/algorithms/stacks-queues/queue-design/task-scheduler/sources/TaskScheduler.java b/src/algorithms/stacks-queues/queue-design/task-scheduler/sources/TaskScheduler.java new file mode 100644 index 00000000..951d00d3 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/task-scheduler/sources/TaskScheduler.java @@ -0,0 +1,59 @@ +// Task Scheduler — greedy formula with cooldown queue simulation (LeetCode 621) +import java.util.*; + +public class TaskScheduler { + public static int taskSchedulerQueue(String[] tasks, int cooldown) { + Map freqMap = new HashMap<>(); // @step:initialize + for (String task : tasks) { // @step:initialize + freqMap.put(task, freqMap.getOrDefault(task, 0) + 1); // @step:initialize + } + + int maxFreq = 0; // @step:initialize + int maxFreqCount = 0; // @step:initialize + + for (int freq : freqMap.values()) { // @step:visit + if (freq > maxFreq) { // @step:compare + maxFreq = freq; // @step:compare + maxFreqCount = 1; // @step:compare + } else if (freq == maxFreq) { // @step:compare + maxFreqCount++; // @step:compare + } + } + + // Queue holds [taskName, remainingFreq, availableAtTime] for cooling-down tasks + Queue cooldownQueue = new LinkedList<>(); // @step:enqueue + + // Sorted descending by frequency — acts as a max-heap + List taskHeap = new ArrayList<>(); // @step:enqueue + for (Map.Entry entry : freqMap.entrySet()) { // @step:enqueue + taskHeap.add(new int[]{entry.getValue()}); // @step:enqueue + } + taskHeap.sort((entryA, entryB) -> entryB[0] - entryA[0]); // @step:enqueue + + int currentTime = 0; // @step:enqueue + + while (!taskHeap.isEmpty() || !cooldownQueue.isEmpty()) { // @step:visit + currentTime++; // @step:visit + + // Release tasks from the cooldown queue when their wait is over + if (!cooldownQueue.isEmpty() && cooldownQueue.peek()[2] <= currentTime) { // @step:dequeue + int[] released = cooldownQueue.poll(); // @step:dequeue + taskHeap.add(new int[]{released[1]}); // @step:dequeue + taskHeap.sort((entryA, entryB) -> entryB[0] - entryA[0]); // @step:dequeue + } + + // Execute the highest-frequency available task and enqueue it to cool down + if (!taskHeap.isEmpty()) { // @step:enqueue + int[] topEntry = taskHeap.remove(0); // @step:enqueue + int remainingFreq = topEntry[0] - 1; // @step:enqueue + if (remainingFreq > 0) { // @step:enqueue + cooldownQueue.add(new int[]{0, remainingFreq, currentTime + cooldown + 1}); // @step:enqueue + } + } + } + + // Greedy formula — closed-form solution is equivalent to the simulation result + int formulaResult = (maxFreq - 1) * (cooldown + 1) + maxFreqCount; // @step:complete + return Math.max(tasks.length, formulaResult); // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/queue-design/task-scheduler/sources/task-scheduler.py b/src/algorithms/stacks-queues/queue-design/task-scheduler/sources/task-scheduler.py new file mode 100644 index 00000000..f17daed2 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/task-scheduler/sources/task-scheduler.py @@ -0,0 +1,52 @@ +# Task Scheduler — greedy formula with cooldown queue simulation (LeetCode 621) +from collections import deque +from typing import List, Tuple + + +def task_scheduler_queue(tasks: List[str], cooldown: int) -> int: + freq_map: dict[str, int] = {} # @step:initialize + for task in tasks: # @step:initialize + freq_map[task] = freq_map.get(task, 0) + 1 # @step:initialize + + max_freq: int = 0 # @step:initialize + max_freq_count: int = 0 # @step:initialize + + for freq in freq_map.values(): # @step:visit + if freq > max_freq: # @step:compare + max_freq = freq # @step:compare + max_freq_count = 1 # @step:compare + elif freq == max_freq: # @step:compare + max_freq_count += 1 # @step:compare + + # Queue holds (taskName, remainingFreq, availableAtTime) for cooling-down tasks + cooldown_queue: deque[Tuple[str, int, int]] = deque() # @step:enqueue + + # Sorted descending by frequency — acts as a max-heap + task_heap = sorted( + [{"task": task, "freq": freq} for task, freq in freq_map.items()], + key=lambda entry: -entry["freq"], + ) # @step:enqueue + + current_time: int = 0 # @step:enqueue + + while task_heap or cooldown_queue: # @step:visit + current_time += 1 # @step:visit + + # Release tasks from the cooldown queue when their wait is over + if cooldown_queue and cooldown_queue[0][2] <= current_time: # @step:dequeue + task_name, remaining_freq, _ = cooldown_queue.popleft() # @step:dequeue + task_heap.append({"task": task_name, "freq": remaining_freq}) # @step:dequeue + task_heap.sort(key=lambda entry: -entry["freq"]) # @step:dequeue + + # Execute the highest-frequency available task and enqueue it to cool down + if task_heap: # @step:enqueue + top_entry = task_heap.pop(0) # @step:enqueue + remaining_freq = top_entry["freq"] - 1 # @step:enqueue + if remaining_freq > 0: # @step:enqueue + cooldown_queue.append( # @step:enqueue + (top_entry["task"], remaining_freq, current_time + cooldown + 1) + ) # @step:enqueue + + # Greedy formula — closed-form solution is equivalent to the simulation result + formula_result = (max_freq - 1) * (cooldown + 1) + max_freq_count # @step:complete + return max(len(tasks), formula_result) # @step:complete diff --git a/src/algorithms/stacks-queues/queue-design/task-scheduler/sources/task-scheduler.ts b/src/algorithms/stacks-queues/queue-design/task-scheduler/sources/task-scheduler.ts new file mode 100644 index 00000000..ca542f19 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/task-scheduler/sources/task-scheduler.ts @@ -0,0 +1,62 @@ +// Task Scheduler — greedy formula with cooldown queue simulation (LeetCode 621) +function taskSchedulerQueue(tasks: string[], cooldown: number): number { + const freqMap: Map = new Map(); // @step:initialize + for (const task of tasks) { + // @step:initialize + freqMap.set(task, (freqMap.get(task) ?? 0) + 1); // @step:initialize + } + + let maxFreq = 0; // @step:initialize + let maxFreqCount = 0; // @step:initialize + + for (const freq of freqMap.values()) { + // @step:visit + if (freq > maxFreq) { + // @step:compare + maxFreq = freq; // @step:compare + maxFreqCount = 1; // @step:compare + } else if (freq === maxFreq) { + // @step:compare + maxFreqCount++; // @step:compare + } + } + + // Queue holds [taskName, remainingFreq, availableAtTime] for cooling-down tasks + const cooldownQueue: [string, number, number][] = []; // @step:enqueue + + // Sorted descending by frequency — acts as a max-heap + const taskHeap = Array.from(freqMap.entries()) + .map(([task, freq]) => ({ task, freq })) + .sort((entryA, entryB) => entryB.freq - entryA.freq); // @step:enqueue + + let currentTime = 0; // @step:enqueue + + while (taskHeap.length > 0 || cooldownQueue.length > 0) { + // @step:visit + currentTime++; // @step:visit + + // Release tasks from the cooldown queue when their wait is over + const nextEntry = cooldownQueue[0]; // @step:dequeue + if (nextEntry !== undefined && nextEntry[2] <= currentTime) { + // @step:dequeue + cooldownQueue.shift(); // @step:dequeue + taskHeap.push({ task: nextEntry[0], freq: nextEntry[1] }); // @step:dequeue + taskHeap.sort((entryA, entryB) => entryB.freq - entryA.freq); // @step:dequeue + } + + // Execute the highest-frequency available task and enqueue it to cool down + const topEntry = taskHeap.shift(); // @step:enqueue + if (topEntry !== undefined) { + // @step:enqueue + const remainingFreq = topEntry.freq - 1; // @step:enqueue + if (remainingFreq > 0) { + // @step:enqueue + cooldownQueue.push([topEntry.task, remainingFreq, currentTime + cooldown + 1]); // @step:enqueue + } + } + } + + // Greedy formula — closed-form solution is equivalent to the simulation result + const formulaResult = (maxFreq - 1) * (cooldown + 1) + maxFreqCount; // @step:complete + return Math.max(tasks.length, formulaResult); // @step:complete +} diff --git a/src/algorithms/stacks-queues/queue-design/task-scheduler/step-generator.test.ts b/src/algorithms/stacks-queues/queue-design/task-scheduler/step-generator.test.ts new file mode 100644 index 00000000..f8d6b2a6 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/task-scheduler/step-generator.test.ts @@ -0,0 +1,105 @@ +import { describe, it, expect } from "vitest"; +import { generateTaskSchedulerSteps } from "./step-generator"; +import type { StackQueueVisualState } from "@/types"; + +const DEFAULT_INPUT = { + tasks: ["A", "A", "A", "B", "B", "B"], + cooldown: 2, +}; + +describe("generateTaskSchedulerSteps", () => { + it("produces steps for the default input", () => { + const steps = generateTaskSchedulerSteps(DEFAULT_INPUT); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateTaskSchedulerSteps(DEFAULT_INPUT); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateTaskSchedulerSteps(DEFAULT_INPUT); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateTaskSchedulerSteps(DEFAULT_INPUT); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateTaskSchedulerSteps(DEFAULT_INPUT); + for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { + expect(steps[stepIndex]?.index).toBe(stepIndex); + } + }); + + it("records totalTime in the complete step variables", () => { + const steps = generateTaskSchedulerSteps(DEFAULT_INPUT); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.variables["totalTime"]).toBe(8); + }); + + it("records formulaResult in the complete step variables", () => { + const steps = generateTaskSchedulerSteps(DEFAULT_INPUT); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.variables["formulaResult"]).toBe(8); + }); + + it("records maxFreq and maxFreqCount in initialize step variables", () => { + const steps = generateTaskSchedulerSteps(DEFAULT_INPUT); + const initStep = steps[0]!; + expect(initStep.variables["maxFreq"]).toBe(3); + expect(initStep.variables["maxFreqCount"]).toBe(2); + }); + + it("emits enqueue steps as tasks enter the cooldown waiting area", () => { + const steps = generateTaskSchedulerSteps(DEFAULT_INPUT); + const enqueueSteps = steps.filter((step) => step.type === "enqueue"); + expect(enqueueSteps.length).toBeGreaterThan(0); + }); + + it("emits dequeue steps as tasks are released from cooldown", () => { + const steps = generateTaskSchedulerSteps(DEFAULT_INPUT); + const dequeueSteps = steps.filter((step) => step.type === "dequeue"); + expect(dequeueSteps.length).toBeGreaterThan(0); + }); + + it("produces visit steps during the frequency counting phase", () => { + const steps = generateTaskSchedulerSteps(DEFAULT_INPUT); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBeGreaterThan(0); + }); + + it("handles cooldown 0 — no enqueue steps into the cooldown queue", () => { + const noCooldownInput = { tasks: ["A", "A", "A", "B", "B", "B"], cooldown: 0 }; + const steps = generateTaskSchedulerSteps(noCooldownInput); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.variables["totalTime"]).toBe(6); + }); + + it("handles a single task type", () => { + const singleTaskInput = { tasks: ["A", "A", "A"], cooldown: 2 }; + const steps = generateTaskSchedulerSteps(singleTaskInput); + const completeStep = steps[steps.length - 1]!; + // maxFreq=3, maxFreqCount=1: formula=(3-1)*(2+1)+1=7; totalTime=max(3,7)=7 + expect(completeStep.variables["totalTime"]).toBe(7); + }); + + it("records cooldown parameter in initialize step variables", () => { + const steps = generateTaskSchedulerSteps(DEFAULT_INPUT); + const initStep = steps[0]!; + expect(initStep.variables["cooldown"]).toBe(DEFAULT_INPUT.cooldown); + }); + + it("all visual states have defined queue elements", () => { + const steps = generateTaskSchedulerSteps(DEFAULT_INPUT); + for (const step of steps) { + const visualState = step.visualState as StackQueueVisualState; + expect(visualState.queueElements).toBeDefined(); + } + }); +}); diff --git a/src/algorithms/stacks-queues/queue-design/task-scheduler/step-generator.ts b/src/algorithms/stacks-queues/queue-design/task-scheduler/step-generator.ts new file mode 100644 index 00000000..b5736e6f --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/task-scheduler/step-generator.ts @@ -0,0 +1,143 @@ +/** Step generator for Task Scheduler — produces ExecutionStep[] using QueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { QueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const TS_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.TASK_SCHEDULER!); + +export interface TaskSchedulerInput { + tasks: string[]; + cooldown: number; +} + +export function generateTaskSchedulerSteps(input: TaskSchedulerInput): ExecutionStep[] { + const { tasks, cooldown } = input; + + // Build frequency map + const freqMap = new Map(); + for (const task of tasks) { + freqMap.set(task, (freqMap.get(task) ?? 0) + 1); + } + + let maxFreq = 0; + let maxFreqCount = 0; + for (const freq of freqMap.values()) { + if (freq > maxFreq) { + maxFreq = freq; + maxFreqCount = 1; + } else if (freq === maxFreq) { + maxFreqCount++; + } + } + + const formulaResult = (maxFreq - 1) * (cooldown + 1) + maxFreqCount; + const totalTime = Math.max(tasks.length, formulaResult); + + const tracker = new QueueTracker([], TS_LINE_MAP); + + tracker.initialize({ + tasks: [...tasks], + cooldown, + maxFreq, + maxFreqCount, + formulaResult, + totalTime, + }); + + // Simulate round-robin scheduling across frames to show the queue in action + // cooldownWaiting entries store the remaining frequency alongside the availability time + const cooldownWaiting: Array<{ task: string; remainingFreq: number; availableAt: number }> = []; + + // Ready tasks sorted descending by remaining frequency (acts as a max-heap) + const readyTasks: Array<{ task: string; freq: number }> = Array.from(freqMap.entries()).map( + ([task, freq]) => ({ task, freq }), + ); + readyTasks.sort((entryA, entryB) => entryB.freq - entryA.freq); + + let currentTime = 0; + + // Visit each unique task to show frequency counting phase + for (const [task, freq] of freqMap.entries()) { + tracker.processElement(0, { + task, + freq, + maxFreq, + maxFreqCount, + freqSnapshot: Object.fromEntries(freqMap), + }); + } + + // Simulate scheduling with queue visualization + while (readyTasks.length > 0 || cooldownWaiting.length > 0) { + currentTime++; + + // Release tasks from the cooldown waiting area when their cooldown has elapsed + const readyIdx = cooldownWaiting.findIndex((entry) => entry.availableAt <= currentTime); + if (readyIdx !== -1) { + const releasedEntry = cooldownWaiting.splice(readyIdx, 1)[0]!; + readyTasks.push({ task: releasedEntry.task, freq: releasedEntry.remainingFreq }); + readyTasks.sort((entryA, entryB) => entryB.freq - entryA.freq); + tracker.dequeue( + { + currentTime, + releasedTask: releasedEntry.task, + releasedFreq: releasedEntry.remainingFreq, + cooldownQueueSize: cooldownWaiting.length, + readyQueueSize: readyTasks.length, + }, + `Release task '${releasedEntry.task}' from cooldown — available at time ${currentTime}`, + ); + } + + // Execute the highest-frequency ready task and enqueue it to cool down if still needed + const topEntry = readyTasks.shift(); + if (topEntry !== undefined) { + const newFreq = topEntry.freq - 1; + + if (newFreq > 0) { + const availableAt = currentTime + cooldown + 1; + cooldownWaiting.push({ task: topEntry.task, remainingFreq: newFreq, availableAt }); + tracker.enqueue( + topEntry.task, + { + currentTime, + executedTask: topEntry.task, + remainingFreq: newFreq, + cooldownUntil: availableAt, + cooldownQueueSize: cooldownWaiting.length, + readyQueueSize: readyTasks.length, + }, + `Execute task '${topEntry.task}' at time ${currentTime} — enqueue to cooldown until time ${availableAt}`, + ); + } else { + tracker.enqueue( + topEntry.task, + { + currentTime, + executedTask: topEntry.task, + remainingFreq: 0, + cooldownQueueSize: cooldownWaiting.length, + readyQueueSize: readyTasks.length, + }, + `Execute task '${topEntry.task}' at time ${currentTime} — all occurrences complete`, + ); + } + } + } + + tracker.complete( + { + totalTime, + formulaResult, + tasksLength: tasks.length, + maxFreq, + maxFreqCount, + cooldown, + }, + `Scheduling complete — minimum intervals: max(${tasks.length}, (${maxFreq}-1)×(${cooldown}+1)+${maxFreqCount}) = ${totalTime}`, + ); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/queue-design/task-scheduler/task-scheduler.test.ts b/src/algorithms/stacks-queues/queue-design/task-scheduler/task-scheduler.test.ts new file mode 100644 index 00000000..0187bfa2 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-design/task-scheduler/task-scheduler.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from "vitest"; +import { taskSchedulerQueue } from "./sources/task-scheduler.ts?fn"; + +describe("taskSchedulerQueue", () => { + it("returns 8 for the canonical example with cooldown 2", () => { + expect(taskSchedulerQueue(["A", "A", "A", "B", "B", "B"], 2)).toBe(8); + }); + + it("returns tasks.length when tasks are dense enough to fill all slots", () => { + // A×2, B×2, C×2 with cooldown 1 — no idle slots needed + expect(taskSchedulerQueue(["A", "A", "B", "B", "C", "C"], 1)).toBe(6); + }); + + it("returns tasks.length when cooldown is 0", () => { + expect(taskSchedulerQueue(["A", "A", "A", "B", "B", "B"], 0)).toBe(6); + }); + + it("handles a single task type with high frequency and large cooldown", () => { + // (3-1)*(100+1)+1 = 203 + expect(taskSchedulerQueue(["A", "A", "A"], 100)).toBe(203); + }); + + it("handles a single task occurrence", () => { + expect(taskSchedulerQueue(["A"], 5)).toBe(1); + }); + + it("handles two task types with equal frequency", () => { + // maxFreq=2, maxFreqCount=2, cooldown=2 → (2-1)*(3)+2 = 5 + expect(taskSchedulerQueue(["A", "A", "B", "B"], 2)).toBe(5); + }); + + it("handles all identical tasks with zero cooldown", () => { + expect(taskSchedulerQueue(["A", "A", "A", "A"], 0)).toBe(4); + }); + + it("returns at least tasks.length for any input", () => { + const result = taskSchedulerQueue(["A", "B", "C", "D", "E", "F"], 3); + expect(result).toBeGreaterThanOrEqual(6); + }); + + it("handles many distinct task types so idle slots are impossible", () => { + // 26 distinct tasks, cooldown 25 — exactly 26 tasks fill all slots + const distinctTasks = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); + expect(taskSchedulerQueue(distinctTasks, 25)).toBe(26); + }); + + it("handles repeated application of the same input consistently", () => { + const firstResult = taskSchedulerQueue(["A", "A", "A", "B", "B", "B"], 2); + const secondResult = taskSchedulerQueue(["A", "A", "A", "B", "B", "B"], 2); + expect(firstResult).toBe(secondResult); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/ImplementQueueUsingStacksPipeline.stories.tsx b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/ImplementQueueUsingStacksPipeline.stories.tsx new file mode 100644 index 00000000..d8ef83e3 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/ImplementQueueUsingStacksPipeline.stories.tsx @@ -0,0 +1,77 @@ +/** + * Storybook stories for the Implement Queue Using Stacks algorithm pipeline. + * Uses the real step generator with [1, 2, 3, 4, 5], rendering the + * StackQueueVisualizer at key execution states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateImplementQueueUsingStacksSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateImplementQueueUsingStacksSteps({ values: [1, 2, 3, 4, 5] }); +const smallSteps = generateImplementQueueUsingStacksSteps({ values: [10, 20, 30] }); + +const meta: Meta = { + title: "Algorithm Pipelines/Implement Queue Using Stacks", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — empty stacks, input array ready */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Push phase complete — all values loaded onto the input stack */ +export const InputStackLoaded: Story = { + args: { + visualState: (() => { + const pushSteps = defaultSteps.filter((step) => step.type === "push"); + return pushSteps[pushSteps.length - 1]!.visualState as StackQueueVisualState; + })(), + }, +}; + +/** Mid-transfer — elements moving from input stack to output stack */ +export const MidTransfer: Story = { + args: { + visualState: (() => { + const transferSteps = defaultSteps.filter((step) => step.type === "transfer"); + const midIdx = Math.floor(transferSteps.length / 2); + return transferSteps[midIdx]!.visualState as StackQueueVisualState; + })(), + }, +}; + +/** Dequeue in progress — output stack being consumed in FIFO order */ +export const DequeueInProgress: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length * 0.75)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Complete — all values dequeued in FIFO order */ +export const AllDequeued: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Small input — three values showing the full transfer cycle */ +export const SmallInputComplete: Story = { + args: { + visualState: smallSteps[smallSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/educational.ts b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/educational.ts new file mode 100644 index 00000000..7798cd09 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/educational.ts @@ -0,0 +1,66 @@ +import type { EducationalContent } from "@/types"; + +export const implementQueueUsingStacksEducational: EducationalContent = { + overview: + "**Implement Queue Using Stacks** (LeetCode 232) constructs a FIFO queue using only two stacks. A stack is LIFO — last in, first out — so two reversals cancel each other out and restore FIFO order.\n\n" + + "Values are pushed onto an **input stack**. When a dequeue is requested and the **output stack** is empty, all elements are transferred from the input stack to the output stack, reversing their order. The output stack is then popped normally, yielding elements in original insertion order.", + + howItWorks: + "The algorithm operates in two phases:\n\n" + + "**Push Phase** — every new value is pushed directly onto the input stack. This is always `O(1)`.\n\n" + + "**Dequeue Phase:**\n" + + "1. If the output stack is **non-empty**, pop from it — already in FIFO order.\n" + + "2. If the output stack is **empty**, transfer every element from the input stack:\n" + + " - Pop each element from the input stack and push it onto the output stack.\n" + + " - This reverses the insertion order so the earliest-enqueued element sits at the output stack's top.\n" + + "3. Pop from the output stack to complete the dequeue.\n\n" + + "### Example trace on `[1, 2, 3]`\n\n" + + "```\n" + + "push 1 → inputStack: [1]\n" + + "push 2 → inputStack: [1, 2]\n" + + "push 3 → inputStack: [1, 2, 3] (top = 3)\n\n" + + "dequeue — outputStack empty, transfer:\n" + + " pop 3 → outputStack: [3]\n" + + " pop 2 → outputStack: [3, 2]\n" + + " pop 1 → outputStack: [3, 2, 1] (top = 1)\n" + + " pop outputStack → dequeue returns 1 ✓ (first enqueued)\n\n" + + "dequeue → pop outputStack → returns 2 ✓\n" + + "dequeue → pop outputStack → returns 3 ✓\n" + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(1)` amortized per operation**\n\n" + + "Each element is pushed once and popped at most twice (once during transfer, once during dequeue). Averaged over all operations, the cost per element is constant.\n\n" + + "Individual dequeue calls may be `O(n)` when a transfer is triggered, but that cost is amortized across subsequent `O(1)` dequeues until the output stack is exhausted again.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "At any point, all `n` elements reside in one of the two stacks, requiring `O(n)` extra space in total.", + + bestAndWorstCase: + "**Best case** — output stack already has elements; dequeue is a single `O(1)` pop with no transfer needed.\n\n" + + "**Worst case** — output stack is empty and all `n` elements sit in the input stack; one dequeue triggers an `O(n)` transfer. However, this can only happen after `n` pushes, so the amortized cost remains `O(1)` per operation.\n\n" + + "**Push** is always `O(1)` — no transfer ever occurs on the push path.", + + realWorldUses: [ + "**Constraint-based systems:** In environments where only a stack primitive is available (e.g., certain embedded systems or language runtimes), this pattern provides full queue semantics.", + "**Algorithm design interviews:** Demonstrates understanding of amortized analysis and the insight that two LIFO reversals produce FIFO order.", + "**Recursive call stacks as queues:** BFS can be simulated with two stacks using this pattern when an explicit queue is unavailable.", + "**Undo/redo with ordered replay:** Two stacks can model a queue of operations that must be replayed in insertion order.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(1) amortized time per operation — competitive with a real queue implementation.", + "Uses only two stacks, making it useful when queue primitives are unavailable.", + "Simple invariant: push always goes to inputStack, pop always comes from outputStack.", + ], + limitations: [ + "Individual dequeue calls can spike to O(n) when a transfer is triggered — not suitable for hard real-time systems.", + "O(n) extra space overhead compared to a single-array queue.", + "More complex than a direct queue; the two-stack indirection increases cognitive load.", + ], + }, + + whenToUseIt: + "Use this pattern when you need queue semantics but only have stack primitives — a common interview constraint. For production code, prefer a language-native deque or linked-list queue.\n\n" + + "Avoid this approach in latency-sensitive systems where worst-case O(n) per dequeue is unacceptable. In those cases, a circular buffer queue offers O(1) worst-case dequeue.", +}; diff --git a/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/implement-queue-using-stacks.test.ts b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/implement-queue-using-stacks.test.ts new file mode 100644 index 00000000..ec1328f6 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/implement-queue-using-stacks.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from "vitest"; +import { implementQueueUsingStacks } from "./sources/implement-queue-using-stacks.ts?fn"; + +describe("implementQueueUsingStacks", () => { + it("dequeues a sequence of values in FIFO order", () => { + expect(implementQueueUsingStacks([1, 2, 3, 4, 5])).toEqual([1, 2, 3, 4, 5]); + }); + + it("preserves FIFO order for a two-element input", () => { + expect(implementQueueUsingStacks([10, 20])).toEqual([10, 20]); + }); + + it("returns a single-element array unchanged", () => { + expect(implementQueueUsingStacks([42])).toEqual([42]); + }); + + it("returns an empty array when given no values", () => { + expect(implementQueueUsingStacks([])).toEqual([]); + }); + + it("preserves FIFO order for duplicate values", () => { + expect(implementQueueUsingStacks([7, 7, 7])).toEqual([7, 7, 7]); + }); + + it("preserves FIFO order for values in descending order", () => { + expect(implementQueueUsingStacks([5, 4, 3, 2, 1])).toEqual([5, 4, 3, 2, 1]); + }); + + it("preserves FIFO order for values in ascending order", () => { + expect(implementQueueUsingStacks([1, 2, 3])).toEqual([1, 2, 3]); + }); + + it("preserves FIFO order with negative values", () => { + expect(implementQueueUsingStacks([-3, -1, 0, 2])).toEqual([-3, -1, 0, 2]); + }); + + it("preserves FIFO order for a larger input", () => { + const inputValues = [10, 20, 30, 40, 50, 60, 70, 80]; + expect(implementQueueUsingStacks(inputValues)).toEqual(inputValues); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/index.ts b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/index.ts new file mode 100644 index 00000000..2a50f875 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { implementQueueUsingStacks } from "./sources/implement-queue-using-stacks.ts?fn"; +import { generateImplementQueueUsingStacksSteps } from "./step-generator"; +import type { ImplementQueueUsingStacksInput } from "./step-generator"; +import { implementQueueUsingStacksEducational } from "./educational"; + +import typescriptSource from "./sources/implement-queue-using-stacks.ts?raw"; +import pythonSource from "./sources/implement-queue-using-stacks.py?raw"; +import javaSource from "./sources/ImplementQueueUsingStacks.java?raw"; + +function executeImplementQueueUsingStacks(input: ImplementQueueUsingStacksInput): number[] { + return implementQueueUsingStacks(input.values) as number[]; +} + +const implementQueueUsingStacksDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.IMPLEMENT_QUEUE_USING_STACKS!, + name: "Implement Queue Using Stacks", + category: CATEGORY.STACKS_QUEUES!, + technique: "queue-operations", + description: + "Simulate a FIFO queue with two stacks: push to the input stack, transfer all to the output stack on dequeue when empty, yielding amortized O(1) per operation", + timeComplexity: { + best: "O(1)", + average: "O(1)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { values: [1, 2, 3, 4, 5] }, + }, + execute: executeImplementQueueUsingStacks, + generateSteps: generateImplementQueueUsingStacksSteps, + educational: implementQueueUsingStacksEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(implementQueueUsingStacksDefinition); diff --git a/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/sources/ImplementQueueUsingStacks.java b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/sources/ImplementQueueUsingStacks.java new file mode 100644 index 00000000..9f1f1106 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/sources/ImplementQueueUsingStacks.java @@ -0,0 +1,34 @@ +// Implement Queue Using Stacks — use two stacks to emulate FIFO queue behaviour (LeetCode 232) +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +public class ImplementQueueUsingStacks { + public static List implementQueueUsingStacks(int[] values) { + Deque inputStack = new ArrayDeque<>(); // @step:initialize + Deque outputStack = new ArrayDeque<>(); // @step:initialize + List dequeueResults = new ArrayList<>(); // @step:initialize + + // Push phase — enqueue all values into the input stack + for (int elementIdx = 0; elementIdx < values.length; elementIdx++) { + int currentValue = values[elementIdx]; // @step:visit + inputStack.push(currentValue); // @step:push + } + + // Dequeue phase — transfer when output stack is empty, then pop + while (!inputStack.isEmpty() || !outputStack.isEmpty()) { + if (outputStack.isEmpty()) { + // Transfer all elements from input stack to output stack + while (!inputStack.isEmpty()) { + int transferredValue = inputStack.pop(); // @step:transfer + outputStack.push(transferredValue); // @step:transfer + } + } + int dequeuedValue = outputStack.pop(); // @step:pop + dequeueResults.add(dequeuedValue); // @step:pop + } + + return dequeueResults; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/sources/implement-queue-using-stacks.py b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/sources/implement-queue-using-stacks.py new file mode 100644 index 00000000..9a2e2f97 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/sources/implement-queue-using-stacks.py @@ -0,0 +1,22 @@ +# Implement Queue Using Stacks — use two stacks to emulate FIFO queue behaviour (LeetCode 232) +def implement_queue_using_stacks(values): + input_stack = [] # @step:initialize + output_stack = [] # @step:initialize + dequeue_results = [] # @step:initialize + + # Push phase — enqueue all values into the input stack + for element_idx in range(len(values)): + current_value = values[element_idx] # @step:visit + input_stack.append(current_value) # @step:push + + # Dequeue phase — transfer when output stack is empty, then pop + while input_stack or output_stack: + if not output_stack: + # Transfer all elements from input stack to output stack + while input_stack: + transferred_value = input_stack.pop() # @step:transfer + output_stack.append(transferred_value) # @step:transfer + dequeued_value = output_stack.pop() # @step:pop + dequeue_results.append(dequeued_value) # @step:pop + + return dequeue_results # @step:complete diff --git a/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/sources/implement-queue-using-stacks.ts b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/sources/implement-queue-using-stacks.ts new file mode 100644 index 00000000..c8799403 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/sources/implement-queue-using-stacks.ts @@ -0,0 +1,27 @@ +// Implement Queue Using Stacks — use two stacks to emulate FIFO queue behaviour (LeetCode 232) +function implementQueueUsingStacks(values: number[]): number[] { + const inputStack: number[] = []; // @step:initialize + const outputStack: number[] = []; // @step:initialize + const dequeueResults: number[] = []; // @step:initialize + + // Push phase — enqueue all values into the input stack + for (let elementIdx = 0; elementIdx < values.length; elementIdx++) { + const currentValue = values[elementIdx]!; // @step:visit + inputStack.push(currentValue); // @step:push + } + + // Dequeue phase — transfer when output stack is empty, then pop + while (inputStack.length > 0 || outputStack.length > 0) { + if (outputStack.length === 0) { + // Transfer all elements from input stack to output stack + while (inputStack.length > 0) { + const transferredValue = inputStack.pop()!; // @step:transfer + outputStack.push(transferredValue); // @step:transfer + } + } + const dequeuedValue = outputStack.pop()!; // @step:pop + dequeueResults.push(dequeuedValue); // @step:pop + } + + return dequeueResults; // @step:complete +} diff --git a/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/step-generator.test.ts b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/step-generator.test.ts new file mode 100644 index 00000000..141ebb81 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/step-generator.test.ts @@ -0,0 +1,91 @@ +import { describe, it, expect } from "vitest"; +import { generateImplementQueueUsingStacksSteps } from "./step-generator"; +import type { StackQueueVisualState } from "@/types"; + +const DEFAULT_INPUT = { values: [1, 2, 3, 4, 5] }; + +describe("generateImplementQueueUsingStacksSteps", () => { + it("produces steps for the default input", () => { + const steps = generateImplementQueueUsingStacksSteps(DEFAULT_INPUT); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateImplementQueueUsingStacksSteps(DEFAULT_INPUT); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateImplementQueueUsingStacksSteps(DEFAULT_INPUT); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateImplementQueueUsingStacksSteps(DEFAULT_INPUT); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateImplementQueueUsingStacksSteps(DEFAULT_INPUT); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits one push step per input value", () => { + const steps = generateImplementQueueUsingStacksSteps(DEFAULT_INPUT); + const pushSteps = steps.filter((step) => step.type === "push"); + expect(pushSteps.length).toBe(DEFAULT_INPUT.values.length); + }); + + it("emits transfer steps equal to the number of values", () => { + const steps = generateImplementQueueUsingStacksSteps(DEFAULT_INPUT); + const transferSteps = steps.filter((step) => step.type === "transfer"); + expect(transferSteps.length).toBe(DEFAULT_INPUT.values.length); + }); + + it("emits one dequeue step per input value", () => { + const steps = generateImplementQueueUsingStacksSteps(DEFAULT_INPUT); + const dequeueSteps = steps.filter( + (step) => step.type === "dequeue" || step.type === "dequeue-rear", + ); + expect(dequeueSteps.length).toBe(DEFAULT_INPUT.values.length); + }); + + it("includes visit steps during the push phase", () => { + const steps = generateImplementQueueUsingStacksSteps(DEFAULT_INPUT); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(DEFAULT_INPUT.values.length); + }); + + it("handles a single-value input without errors", () => { + const steps = generateImplementQueueUsingStacksSteps({ values: [99] }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("records dequeue results in FIFO order in the complete step variables", () => { + const steps = generateImplementQueueUsingStacksSteps({ values: [10, 20, 30] }); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.variables["dequeueResults"]).toEqual([10, 20, 30]); + }); + + it("shows the input stack populated after the push phase", () => { + const steps = generateImplementQueueUsingStacksSteps({ values: [1, 2, 3] }); + // Last push step should show all values on the stack + const pushSteps = steps.filter((step) => step.type === "push"); + const lastPushStep = pushSteps[pushSteps.length - 1]!; + const visualState = lastPushStep.visualState as StackQueueVisualState; + expect(visualState.stackElements.length).toBe(3); + }); + + it("shows input array in visual state", () => { + const steps = generateImplementQueueUsingStacksSteps(DEFAULT_INPUT); + const initialStep = steps[0]!; + const visualState = initialStep.visualState as StackQueueVisualState; + expect(visualState.inputArray).toBeDefined(); + expect(visualState.inputArray?.length).toBe(DEFAULT_INPUT.values.length); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/step-generator.ts b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/step-generator.ts new file mode 100644 index 00000000..524b8b49 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-queue-using-stacks/step-generator.ts @@ -0,0 +1,94 @@ +/** Step generator for Implement Queue Using Stacks — produces ExecutionStep[] using QueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { QueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const IQUS_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.IMPLEMENT_QUEUE_USING_STACKS!); + +export interface ImplementQueueUsingStacksInput { + values: number[]; +} + +export function generateImplementQueueUsingStacksSteps( + input: ImplementQueueUsingStacksInput, +): ExecutionStep[] { + const { values } = input; + + // stackElements = inputStack, queueElements = outputStack + // transfer("stack","queue") pops stackElements top → pushes queueElements rear + // After full transfer of inputStack [1,2,3] (top=3): queueElements = [3,2,1] + // dequeueRear removes queueElements rear = element 1, then 2, then 3 → FIFO order + const tracker = new QueueTracker(values, IQUS_LINE_MAP); + + tracker.initialize({ + inputStack: [], + outputStack: [], + dequeueResults: [], + }); + + // Push phase — enqueue all values onto the input stack + tracker.setPhase("Push Phase"); + for (let elementIdx = 0; elementIdx < values.length; elementIdx++) { + const currentValue = values[elementIdx]!; + + tracker.processElement(elementIdx, { + elementIdx, + currentValue, + phase: "push", + }); + + tracker.pushToStack(String(currentValue), { + elementIdx, + currentValue, + inputStackSize: elementIdx + 1, + }); + } + + // Dequeue phase — transfer inputStack → outputStack, then dequeue rear (preserves FIFO) + tracker.setPhase("Dequeue Phase"); + + // Mirror the logical stack states to generate accurate variable snapshots + const inputStackMirror = [...values]; // bottom-to-top: last element is top + const outputStackMirror: number[] = []; // bottom-to-top: last element is top + const dequeueResults: number[] = []; + let transferCount = 0; + + while (inputStackMirror.length > 0 || outputStackMirror.length > 0) { + if (outputStackMirror.length === 0) { + // Transfer all elements: inputStack top → outputStack top reverses insertion order + while (inputStackMirror.length > 0) { + const transferredValue = inputStackMirror.pop()!; + outputStackMirror.push(transferredValue); + transferCount++; + tracker.transfer("stack", "queue", { + transferredValue, + transferCount, + inputStackSize: inputStackMirror.length, + outputStackSize: outputStackMirror.length, + }); + } + } + + // outputStackMirror bottom = first-enqueued element = FIFO front + // dequeueRear removes queueElements rear = outputStack bottom = correct FIFO element + const dequeuedValue = outputStackMirror.pop()!; + tracker.dequeueRear( + { + dequeuedValue, + dequeueCount: dequeueResults.length + 1, + outputStackSize: outputStackMirror.length, + }, + `Dequeue ${String(dequeuedValue)} — first in, first out`, + ); + dequeueResults.push(dequeuedValue); + } + + tracker.complete( + { dequeueResults }, + `Queue emptied — all ${values.length} values dequeued in FIFO order`, + ); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/ImplementStackUsingQueuesPipeline.stories.tsx b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/ImplementStackUsingQueuesPipeline.stories.tsx new file mode 100644 index 00000000..83ccef0f --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/ImplementStackUsingQueuesPipeline.stories.tsx @@ -0,0 +1,87 @@ +/** + * Storybook stories for the Implement Stack Using Queues algorithm pipeline. + * Uses the real step generator with [1, 2, 3, 4, 5], rendering the + * StackQueueVisualizer at key execution states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateImplementStackUsingQueuesSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateImplementStackUsingQueuesSteps({ values: [1, 2, 3, 4, 5] }); +const smallSteps = generateImplementStackUsingQueuesSteps({ values: [10, 20, 30] }); + +const meta: Meta = { + title: "Algorithm Pipelines/Implement Stack Using Queues", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — empty queue, input array ready */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** First enqueue complete — value 1 at queue front, no rotations needed */ +export const FirstEnqueue: Story = { + args: { + visualState: (() => { + const enqueueSteps = defaultSteps.filter((step) => step.type === "enqueue"); + return enqueueSteps[0]!.visualState as StackQueueVisualState; + })(), + }, +}; + +/** Mid-rotation — elements cycling behind the newly pushed value */ +export const MidRotation: Story = { + args: { + visualState: (() => { + const transferSteps = defaultSteps.filter((step) => step.type === "transfer"); + const midIdx = Math.floor(transferSteps.length / 2); + return transferSteps[midIdx]!.visualState as StackQueueVisualState; + })(), + }, +}; + +/** All pushes complete — queue front holds the most-recently pushed value */ +export const AllPushesComplete: Story = { + args: { + visualState: (() => { + const enqueueSteps = defaultSteps.filter((step) => step.type === "enqueue"); + return enqueueSteps[enqueueSteps.length - 1]!.visualState as StackQueueVisualState; + })(), + }, +}; + +/** Pop in progress — dequeuing values in LIFO order */ +export const PopInProgress: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length * 0.8)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Complete — all values popped in LIFO order */ +export const AllPopped: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Small input — three values showing the full rotation and pop cycle */ +export const SmallInputComplete: Story = { + args: { + visualState: smallSteps[smallSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/educational.ts b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/educational.ts new file mode 100644 index 00000000..acec3247 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/educational.ts @@ -0,0 +1,56 @@ +import type { EducationalContent } from "@/types"; + +export const implementStackUsingQueuesEducational: EducationalContent = { + overview: + "**Implement Stack Using Queues** (LeetCode 225) shows how to build a LIFO stack with only queue primitives — enqueue and dequeue. After each push, the new element is rotated to the front so that the front of the queue always holds the most-recently pushed value, giving O(1) pop at the cost of O(n) push.\n\nThis problem is the conceptual mirror of LeetCode 232 (Implement Queue Using Stacks) and together they illustrate how the two fundamental linear structures can emulate each other.", + + howItWorks: + "The algorithm uses a **single queue** and re-orders it on every push:\n\n" + + "1. **Push(x):** Enqueue `x` at the rear. Then dequeue and re-enqueue every element that was already in the queue (i.e. `queue.length - 1` rotations). This cycles the old elements behind the new one, leaving `x` at the front.\n" + + "2. **Pop / top:** The front of the queue is always the most-recently pushed element, so a plain dequeue gives LIFO order.\n\n" + + "### Example trace on `[1, 2, 3]`\n\n" + + "```\n" + + "push(1): enqueue 1 → queue: [1] (0 rotations)\n" + + "push(2): enqueue 2 → queue: [1,2]\n" + + " rotate 1 → queue: [2,1]\n" + + "push(3): enqueue 3 → queue: [2,1,3]\n" + + " rotate 2,1 → queue: [3,2,1]\n" + + "pop: dequeue 3 ✓\n" + + "pop: dequeue 2 ✓\n" + + "pop: dequeue 1 ✓\n" + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: O(n) push / O(1) pop**\n\n" + + "Each push enqueues one element and then rotates all previously enqueued elements — `k` rotations for the k-th push. Summed over `n` pushes this is `0 + 1 + 2 + … + (n-1) = O(n²)` total rotations, but each individual push is O(n) in the worst case. Pop is O(1) because no rotation is needed.\n\n" + + "**Space Complexity: O(n)**\n\n" + + "Only one queue is used, holding at most `n` elements.", + + bestAndWorstCase: + "**Best case push** — pushing onto an empty stack requires zero rotations: O(1).\n\n" + + "**Worst case push** — the n-th push triggers n-1 rotations: O(n).\n\n" + + "**Pop** is always O(1) because the front is maintained as the stack top.", + + realWorldUses: [ + "**Interview preparation:** A classic problem that tests understanding of both stack and queue semantics and the cost of emulating one with the other.", + "**Constrained environments:** Situations where only queue-based APIs are exposed (e.g., message brokers, job queues) but LIFO access order is required.", + "**Concurrent systems:** Task-reversal patterns where the last-submitted task must run first, implemented on top of a FIFO channel.", + "**Algorithm design:** Demonstrates that any computable behaviour can be built from simpler primitives, at varying efficiency trade-offs.", + ], + + strengthsAndLimitations: { + strengths: [ + "Single queue — simpler state than the two-stack approach used for the inverse problem.", + "O(1) pop and top — reads are instant regardless of queue size.", + "Conceptually clear: rotation keeps the invariant that the queue front equals the stack top.", + ], + limitations: [ + "O(n) push — each insertion triggers up to n-1 extra dequeue/enqueue operations.", + "Not practical for write-heavy workloads; the two-stack approach offers amortized O(1) push.", + "Total work across n pushes is O(n²), making it unsuitable for large data sets.", + ], + }, + + whenToUseIt: + "Use this approach when pops are far more frequent than pushes, or when only a single queue primitive is available. If both pushes and pops need to be fast, use two stacks (LeetCode 232 in reverse) which gives amortized O(1) for both operations.", +}; diff --git a/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/implement-stack-using-queues.test.ts b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/implement-stack-using-queues.test.ts new file mode 100644 index 00000000..1b41145d --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/implement-stack-using-queues.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from "vitest"; +import { implementStackUsingQueues } from "./sources/implement-stack-using-queues.ts?fn"; + +describe("implementStackUsingQueues", () => { + it("pops a sequence of values in LIFO order", () => { + expect(implementStackUsingQueues([1, 2, 3, 4, 5])).toEqual([5, 4, 3, 2, 1]); + }); + + it("returns a two-element array in reverse insertion order", () => { + expect(implementStackUsingQueues([10, 20])).toEqual([20, 10]); + }); + + it("returns a single-element array unchanged", () => { + expect(implementStackUsingQueues([42])).toEqual([42]); + }); + + it("returns an empty array when given no values", () => { + expect(implementStackUsingQueues([])).toEqual([]); + }); + + it("returns duplicate values in LIFO order", () => { + expect(implementStackUsingQueues([7, 7, 7])).toEqual([7, 7, 7]); + }); + + it("returns values originally in descending order in ascending LIFO order", () => { + expect(implementStackUsingQueues([5, 4, 3, 2, 1])).toEqual([1, 2, 3, 4, 5]); + }); + + it("returns values originally in ascending order in descending LIFO order", () => { + expect(implementStackUsingQueues([1, 2, 3])).toEqual([3, 2, 1]); + }); + + it("handles negative values in LIFO order", () => { + expect(implementStackUsingQueues([-3, -1, 0, 2])).toEqual([2, 0, -1, -3]); + }); + + it("returns a larger input in LIFO order", () => { + const inputValues = [10, 20, 30, 40, 50]; + expect(implementStackUsingQueues(inputValues)).toEqual([50, 40, 30, 20, 10]); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/index.ts b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/index.ts new file mode 100644 index 00000000..f980b81f --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { implementStackUsingQueues } from "./sources/implement-stack-using-queues.ts?fn"; +import { generateImplementStackUsingQueuesSteps } from "./step-generator"; +import type { ImplementStackUsingQueuesInput } from "./step-generator"; +import { implementStackUsingQueuesEducational } from "./educational"; + +import typescriptSource from "./sources/implement-stack-using-queues.ts?raw"; +import pythonSource from "./sources/implement-stack-using-queues.py?raw"; +import javaSource from "./sources/ImplementStackUsingQueues.java?raw"; + +function executeImplementStackUsingQueues(input: ImplementStackUsingQueuesInput): number[] { + return implementStackUsingQueues(input.values) as number[]; +} + +const implementStackUsingQueuesDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.IMPLEMENT_STACK_USING_QUEUES!, + name: "Implement Stack Using Queues", + category: CATEGORY.STACKS_QUEUES!, + technique: "queue-operations", + description: + "Simulate a LIFO stack with a single queue: after each enqueue, rotate all prior elements behind the new one so the queue front always holds the stack top, yielding O(1) pop and O(n) push", + timeComplexity: { + best: "O(1)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { values: [1, 2, 3, 4, 5] }, + }, + execute: executeImplementStackUsingQueues, + generateSteps: generateImplementStackUsingQueuesSteps, + educational: implementStackUsingQueuesEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(implementStackUsingQueuesDefinition); diff --git a/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/sources/ImplementStackUsingQueues.java b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/sources/ImplementStackUsingQueues.java new file mode 100644 index 00000000..5f2b4059 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/sources/ImplementStackUsingQueues.java @@ -0,0 +1,31 @@ +// Implement Stack Using Queues — use one queue to emulate LIFO stack behaviour (LeetCode 225) +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +public class ImplementStackUsingQueues { + public static List implementStackUsingQueues(int[] values) { + Deque queue = new ArrayDeque<>(); // @step:initialize + List popResults = new ArrayList<>(); // @step:initialize + + // Push phase — enqueue each value, then rotate all prior elements behind it + for (int elementIdx = 0; elementIdx < values.length; elementIdx++) { + int currentValue = values[elementIdx]; // @step:visit + queue.addLast(currentValue); // @step:enqueue + // Rotate: move every element that was there before the new one to the back + for (int rotationIdx = 0; rotationIdx < queue.size() - 1; rotationIdx++) { + int transferred = queue.removeFirst(); // @step:transfer + queue.addLast(transferred); // @step:transfer + } + } + + // Pop phase — front of queue is always the most-recently pushed element (LIFO) + while (!queue.isEmpty()) { + int poppedValue = queue.removeFirst(); // @step:dequeue + popResults.add(poppedValue); // @step:dequeue + } + + return popResults; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/sources/implement-stack-using-queues.py b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/sources/implement-stack-using-queues.py new file mode 100644 index 00000000..6d8918ea --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/sources/implement-stack-using-queues.py @@ -0,0 +1,22 @@ +# Implement Stack Using Queues — use one queue to emulate LIFO stack behaviour (LeetCode 225) +from collections import deque + +def implement_stack_using_queues(values): + queue = deque() # @step:initialize + pop_results = [] # @step:initialize + + # Push phase — enqueue each value, then rotate all prior elements behind it + for element_idx in range(len(values)): + current_value = values[element_idx] # @step:visit + queue.append(current_value) # @step:enqueue + # Rotate: move every element that was there before the new one to the back + for rotation_idx in range(len(queue) - 1): + transferred = queue.popleft() # @step:transfer + queue.append(transferred) # @step:transfer + + # Pop phase — front of queue is always the most-recently pushed element (LIFO) + while queue: + popped_value = queue.popleft() # @step:dequeue + pop_results.append(popped_value) # @step:dequeue + + return pop_results # @step:complete diff --git a/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/sources/implement-stack-using-queues.ts b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/sources/implement-stack-using-queues.ts new file mode 100644 index 00000000..9aabab02 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/sources/implement-stack-using-queues.ts @@ -0,0 +1,24 @@ +// Implement Stack Using Queues — use one queue to emulate LIFO stack behaviour (LeetCode 225) +function implementStackUsingQueues(values: number[]): number[] { + const queue: number[] = []; // @step:initialize + const popResults: number[] = []; // @step:initialize + + // Push phase — enqueue each value, then rotate all prior elements behind it + for (let elementIdx = 0; elementIdx < values.length; elementIdx++) { + const currentValue = values[elementIdx]!; // @step:visit + queue.push(currentValue); // @step:enqueue + // Rotate: move every element that was there before the new one to the back + for (let rotationIdx = 0; rotationIdx < queue.length - 1; rotationIdx++) { + const transferred = queue.shift()!; // @step:transfer + queue.push(transferred); // @step:transfer + } + } + + // Pop phase — front of queue is always the most-recently pushed element (LIFO) + while (queue.length > 0) { + const poppedValue = queue.shift()!; // @step:dequeue + popResults.push(poppedValue); // @step:dequeue + } + + return popResults; // @step:complete +} diff --git a/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/step-generator.test.ts b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/step-generator.test.ts new file mode 100644 index 00000000..05d1091a --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/step-generator.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from "vitest"; +import { generateImplementStackUsingQueuesSteps } from "./step-generator"; +import type { StackQueueVisualState } from "@/types"; + +const DEFAULT_INPUT = { values: [1, 2, 3, 4, 5] }; + +describe("generateImplementStackUsingQueuesSteps", () => { + it("produces steps for the default input", () => { + const steps = generateImplementStackUsingQueuesSteps(DEFAULT_INPUT); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateImplementStackUsingQueuesSteps(DEFAULT_INPUT); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateImplementStackUsingQueuesSteps(DEFAULT_INPUT); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateImplementStackUsingQueuesSteps(DEFAULT_INPUT); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateImplementStackUsingQueuesSteps(DEFAULT_INPUT); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits one enqueue step per input value", () => { + const steps = generateImplementStackUsingQueuesSteps(DEFAULT_INPUT); + const enqueueSteps = steps.filter((step) => step.type === "enqueue"); + expect(enqueueSteps.length).toBe(DEFAULT_INPUT.values.length); + }); + + it("emits transfer steps equal to the total rotation count (0+1+2+...+(n-1))", () => { + const steps = generateImplementStackUsingQueuesSteps(DEFAULT_INPUT); + const transferSteps = steps.filter((step) => step.type === "transfer"); + const expectedRotations = (DEFAULT_INPUT.values.length * (DEFAULT_INPUT.values.length - 1)) / 2; + expect(transferSteps.length).toBe(expectedRotations); + }); + + it("emits one dequeue step per input value during the pop phase", () => { + const steps = generateImplementStackUsingQueuesSteps(DEFAULT_INPUT); + const dequeueSteps = steps.filter((step) => step.type === "dequeue"); + expect(dequeueSteps.length).toBe(DEFAULT_INPUT.values.length); + }); + + it("emits visit steps equal to the number of input values", () => { + const steps = generateImplementStackUsingQueuesSteps(DEFAULT_INPUT); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(DEFAULT_INPUT.values.length); + }); + + it("handles a single-value input without errors", () => { + const steps = generateImplementStackUsingQueuesSteps({ values: [99] }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("records pop results in LIFO order in the complete step variables", () => { + const steps = generateImplementStackUsingQueuesSteps({ values: [10, 20, 30] }); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.variables["popResults"]).toEqual([30, 20, 10]); + }); + + it("shows the queue populated with latest value at front after first push", () => { + const steps = generateImplementStackUsingQueuesSteps({ values: [1, 2] }); + const enqueueSteps = steps.filter((step) => step.type === "enqueue"); + const secondEnqueue = enqueueSteps[1]!; + const visualState = secondEnqueue.visualState as StackQueueVisualState; + // After enqueue(2) but before rotation, queue has [1, 2] — front is 1 momentarily + expect(visualState.queueElements?.length).toBe(2); + }); + + it("shows input array in visual state", () => { + const steps = generateImplementStackUsingQueuesSteps(DEFAULT_INPUT); + const initialStep = steps[0]!; + const visualState = initialStep.visualState as StackQueueVisualState; + expect(visualState.inputArray).toBeDefined(); + expect(visualState.inputArray?.length).toBe(DEFAULT_INPUT.values.length); + }); + + it("handles an empty input without errors", () => { + const steps = generateImplementStackUsingQueuesSteps({ values: [] }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/step-generator.ts b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/step-generator.ts new file mode 100644 index 00000000..a2e592a7 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/implement-stack-using-queues/step-generator.ts @@ -0,0 +1,92 @@ +/** Step generator for Implement Stack Using Queues — produces ExecutionStep[] using QueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { QueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const ISUQ_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.IMPLEMENT_STACK_USING_QUEUES!); + +export interface ImplementStackUsingQueuesInput { + values: number[]; +} + +export function generateImplementStackUsingQueuesSteps( + input: ImplementStackUsingQueuesInput, +): ExecutionStep[] { + const { values } = input; + + // queueElements mirrors the single queue. + // After each push+rotation the new value sits at the front — LIFO top. + const tracker = new QueueTracker(values, ISUQ_LINE_MAP); + + tracker.initialize({ + queue: [], + popResults: [], + }); + + // Push phase — enqueue each value then rotate prior elements behind it + tracker.setPhase("Push Phase"); + + const queueMirror: number[] = []; + + for (let elementIdx = 0; elementIdx < values.length; elementIdx++) { + const currentValue = values[elementIdx]!; + + tracker.processElement(elementIdx, { + elementIdx, + currentValue, + phase: "push", + }); + + // Enqueue the new value at the rear + queueMirror.push(currentValue); + tracker.enqueue(String(currentValue), { + elementIdx, + currentValue, + queueSize: queueMirror.length, + }); + + // Rotate: move queue.length-1 elements from front to rear + const rotationCount = queueMirror.length - 1; + for (let rotationIdx = 0; rotationIdx < rotationCount; rotationIdx++) { + const transferred = queueMirror.shift()!; + queueMirror.push(transferred); + tracker.transfer( + "queue", + "queue", + { + rotationIdx, + transferred, + queueSize: queueMirror.length, + }, + `Rotate ${String(transferred)} to the rear — new element stays at front`, + ); + } + } + + // Pop phase — front of queue is the LIFO top + tracker.setPhase("Pop Phase"); + + const popResults: number[] = []; + + while (queueMirror.length > 0) { + const poppedValue = queueMirror.shift()!; + popResults.push(poppedValue); + tracker.dequeue( + { + poppedValue, + popCount: popResults.length, + queueSize: queueMirror.length, + }, + `Pop ${String(poppedValue)} — last in, first out`, + ); + } + + tracker.complete( + { popResults }, + `Stack emptied — all ${values.length} values popped in LIFO order`, + ); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/NumberOfRecentCallsPipeline.stories.tsx b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/NumberOfRecentCallsPipeline.stories.tsx new file mode 100644 index 00000000..2c48efc7 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/NumberOfRecentCallsPipeline.stories.tsx @@ -0,0 +1,77 @@ +/** + * Storybook stories for the Number of Recent Calls algorithm pipeline. + * Uses the real step generator with [1, 100, 3001, 3002], rendering the + * StackQueueVisualizer at key execution states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateNumberOfRecentCallsSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateNumberOfRecentCallsSteps({ timestamps: [1, 100, 3001, 3002] }); +const burstSteps = generateNumberOfRecentCallsSteps({ timestamps: [100, 200, 300, 400, 500] }); + +const meta: Meta = { + title: "Algorithm Pipelines/Number of Recent Calls", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — empty queue, timestamps ready */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** After first enqueue — timestamp 1 enters the window */ +export const FirstEnqueue: Story = { + args: { + visualState: (() => { + const enqueueSteps = defaultSteps.filter((step) => step.type === "enqueue"); + return enqueueSteps[0]!.visualState as StackQueueVisualState; + })(), + }, +}; + +/** Window growing — multiple timestamps present, none expired yet */ +export const WindowGrowing: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Expiry in action — old timestamp dequeued as window slides */ +export const ExpiryInAction: Story = { + args: { + visualState: (() => { + const dequeueSteps = defaultSteps.filter((step) => step.type === "dequeue"); + return (dequeueSteps[0]?.visualState ?? + defaultSteps[defaultSteps.length - 1]!.visualState) as StackQueueVisualState; + })(), + }, +}; + +/** Final state — all timestamps processed */ +export const AllProcessed: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Burst scenario — all calls within window, no expiry */ +export const BurstNoExpiry: Story = { + args: { + visualState: burstSteps[burstSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/educational.ts b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/educational.ts new file mode 100644 index 00000000..30773a6f --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/educational.ts @@ -0,0 +1,53 @@ +import type { EducationalContent } from "@/types"; + +export const numberOfRecentCallsEducational: EducationalContent = { + overview: + "**Number of Recent Calls** (LeetCode 933) counts how many calls have been made within the last 3000 milliseconds, including the current call. A queue acts as a sliding time window: timestamps are enqueued as they arrive, and any timestamp older than `t - 3000` is dequeued from the front.\n\nThe queue always holds exactly the timestamps that fall within the current window, so its length is the answer for each call.", + + howItWorks: + "For each incoming timestamp `t`:\n\n" + + "1. **Enqueue** `t` at the rear of the queue.\n" + + "2. **Expire old entries:** while the front of the queue is less than `t - 3000`, dequeue it.\n" + + "3. **Count:** the queue length is the number of recent calls in `[t - 3000, t]`.\n\n" + + "### Example trace on `[1, 100, 3001, 3002]`\n\n" + + "```\n" + + "t=1 queue=[1] window=[−2999, 1] count=1\n" + + "t=100 queue=[1, 100] window=[−2900, 100] count=2\n" + + "t=3001 queue=[1, 100, 3001] window=[1, 3001] count=3 (1 stays: 1 >= 1)\n" + + "t=3002 queue=[100, 3001,3002] window=[2, 3002] count=3 (1 expired: 1 < 2)\n" + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each timestamp is enqueued once and dequeued at most once, giving amortized `O(1)` per call and `O(n)` overall for `n` calls.\n\n" + + "**Space Complexity: `O(w)`**\n\n" + + "Where `w` is the maximum number of timestamps that fit within any 3000ms window. In the worst case (all calls in one window) this is `O(n)`, but in practice `w` is bounded by the input rate.", + + bestAndWorstCase: + "**Best case** — calls are spaced more than 3000ms apart: the queue never holds more than one element at a time, dequeue overhead is zero, and each call is `O(1)`.\n\n" + + "**Worst case** — all calls arrive within a single 3000ms window: no timestamps are ever expired, the queue grows to length `n`, and space usage is `O(n)`.\n\n" + + "In both cases the total work is `O(n)` because each timestamp is enqueued and dequeued at most once.", + + realWorldUses: [ + "**Rate limiting:** API gateways use a sliding-window counter built on the same queue principle to cap requests per second per client.", + "**Event stream analytics:** Counting events (clicks, purchases, errors) that occurred within a rolling time window in real-time dashboards.", + "**Network packet monitoring:** Measuring packets received per second with a sliding window to detect traffic spikes.", + "**Debounce and throttle utilities:** Frontend frameworks track recent invocations in a time window to decide whether to suppress a callback.", + ], + + strengthsAndLimitations: { + strengths: [ + "Amortized O(1) per call — each timestamp is enqueued and dequeued at most once.", + "Constant extra work per query beyond the natural window expiry.", + "Simple implementation with a standard queue; no sorting or hashing required.", + ], + limitations: [ + "Space scales with the window population, not a fixed constant.", + "Only supports a fixed 3000ms window; a variable window requires a parameter change.", + "Assumes timestamps are non-decreasing (guaranteed by the problem); unsorted input would need a different approach.", + ], + }, + + whenToUseIt: + "Use a queue-based sliding window when you need to count or aggregate events within a fixed time range and timestamps arrive in non-decreasing order. If the window size is variable or timestamps can arrive out of order, consider a sorted structure or a deque with binary search instead.", +}; diff --git a/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/index.ts b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/index.ts new file mode 100644 index 00000000..cad73eaa --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { numberOfRecentCalls } from "./sources/number-of-recent-calls.ts?fn"; +import { generateNumberOfRecentCallsSteps } from "./step-generator"; +import type { NumberOfRecentCallsInput } from "./step-generator"; +import { numberOfRecentCallsEducational } from "./educational"; + +import typescriptSource from "./sources/number-of-recent-calls.ts?raw"; +import pythonSource from "./sources/number-of-recent-calls.py?raw"; +import javaSource from "./sources/NumberOfRecentCalls.java?raw"; + +function executeNumberOfRecentCalls(input: NumberOfRecentCallsInput): number[] { + return numberOfRecentCalls(input.timestamps) as number[]; +} + +const numberOfRecentCallsDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.NUMBER_OF_RECENT_CALLS!, + name: "Number of Recent Calls", + category: CATEGORY.STACKS_QUEUES!, + technique: "queue-operations", + description: + "Count how many calls were made within the last 3000 milliseconds using a queue as a sliding time window that expires outdated timestamps from the front", + timeComplexity: { + best: "O(1)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(w)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { timestamps: [1, 100, 3001, 3002] }, + }, + execute: executeNumberOfRecentCalls, + generateSteps: generateNumberOfRecentCallsSteps, + educational: numberOfRecentCallsEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(numberOfRecentCallsDefinition); diff --git a/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/number-of-recent-calls.test.ts b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/number-of-recent-calls.test.ts new file mode 100644 index 00000000..e534d5bf --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/number-of-recent-calls.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from "vitest"; +import { numberOfRecentCalls } from "./sources/number-of-recent-calls.ts?fn"; + +describe("numberOfRecentCalls", () => { + it("produces [1,2,3,3] for the default input [1,100,3001,3002]", () => { + expect(numberOfRecentCalls([1, 100, 3001, 3002])).toEqual([1, 2, 3, 3]); + }); + + it("returns [1] for a single timestamp", () => { + expect(numberOfRecentCalls([500])).toEqual([1]); + }); + + it("counts all calls when all timestamps fit in one window", () => { + expect(numberOfRecentCalls([1, 500, 1000, 2000, 3000])).toEqual([1, 2, 3, 4, 5]); + }); + + it("expires old timestamps when window slides forward", () => { + const result = numberOfRecentCalls([1, 100, 3001, 3002, 6002]); + expect(result).toEqual([1, 2, 3, 3, 2]); + }); + + it("handles timestamps exactly at the window boundary", () => { + // t=3001: window is [1, 3001], so timestamp=1 is included (1 >= 3001-3000 = 1) + expect(numberOfRecentCalls([1, 3001])).toEqual([1, 2]); + }); + + it("expires a timestamp one millisecond past the boundary", () => { + // t=3002: window is [2, 3002], timestamp=1 is excluded (1 < 2) + expect(numberOfRecentCalls([1, 3002])).toEqual([1, 1]); + }); + + it("returns [1] for each call when all are spaced more than 3000ms apart", () => { + expect(numberOfRecentCalls([1, 3002, 6003, 9004])).toEqual([1, 1, 1, 1]); + }); + + it("handles an empty timestamps array", () => { + expect(numberOfRecentCalls([])).toEqual([]); + }); + + it("handles a large burst followed by silence", () => { + const burstTimestamps = [100, 200, 300, 400, 500]; + const result = numberOfRecentCalls(burstTimestamps); + expect(result).toEqual([1, 2, 3, 4, 5]); + }); + + it("handles sequential timestamps that all expire one by one", () => { + const result = numberOfRecentCalls([1000, 2000, 4001, 5001, 7002]); + expect(result).toEqual([1, 2, 2, 2, 2]); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/sources/NumberOfRecentCalls.java b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/sources/NumberOfRecentCalls.java new file mode 100644 index 00000000..adcb8834 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/sources/NumberOfRecentCalls.java @@ -0,0 +1,27 @@ +// Number of Recent Calls — count calls in a 3000ms sliding window using a queue (LeetCode 933) +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +public class NumberOfRecentCalls { + public static List numberOfRecentCalls(int[] timestamps) { + Deque queue = new ArrayDeque<>(); // @step:initialize + List results = new ArrayList<>(); // @step:initialize + + for (int timestampIdx = 0; timestampIdx < timestamps.length; timestampIdx++) { + int currentTimestamp = timestamps[timestampIdx]; // @step:visit + + queue.addLast(currentTimestamp); // @step:enqueue + + // Remove timestamps outside the 3000ms window + while (queue.peekFirst() < currentTimestamp - 3000) { // @step:dequeue + queue.pollFirst(); // @step:dequeue + } + + results.add(queue.size()); // @step:complete + } + + return results; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/sources/number-of-recent-calls.py b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/sources/number-of-recent-calls.py new file mode 100644 index 00000000..e5bd1719 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/sources/number-of-recent-calls.py @@ -0,0 +1,17 @@ +# Number of Recent Calls — count calls in a 3000ms sliding window using a queue (LeetCode 933) +from collections import deque + +def number_of_recent_calls(timestamps): + queue = deque() # @step:initialize + results = [] # @step:initialize + + for current_timestamp in timestamps: # @step:visit + queue.append(current_timestamp) # @step:enqueue + + # Remove timestamps outside the 3000ms window + while queue[0] < current_timestamp - 3000: # @step:dequeue + queue.popleft() # @step:dequeue + + results.append(len(queue)) # @step:complete + + return results # @step:complete diff --git a/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/sources/number-of-recent-calls.ts b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/sources/number-of-recent-calls.ts new file mode 100644 index 00000000..bb05bf30 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/sources/number-of-recent-calls.ts @@ -0,0 +1,20 @@ +// Number of Recent Calls — count calls in a 3000ms sliding window using a queue (LeetCode 933) +function numberOfRecentCalls(timestamps: number[]): number[] { + const queue: number[] = []; // @step:initialize + const results: number[] = []; // @step:initialize + + for (let timestampIdx = 0; timestampIdx < timestamps.length; timestampIdx++) { + const currentTimestamp = timestamps[timestampIdx]!; // @step:visit + + queue.push(currentTimestamp); // @step:enqueue + + // Remove timestamps outside the 3000ms window + while (queue[0]! < currentTimestamp - 3000) { + queue.shift(); // @step:dequeue + } + + results.push(queue.length); // @step:complete + } + + return results; // @step:complete +} diff --git a/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/step-generator.test.ts b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/step-generator.test.ts new file mode 100644 index 00000000..a0f7388b --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/step-generator.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect } from "vitest"; +import { generateNumberOfRecentCallsSteps } from "./step-generator"; + +const DEFAULT_INPUT = { timestamps: [1, 100, 3001, 3002] }; + +describe("generateNumberOfRecentCallsSteps", () => { + it("produces steps for the default input", () => { + const steps = generateNumberOfRecentCallsSteps(DEFAULT_INPUT); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateNumberOfRecentCallsSteps(DEFAULT_INPUT); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateNumberOfRecentCallsSteps(DEFAULT_INPUT); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateNumberOfRecentCallsSteps(DEFAULT_INPUT); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateNumberOfRecentCallsSteps(DEFAULT_INPUT); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits one visit step per timestamp", () => { + const steps = generateNumberOfRecentCallsSteps(DEFAULT_INPUT); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(DEFAULT_INPUT.timestamps.length); + }); + + it("emits one enqueue step per timestamp", () => { + const steps = generateNumberOfRecentCallsSteps(DEFAULT_INPUT); + const enqueueSteps = steps.filter((step) => step.type === "enqueue"); + expect(enqueueSteps.length).toBe(DEFAULT_INPUT.timestamps.length); + }); + + it("emits a dequeue step when a timestamp expires", () => { + // timestamp=1 expires at t=3002 (1 < 3002-3000 = 2) + const steps = generateNumberOfRecentCallsSteps(DEFAULT_INPUT); + const dequeueSteps = steps.filter((step) => step.type === "dequeue"); + expect(dequeueSteps.length).toBe(1); + }); + + it("emits one complete step per timestamp", () => { + const steps = generateNumberOfRecentCallsSteps(DEFAULT_INPUT); + const completeSteps = steps.filter((step) => step.type === "complete"); + expect(completeSteps.length).toBe(DEFAULT_INPUT.timestamps.length); + }); + + it("handles an empty timestamps array", () => { + const steps = generateNumberOfRecentCallsSteps({ timestamps: [] }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps.length).toBe(1); + }); + + it("emits no dequeue steps when all timestamps fit in the window", () => { + const steps = generateNumberOfRecentCallsSteps({ timestamps: [1, 500, 1000, 2000] }); + const dequeueSteps = steps.filter((step) => step.type === "dequeue"); + expect(dequeueSteps.length).toBe(0); + }); + + it("tracks queue operations in metrics", () => { + const steps = generateNumberOfRecentCallsSteps(DEFAULT_INPUT); + const lastStep = steps[steps.length - 1]; + expect(lastStep?.metrics.queueOperations).toBeGreaterThan(0); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/step-generator.ts b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/step-generator.ts new file mode 100644 index 00000000..102df231 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/number-of-recent-calls/step-generator.ts @@ -0,0 +1,73 @@ +/** Step generator for Number of Recent Calls — produces ExecutionStep[] using QueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { QueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const NORC_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.NUMBER_OF_RECENT_CALLS!); + +export interface NumberOfRecentCallsInput { + timestamps: number[]; +} + +export function generateNumberOfRecentCallsSteps(input: NumberOfRecentCallsInput): ExecutionStep[] { + const { timestamps } = input; + const tracker = new QueueTracker(timestamps, NORC_LINE_MAP); + + const windowQueue: number[] = []; + const results: number[] = []; + + tracker.initialize({ + queue: [], + results: [], + }); + + for (let timestampIdx = 0; timestampIdx < timestamps.length; timestampIdx++) { + const currentTimestamp = timestamps[timestampIdx]!; + + tracker.processElement(timestampIdx, { + timestampIdx, + currentTimestamp, + windowSize: windowQueue.length, + }); + + windowQueue.push(currentTimestamp); + tracker.enqueue( + String(currentTimestamp), + { + timestampIdx, + currentTimestamp, + queueSize: windowQueue.length, + }, + `Enqueue timestamp ${String(currentTimestamp)} — add to rear of window queue`, + ); + + // Expire timestamps outside the 3000ms window + while (windowQueue.length > 0 && windowQueue[0]! < currentTimestamp - 3000) { + const expiredTimestamp = windowQueue[0]!; + windowQueue.shift(); + tracker.dequeue( + { + expiredTimestamp, + currentTimestamp, + threshold: currentTimestamp - 3000, + queueSize: windowQueue.length, + }, + `Dequeue ${String(expiredTimestamp)} — expired (< ${String(currentTimestamp - 3000)})`, + ); + } + + results.push(windowQueue.length); + tracker.complete( + { + currentTimestamp, + recentCallCount: windowQueue.length, + results: [...results], + }, + `${String(windowQueue.length)} call${windowQueue.length === 1 ? "" : "s"} in window [${String(currentTimestamp - 3000)}, ${String(currentTimestamp)}]`, + ); + } + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/SlidingWindowMaximumPipeline.stories.tsx b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/SlidingWindowMaximumPipeline.stories.tsx new file mode 100644 index 00000000..ed2069b0 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/SlidingWindowMaximumPipeline.stories.tsx @@ -0,0 +1,51 @@ +/** + * Storybook stories for the Sliding Window Maximum algorithm pipeline. + * Uses the real step generator with [1, 3, -1, -3, 5, 3, 6, 7] k=3, rendering + * the StackQueueVisualizer at key deque operation states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateSlidingWindowMaximumSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateSlidingWindowMaximumSteps({ + nums: [1, 3, -1, -3, 5, 3, 6, 7], + windowSize: 3, +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Sliding Window Maximum", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — full input array unprocessed, empty deque */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing with deque partially filled */ +export const MidProcessing: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Completed — all windows processed, result array populated */ +export const AllWindowsComplete: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/educational.ts b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/educational.ts new file mode 100644 index 00000000..b337d767 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/educational.ts @@ -0,0 +1,60 @@ +import type { EducationalContent } from "@/types"; + +export const slidingWindowMaximumEducational: EducationalContent = { + overview: + "**Sliding Window Maximum** (LeetCode 239) finds the maximum element in every contiguous subarray of size `k` within a given array. A naïve scan of each window is `O(nk)`, but a **monotonic deque** of indices reduces this to `O(n)` by ensuring the front of the deque always holds the index of the current window's maximum.\n\nThe deque stores indices in **decreasing order of their values**, so expired indices are removed from the front and smaller indices are evicted from the rear before each new element is added.", + + howItWorks: + "The algorithm maintains a deque whose indices always correspond to elements in **non-increasing** order:\n\n" + + "1. **Expire old indices** — while the front index is outside the current window (`front ≤ elementIdx − k`), remove it from the front.\n" + + "2. **Maintain monotonicity** — while the rear element is ≤ the current element, pop it from the rear (it can never be a window maximum).\n" + + "3. **Enqueue** the current index at the rear.\n" + + "4. **Record maximum** — once `elementIdx ≥ k − 1`, the front of the deque is the index of the window's maximum.\n\n" + + "### Example trace on `[1, 3, -1, -3, 5, 3, 6, 7]`, k = 3\n\n" + + "```\n" + + "idx val deque (indices) window max\n" + + " 0 1 [0] —\n" + + " 1 3 [1] — (3 > 1, so index 0 evicted)\n" + + " 2 -1 [1, 2] 3 (first full window)\n" + + " 3 -3 [1, 2, 3] 3\n" + + " 4 5 [4] 5 (5 > all, deque cleared)\n" + + " 5 3 [4, 5] 5\n" + + " 6 6 [6] 6\n" + + " 7 7 [7] 7\n" + + "result: [3, 3, 5, 5, 6, 7]\n" + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each index is enqueued exactly once and dequeued at most once (either from the front when it expires or from the rear when a larger element arrives). The total number of deque operations is therefore bounded by `2n`, giving linear time.\n\n" + + "**Space Complexity: `O(k)`**\n\n" + + "The deque holds at most `k` indices at any time — one per element in the current window — so extra space is proportional to the window size.", + + bestAndWorstCase: + "**Best case `O(n)`** — a strictly decreasing array: no rear evictions occur, but every index is still enqueued and dequeued once.\n\n" + + "**Worst case `O(n)`** — a strictly increasing array: each new element evicts all previous deque entries, but across the full traversal the total evictions still equal `n`.\n\n" + + "The algorithm is always `O(n)` regardless of input order — the amortised cost of all enqueue and dequeue operations is constant per element.", + + realWorldUses: [ + "**Stock price analysis:** Computing the highest price over a rolling window of recent trading days.", + "**Signal processing:** Finding peak amplitude in a sliding audio or sensor data frame.", + "**Network monitoring:** Tracking the maximum bandwidth usage over a rolling time window for anomaly detection.", + "**Game development:** Efficiently querying the maximum value in a moving region of a grid or timeline.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(n) time — optimal for this problem; each element is touched at most twice.", + "O(k) space — only the current window's candidates are stored.", + "Straightforward to extend to sliding window minimum by reversing the comparison.", + ], + limitations: [ + "Requires a double-ended queue (deque) data structure, which is not always built into every language's standard library.", + "Returns maxima only; recovering the index of each maximum requires minor bookkeeping.", + "For very small k (k = 1), a simple linear scan is simpler and equally fast.", + ], + }, + + whenToUseIt: + "Use the monotonic deque approach whenever you need the maximum (or minimum) of every fixed-size sliding window in `O(n)` time. If the window size changes dynamically or you need range maximum queries on arbitrary intervals, consider a **sparse table** (static arrays, `O(1)` query) or a **segment tree** (dynamic updates, `O(log n)` query) instead.", +}; diff --git a/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/index.ts b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/index.ts new file mode 100644 index 00000000..731452be --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { slidingWindowMaxMonotonic } from "./sources/sliding-window-maximum.ts?fn"; +import { generateSlidingWindowMaximumSteps } from "./step-generator"; +import type { SlidingWindowMaximumInput } from "./step-generator"; +import { slidingWindowMaximumEducational } from "./educational"; + +import typescriptSource from "./sources/sliding-window-maximum.ts?raw"; +import pythonSource from "./sources/sliding-window-maximum.py?raw"; +import javaSource from "./sources/SlidingWindowMaximum.java?raw"; + +function executeSlidingWindowMaximum(input: SlidingWindowMaximumInput): number[] { + return slidingWindowMaxMonotonic(input.nums, input.windowSize) as number[]; +} + +const slidingWindowMaximumDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.SLIDING_WINDOW_MAXIMUM!, + name: "Sliding Window Maximum", + category: CATEGORY.STACKS_QUEUES!, + technique: "queue-operations", + description: + "Find the maximum in each sliding window of size k using a monotonic deque that stores indices in decreasing order of their values for O(n) performance", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(k)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { nums: [1, 3, -1, -3, 5, 3, 6, 7], windowSize: 3 }, + }, + execute: executeSlidingWindowMaximum, + generateSteps: generateSlidingWindowMaximumSteps, + educational: slidingWindowMaximumEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(slidingWindowMaximumDefinition); diff --git a/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/sliding-window-maximum.test.ts b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/sliding-window-maximum.test.ts new file mode 100644 index 00000000..f9825594 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/sliding-window-maximum.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from "vitest"; +import { slidingWindowMaxMonotonic } from "./sources/sliding-window-maximum.ts?fn"; + +describe("slidingWindowMaxMonotonic", () => { + it("returns correct maxima for the default LeetCode 239 example", () => { + expect(slidingWindowMaxMonotonic([1, 3, -1, -3, 5, 3, 6, 7], 3)).toEqual([3, 3, 5, 5, 6, 7]); + }); + + it("returns each element when window size equals array length", () => { + expect(slidingWindowMaxMonotonic([4, 2, 7], 3)).toEqual([7]); + }); + + it("returns the array unchanged when window size is 1", () => { + expect(slidingWindowMaxMonotonic([5, 3, 8, 1], 1)).toEqual([5, 3, 8, 1]); + }); + + it("handles a strictly increasing array", () => { + expect(slidingWindowMaxMonotonic([1, 2, 3, 4, 5], 3)).toEqual([3, 4, 5]); + }); + + it("handles a strictly decreasing array", () => { + expect(slidingWindowMaxMonotonic([5, 4, 3, 2, 1], 3)).toEqual([5, 4, 3]); + }); + + it("handles negative numbers correctly", () => { + expect(slidingWindowMaxMonotonic([-4, -2, -7, -1], 2)).toEqual([-2, -2, -1]); + }); + + it("handles a single-element array with window size 1", () => { + expect(slidingWindowMaxMonotonic([42], 1)).toEqual([42]); + }); + + it("handles all equal elements", () => { + expect(slidingWindowMaxMonotonic([3, 3, 3, 3], 2)).toEqual([3, 3, 3]); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/sources/SlidingWindowMaximum.java b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/sources/SlidingWindowMaximum.java new file mode 100644 index 00000000..24d41b91 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/sources/SlidingWindowMaximum.java @@ -0,0 +1,27 @@ +// Sliding Window Maximum — find the max in each window of size k using a monotonic deque of indices +import java.util.ArrayDeque; +import java.util.Deque; + +public class SlidingWindowMaximum { + public static int[] slidingWindowMaxMonotonic(int[] nums, int windowSize) { + Deque monoDeque = new ArrayDeque<>(); // @step:initialize + int[] result = new int[nums.length - windowSize + 1]; // @step:initialize + int resultIdx = 0; // @step:initialize + for (int elementIdx = 0; elementIdx < nums.length; elementIdx++) { // @step:visit + // Remove indices that have fallen outside the current window + while (!monoDeque.isEmpty() && monoDeque.peekFirst() <= elementIdx - windowSize) { // @step:dequeue + monoDeque.pollFirst(); // @step:dequeue + } + // Maintain monotonic decreasing order — remove smaller elements from the rear + while (!monoDeque.isEmpty() && nums[monoDeque.peekLast()] <= nums[elementIdx]) { // @step:maintain-monotonic + monoDeque.pollLast(); // @step:maintain-monotonic + } + monoDeque.offerLast(elementIdx); // @step:enqueue + // Once the first full window is reached, record the maximum (front of deque) + if (elementIdx >= windowSize - 1) { // @step:peek + result[resultIdx++] = nums[monoDeque.peekFirst()]; // @step:peek + } + } + return result; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/sources/sliding-window-maximum.py b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/sources/sliding-window-maximum.py new file mode 100644 index 00000000..3b311768 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/sources/sliding-window-maximum.py @@ -0,0 +1,18 @@ +# Sliding Window Maximum — find the max in each window of size k using a monotonic deque of indices +from collections import deque + +def sliding_window_max_monotonic(nums, window_size): + mono_deque = deque() # @step:initialize + result = [] # @step:initialize + for element_idx in range(len(nums)): # @step:visit + # Remove indices that have fallen outside the current window + while mono_deque and mono_deque[0] <= element_idx - window_size: # @step:dequeue + mono_deque.popleft() # @step:dequeue + # Maintain monotonic decreasing order — remove smaller elements from the rear + while mono_deque and nums[mono_deque[-1]] <= nums[element_idx]: # @step:maintain-monotonic + mono_deque.pop() # @step:maintain-monotonic + mono_deque.append(element_idx) # @step:enqueue + # Once the first full window is reached, record the maximum (front of deque) + if element_idx >= window_size - 1: # @step:peek + result.append(nums[mono_deque[0]]) # @step:peek + return result # @step:complete diff --git a/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/sources/sliding-window-maximum.ts b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/sources/sliding-window-maximum.ts new file mode 100644 index 00000000..6ffb870d --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/sources/sliding-window-maximum.ts @@ -0,0 +1,25 @@ +// Sliding Window Maximum — find the max in each window of size k using a monotonic deque of indices +function slidingWindowMaxMonotonic(nums: number[], windowSize: number): number[] { + const deque: number[] = []; // @step:initialize + const result: number[] = []; // @step:initialize + for (let elementIdx = 0; elementIdx < nums.length; elementIdx++) { + // @step:visit + // Remove indices that have fallen outside the current window + while (deque.length > 0 && deque[0]! <= elementIdx - windowSize) { + // @step:dequeue + deque.shift(); // @step:dequeue + } + // Maintain monotonic decreasing order — remove smaller elements from the rear + while (deque.length > 0 && nums[deque[deque.length - 1]!]! <= nums[elementIdx]!) { + // @step:maintain-monotonic + deque.pop(); // @step:maintain-monotonic + } + deque.push(elementIdx); // @step:enqueue + // Once the first full window is reached, record the maximum (front of deque) + if (elementIdx >= windowSize - 1) { + // @step:peek + result.push(nums[deque[0]!]!); // @step:peek + } + } + return result; // @step:complete +} diff --git a/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/step-generator.test.ts b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/step-generator.test.ts new file mode 100644 index 00000000..7c9abeac --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/step-generator.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect } from "vitest"; +import { generateSlidingWindowMaximumSteps } from "./step-generator"; + +const DEFAULT_INPUT = { nums: [1, 3, -1, -3, 5, 3, 6, 7], windowSize: 3 }; + +describe("generateSlidingWindowMaximumSteps", () => { + it("produces steps for the default input", () => { + const steps = generateSlidingWindowMaximumSteps(DEFAULT_INPUT); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateSlidingWindowMaximumSteps(DEFAULT_INPUT); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateSlidingWindowMaximumSteps(DEFAULT_INPUT); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateSlidingWindowMaximumSteps(DEFAULT_INPUT); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateSlidingWindowMaximumSteps(DEFAULT_INPUT); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits a visit step for each element", () => { + const steps = generateSlidingWindowMaximumSteps(DEFAULT_INPUT); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(DEFAULT_INPUT.nums.length); + }); + + it("emits peek steps equal to the number of windows", () => { + const steps = generateSlidingWindowMaximumSteps(DEFAULT_INPUT); + const peekSteps = steps.filter((step) => step.type === "peek"); + const expectedWindowCount = DEFAULT_INPUT.nums.length - DEFAULT_INPUT.windowSize + 1; + expect(peekSteps.length).toBe(expectedWindowCount); + }); + + it("records the correct window maxima in complete step variables", () => { + const steps = generateSlidingWindowMaximumSteps(DEFAULT_INPUT); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.variables["result"]).toEqual([3, 3, 5, 5, 6, 7]); + }); + + it("emits enqueue steps equal to the number of elements", () => { + const steps = generateSlidingWindowMaximumSteps(DEFAULT_INPUT); + const enqueueSteps = steps.filter((step) => step.type === "enqueue"); + expect(enqueueSteps.length).toBe(DEFAULT_INPUT.nums.length); + }); +}); diff --git a/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/step-generator.ts b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/step-generator.ts new file mode 100644 index 00000000..691f9a16 --- /dev/null +++ b/src/algorithms/stacks-queues/queue-operations/sliding-window-maximum/step-generator.ts @@ -0,0 +1,118 @@ +/** Step generator for Sliding Window Maximum — produces ExecutionStep[] using QueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { QueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const SWM_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.SLIDING_WINDOW_MAXIMUM!); + +export interface SlidingWindowMaximumInput { + nums: number[]; + windowSize: number; +} + +export function generateSlidingWindowMaximumSteps( + input: SlidingWindowMaximumInput, +): ExecutionStep[] { + const { nums, windowSize } = input; + const tracker = new QueueTracker(nums, SWM_LINE_MAP); + + // deque stores indices; we shadow the tracker's queue with our own index array + const deque: number[] = []; + const result: number[] = []; + + tracker.initialize({ + nums: [...nums], + windowSize, + deque: [], + result: [], + }); + + for (let elementIdx = 0; elementIdx < nums.length; elementIdx++) { + const currentValue = nums[elementIdx]!; + + tracker.processElement(elementIdx, { + elementIdx, + currentValue, + deque: [...deque], + result: [...result], + }); + + // Remove indices that have fallen outside the current window + while (deque.length > 0 && deque[0]! <= elementIdx - windowSize) { + const expiredIdx = deque[0]!; + const expiredValue = nums[expiredIdx]!; + tracker.dequeue( + { + elementIdx, + expiredIdx, + expiredValue, + windowStart: elementIdx - windowSize + 1, + deque: [...deque], + result: [...result], + }, + `Remove index ${expiredIdx} (value=${expiredValue}) — outside window starting at ${elementIdx - windowSize + 1}`, + ); + deque.shift(); + } + + // Maintain monotonic decreasing order — remove smaller elements from the rear + while (deque.length > 0 && nums[deque[deque.length - 1]!]! <= currentValue) { + const smallerIdx = deque[deque.length - 1]!; + const smallerValue = nums[smallerIdx]!; + tracker.maintainDeque( + { + elementIdx, + currentValue, + smallerIdx, + smallerValue, + deque: [...deque], + result: [...result], + }, + `Remove index ${smallerIdx} (value=${smallerValue}) from rear — smaller than current ${currentValue}`, + ); + deque.pop(); + } + + deque.push(elementIdx); + tracker.enqueue( + String(elementIdx), + { + elementIdx, + currentValue, + deque: [...deque], + result: [...result], + }, + `Enqueue index ${elementIdx} (value=${currentValue}) at deque rear`, + ); + + // Once the first full window is reached, record the maximum (front of deque) + if (elementIdx >= windowSize - 1) { + const maxIdx = deque[0]!; + const maxValue = nums[maxIdx]!; + result.push(maxValue); + tracker.peekFront( + { + elementIdx, + windowStart: elementIdx - windowSize + 1, + maxIdx, + maxValue, + deque: [...deque], + result: [...result], + }, + `Window [${elementIdx - windowSize + 1}, ${elementIdx}] max = ${maxValue} at index ${maxIdx}`, + ); + } + } + + tracker.complete( + { + result: [...result], + totalWindows: result.length, + }, + `Sliding window maximum complete — ${result.length} windows, result: [${result.join(", ")}]`, + ); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/BackspaceStringComparePipeline.stories.tsx b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/BackspaceStringComparePipeline.stories.tsx new file mode 100644 index 00000000..43fd3dfe --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/BackspaceStringComparePipeline.stories.tsx @@ -0,0 +1,62 @@ +/** + * Storybook stories for the Backspace String Compare algorithm pipeline. + * Uses the real step generator with "ab#c" / "ad#c", rendering the + * StackQueueVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateBackspaceStringCompareSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const equalSteps = generateBackspaceStringCompareSteps({ + firstString: "ab#c", + secondString: "ad#c", +}); +const unequalSteps = generateBackspaceStringCompareSteps({ + firstString: "a#c", + secondString: "b", +}); + +const meta: Meta = { + title: "Algorithm Pipelines/Backspace String Compare", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — combined input string unprocessed, empty stack */ +export const InitialState: Story = { + args: { + visualState: equalSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing with some characters pushed onto the stack */ +export const MidProcessing: Story = { + args: { + visualState: equalSteps[Math.floor(equalSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Completed — both strings resolve to equal results */ +export const EqualStrings: Story = { + args: { + visualState: equalSteps[equalSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Completed — strings resolve to different results */ +export const UnequalStrings: Story = { + args: { + visualState: unequalSteps[unequalSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/backspace-string-compare.test.ts b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/backspace-string-compare.test.ts new file mode 100644 index 00000000..67e49bff --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/backspace-string-compare.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from "vitest"; +import { backspaceStringCompare } from "./sources/backspace-string-compare.ts?fn"; + +describe("backspaceStringCompare", () => { + it("returns true when both strings resolve to the same characters", () => { + expect(backspaceStringCompare("ab#c", "ad#c")).toBe(true); + }); + + it("returns true when both strings are fully erased by backspaces", () => { + expect(backspaceStringCompare("ab##", "c#d#")).toBe(true); + }); + + it("returns false when processed strings differ", () => { + expect(backspaceStringCompare("a#c", "b")).toBe(false); + }); + + it("returns true when both strings are empty", () => { + expect(backspaceStringCompare("", "")).toBe(true); + }); + + it("returns true when both strings are identical with no backspaces", () => { + expect(backspaceStringCompare("a", "a")).toBe(true); + }); + + it("returns false when strings have different lengths after processing", () => { + expect(backspaceStringCompare("abc", "a")).toBe(false); + }); + + it("handles a backspace on an already-empty stack gracefully", () => { + expect(backspaceStringCompare("#a", "a")).toBe(true); + }); + + it("returns true when multiple backspaces reduce both strings to the same result", () => { + expect(backspaceStringCompare("nzp#o#g", "b#nzp#o#g")).toBe(true); + }); +}); diff --git a/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/educational.ts b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/educational.ts new file mode 100644 index 00000000..4f14d469 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/educational.ts @@ -0,0 +1,59 @@ +import type { EducationalContent } from "@/types"; + +export const backspaceStringCompareEducational: EducationalContent = { + overview: + "**Backspace String Compare** solves LeetCode 844: given two strings where `#` represents a backspace key, determine whether the two strings are equal after all backspaces are applied.\n\nA stack naturally models the backspace operation — push regular characters, and on `#` pop the top character (if any). After processing both strings, compare the two resulting stacks for equality.", + + howItWorks: + "The algorithm processes each string independently using a stack:\n\n" + + "1. **For each character in the string:**\n" + + " - If the character is not `#` → push it onto the stack.\n" + + " - If the character is `#` and the stack is non-empty → pop the top character.\n" + + " - If the character is `#` and the stack is empty → do nothing (backspace on empty text).\n" + + "2. **After processing both strings**, compare the two stacks element by element.\n" + + "3. **Return true** if both stacks are identical in length and content.\n\n" + + "### Example trace on `ab#c` vs `ad#c`\n\n" + + "```\n" + + "Processing 'ab#c': Processing 'ad#c':\n" + + "char action stack char action stack\n" + + "a push [a] a push [a]\n" + + "b push [a, b] d push [a, d]\n" + + "# pop [a] # pop [a]\n" + + "c push [a, c] c push [a, c]\n" + + "\n" + + "Result: [a, c] == [a, c] → true\n" + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n + m)`**\n\n" + + "Each character of both strings is visited exactly once. Each push and pop is `O(1)`, so the total work is linear in the combined length of both input strings (`n` for the first, `m` for the second).\n\n" + + "**Space Complexity: `O(n + m)`**\n\n" + + "In the worst case (no backspaces) both stacks hold the full content of their respective strings, using `O(n + m)` extra space. An optimised two-pointer approach achieves `O(1)` space but the stack version is clearer.", + + bestAndWorstCase: + "**Best case** — one string starts with `#` while empty, allowing early termination if length differences accumulate quickly. Still `O(n + m)` in general.\n\n" + + "**Worst case** — both strings have no `#` characters at all, so every character is pushed and both stacks must be fully compared: `O(n + m)` time and space.\n\n" + + "In practice the algorithm always runs in `O(n + m)` regardless of input content.", + + realWorldUses: [ + "**Text editor undo/delete:** Simulating how a text buffer looks after a sequence of character inputs and deletions, the same stack model applies.", + "**Terminal input processing:** Unix terminals process backspace (`\\b` or `DEL`) the same way — accumulated input characters can be erased before the line is submitted.", + "**Command-line parsers:** Shells that allow inline editing before execution use an equivalent push/pop model for character buffers.", + "**Diff and comparison tools:** Understanding how two sequences relate after local edits is a micro-instance of the broader string comparison problem.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(n + m) time — single pass over each string with no backtracking.", + "Simple and direct: the stack perfectly mirrors the intuition of typing with a backspace key.", + "Handles edge cases naturally — backspacing an empty string does nothing without extra conditionals.", + ], + limitations: [ + "O(n + m) extra space for the two stacks — avoidable with a two-pointer reverse scan at the cost of readability.", + "Only models a simple delete-last-character backspace; does not handle cursor movement or multi-character delete.", + ], + }, + + whenToUseIt: + "Use this stack approach when clarity is more important than minimising space, or when the problem is part of a larger pipeline that already uses stacks. If `O(1)` space is required, use the two-pointer approach that scans both strings from right to left, counting pending backspaces as it goes.", +}; diff --git a/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/index.ts b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/index.ts new file mode 100644 index 00000000..65242ef8 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { backspaceStringCompare } from "./sources/backspace-string-compare.ts?fn"; +import { generateBackspaceStringCompareSteps } from "./step-generator"; +import type { BackspaceStringCompareInput } from "./step-generator"; +import { backspaceStringCompareEducational } from "./educational"; + +import typescriptSource from "./sources/backspace-string-compare.ts?raw"; +import pythonSource from "./sources/backspace-string-compare.py?raw"; +import javaSource from "./sources/BackspaceStringCompare.java?raw"; + +function executeBackspaceStringCompare(input: BackspaceStringCompareInput): boolean { + return backspaceStringCompare(input.firstString, input.secondString) as boolean; +} + +const backspaceStringCompareDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.BACKSPACE_STRING_COMPARE!, + name: "Backspace String Compare", + category: CATEGORY.STACKS_QUEUES!, + technique: "stack-applications", + description: + "Use a stack to process each string where '#' acts as backspace, then compare the two resulting stacks for equality", + timeComplexity: { + best: "O(n+m)", + average: "O(n+m)", + worst: "O(n+m)", + }, + spaceComplexity: "O(n+m)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { firstString: "ab#c", secondString: "ad#c" }, + }, + execute: executeBackspaceStringCompare, + generateSteps: generateBackspaceStringCompareSteps, + educational: backspaceStringCompareEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(backspaceStringCompareDefinition); diff --git a/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/sources/BackspaceStringCompare.java b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/sources/BackspaceStringCompare.java new file mode 100644 index 00000000..45018cb4 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/sources/BackspaceStringCompare.java @@ -0,0 +1,34 @@ +// Backspace String Compare — use a stack to process each string, treating '#' as backspace +import java.util.ArrayDeque; +import java.util.Deque; + +public class BackspaceStringCompare { + private static Deque processWithBackspace(String inputStr) { + Deque resultStack = new ArrayDeque<>(); // @step:initialize + for (int charIdx = 0; charIdx < inputStr.length(); charIdx++) { + char ch = inputStr.charAt(charIdx); // @step:visit + if (ch == '#') { + if (!resultStack.isEmpty()) { + resultStack.pop(); // @step:pop + } + } else { + resultStack.push(ch); // @step:push + } + } + return resultStack; // @step:compare + } + + public static boolean backspaceStringCompare(String firstString, String secondString) { + Deque processedFirst = processWithBackspace(firstString); // @step:initialize + Deque processedSecond = processWithBackspace(secondString); // @step:initialize + if (processedFirst.size() != processedSecond.size()) { // @step:compare + return false; // @step:compare + } + while (!processedFirst.isEmpty()) { // @step:compare + if (!processedFirst.pop().equals(processedSecond.pop())) { // @step:compare + return false; // @step:compare + } + } + return true; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/sources/backspace-string-compare.py b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/sources/backspace-string-compare.py new file mode 100644 index 00000000..27178039 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/sources/backspace-string-compare.py @@ -0,0 +1,21 @@ +# Backspace String Compare — use a stack to process each string, treating '#' as backspace +def process_with_backspace(input_str): + result_stack = [] # @step:initialize + for char in input_str: # @step:visit + if char == "#": + if result_stack: + result_stack.pop() # @step:pop + else: + result_stack.append(char) # @step:push + return result_stack # @step:compare + + +def backspace_string_compare(first_string, second_string): + processed_first = process_with_backspace(first_string) # @step:initialize + processed_second = process_with_backspace(second_string) # @step:initialize + if len(processed_first) != len(processed_second): # @step:compare + return False # @step:compare + for char_idx in range(len(processed_first)): # @step:compare + if processed_first[char_idx] != processed_second[char_idx]: # @step:compare + return False # @step:compare + return True # @step:complete diff --git a/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/sources/backspace-string-compare.ts b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/sources/backspace-string-compare.ts new file mode 100644 index 00000000..7476a657 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/sources/backspace-string-compare.ts @@ -0,0 +1,27 @@ +// Backspace String Compare — use a stack to process each string, treating '#' as backspace +function processWithBackspace(inputStr: string): string[] { + const resultStack: string[] = []; // @step:initialize + for (let charIdx = 0; charIdx < inputStr.length; charIdx++) { + const char = inputStr[charIdx]!; // @step:visit + if (char === "#") { + resultStack.pop(); // @step:pop + } else { + resultStack.push(char); // @step:push + } + } + return resultStack; // @step:compare +} + +function backspaceStringCompare(firstString: string, secondString: string): boolean { + const processedFirst = processWithBackspace(firstString); // @step:initialize + const processedSecond = processWithBackspace(secondString); // @step:initialize + if (processedFirst.length !== processedSecond.length) { + return false; // @step:compare + } + for (let charIdx = 0; charIdx < processedFirst.length; charIdx++) { + if (processedFirst[charIdx] !== processedSecond[charIdx]) { + return false; // @step:compare + } + } + return true; // @step:complete +} diff --git a/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/step-generator.test.ts b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/step-generator.test.ts new file mode 100644 index 00000000..1a7cfd85 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/step-generator.test.ts @@ -0,0 +1,105 @@ +import { describe, it, expect } from "vitest"; +import { generateBackspaceStringCompareSteps } from "./step-generator"; + +describe("generateBackspaceStringCompareSteps", () => { + it("produces steps for the default input", () => { + const steps = generateBackspaceStringCompareSteps({ + firstString: "ab#c", + secondString: "ad#c", + }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateBackspaceStringCompareSteps({ + firstString: "ab#c", + secondString: "ad#c", + }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateBackspaceStringCompareSteps({ + firstString: "ab#c", + secondString: "ad#c", + }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateBackspaceStringCompareSteps({ + firstString: "ab#c", + secondString: "ad#c", + }); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateBackspaceStringCompareSteps({ + firstString: "ab#c", + secondString: "ad#c", + }); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits push steps for non-backspace characters", () => { + const steps = generateBackspaceStringCompareSteps({ + firstString: "ab#c", + secondString: "ad#c", + }); + const pushSteps = steps.filter((step) => step.type === "push"); + // "ab#c" → push a, push b, push c (3 pushes); "ad#c" → push a, push d, push c (3 pushes) = 6 total + expect(pushSteps.length).toBe(6); + }); + + it("emits match steps for backspace characters that pop a character", () => { + const steps = generateBackspaceStringCompareSteps({ + firstString: "ab#c", + secondString: "ad#c", + }); + const matchSteps = steps.filter((step) => step.type === "match"); + // One '#' in each string that has a char to pop = 2 match steps + expect(matchSteps.length).toBe(2); + }); + + it("marks the complete step as equal for matching strings", () => { + const steps = generateBackspaceStringCompareSteps({ + firstString: "ab#c", + secondString: "ad#c", + }); + const completeStep = steps[steps.length - 1]; + expect(completeStep?.variables).toMatchObject({ isEqual: true }); + }); + + it("marks the complete step as not equal for non-matching strings", () => { + const steps = generateBackspaceStringCompareSteps({ + firstString: "a#c", + secondString: "b", + }); + const completeStep = steps[steps.length - 1]; + expect(completeStep?.variables).toMatchObject({ isEqual: false }); + }); + + it("handles empty strings without errors", () => { + const steps = generateBackspaceStringCompareSteps({ + firstString: "", + secondString: "", + }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("handles backspace on empty stack without emitting a match step", () => { + const steps = generateBackspaceStringCompareSteps({ + firstString: "#a", + secondString: "a", + }); + // The '#' at the start of firstString hits an empty stack — no match step for it + const matchSteps = steps.filter((step) => step.type === "match"); + expect(matchSteps.length).toBe(0); + }); +}); diff --git a/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/step-generator.ts b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/step-generator.ts new file mode 100644 index 00000000..8ed96b9d --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/backspace-string-compare/step-generator.ts @@ -0,0 +1,78 @@ +/** Step generator for Backspace String Compare — produces ExecutionStep[] using StackQueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { StackQueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const BSC_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.BACKSPACE_STRING_COMPARE!); + +export interface BackspaceStringCompareInput { + firstString: string; + secondString: string; +} + +function processStringSteps( + inputStr: string, + charOffset: number, + tracker: StackQueueTracker, + stack: string[], + label: string, +): void { + for (let charIdx = 0; charIdx < inputStr.length; charIdx++) { + const char = inputStr[charIdx]!; + const globalIdx = charOffset + charIdx; + + tracker.processChar(globalIdx, { char, charIdx, globalIdx, string: label }); + + if (char === "#") { + const stackTop = stack[stack.length - 1]; + if (stack.length > 0) { + stack.pop(); + tracker.popMatched("#", globalIdx, { + char, + popped: stackTop, + stackSize: stack.length, + string: label, + }); + } + } else { + stack.push(char); + tracker.push(char, globalIdx, { char, stackSize: stack.length, string: label }); + } + } +} + +export function generateBackspaceStringCompareSteps( + input: BackspaceStringCompareInput, +): ExecutionStep[] { + const { firstString, secondString } = input; + + // Concatenate both strings with a "|" separator for visualization + const combinedInput = firstString + "|" + secondString; + const tracker = new StackQueueTracker(combinedInput, BSC_LINE_MAP); + + const firstStack: string[] = []; + const secondStack: string[] = []; + + tracker.initialize({ firstString, secondString }); + + // Process first string + processStringSteps(firstString, 0, tracker, firstStack, "first"); + + // Process second string (offset by firstString.length + 1 for the separator) + processStringSteps(secondString, firstString.length + 1, tracker, secondStack, "second"); + + // Compare the two resulting stacks + const isEqual = + firstStack.length === secondStack.length && + firstStack.every((char, stackIdx) => char === secondStack[stackIdx]); + + tracker.complete(isEqual, { + firstResult: [...firstStack], + secondResult: [...secondStack], + isEqual, + }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/RemoveAllAdjacentDuplicatesPipeline.stories.tsx b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/RemoveAllAdjacentDuplicatesPipeline.stories.tsx new file mode 100644 index 00000000..970ef956 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/RemoveAllAdjacentDuplicatesPipeline.stories.tsx @@ -0,0 +1,56 @@ +/** + * Storybook stories for the Remove All Adjacent Duplicates algorithm pipeline. + * Uses the real step generator with "abbaca", rendering the + * StackQueueVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateRemoveAllAdjacentDuplicatesSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateRemoveAllAdjacentDuplicatesSteps({ inputString: "abbaca" }); +const cascadeSteps = generateRemoveAllAdjacentDuplicatesSteps({ inputString: "azxxzy" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Remove All Adjacent Duplicates", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — full input string unprocessed, empty stack */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing with some characters pushed onto the stack */ +export const MidProcessing: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Completed — result string 'ca' remains after all duplicate pairs removed */ +export const Completed: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Cascading removals — 'azxxzy' collapses to 'ay' via two separate pair removals */ +export const CascadingRemovals: Story = { + args: { + visualState: cascadeSteps[cascadeSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/educational.ts b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/educational.ts new file mode 100644 index 00000000..804e776e --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/educational.ts @@ -0,0 +1,58 @@ +import type { EducationalContent } from "@/types"; + +export const removeAllAdjacentDuplicatesEducational: EducationalContent = { + overview: + '**Remove All Adjacent Duplicates** eliminates every pair of adjacent identical characters from a string, repeating until no adjacent duplicates remain. For example, `"abbaca"` → `"aaca"` → `"ca"`.\n\nA stack provides an elegant single-pass solution: push each character, but if it matches the current stack top, pop instead — cancelling the duplicate pair on the spot.', + + howItWorks: + "The algorithm scans the string left to right, maintaining a stack of unmatched characters:\n\n" + + "1. **For each character**, peek at the stack top.\n" + + "2. **Match** — if the stack is non-empty and the top equals the current character, pop the top (the duplicate pair is cancelled).\n" + + "3. **No match** — otherwise push the character onto the stack.\n" + + "4. **End of string** → join the stack contents into the result string.\n\n" + + "### Example trace on `abbaca`\n\n" + + "```\n" + + "char action stack\n" + + "a push [a]\n" + + "b push [a, b]\n" + + "b pop ✓ [a] ← bb removed\n" + + "a pop ✓ [] ← aa removed\n" + + "c push [c]\n" + + "a push [c, a]\n" + + 'end join → "ca"\n' + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each character is visited exactly once. Every push and pop is `O(1)`, so the total work is linear in the length of the input string.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + 'In the worst case (a string with no adjacent duplicates, e.g. `"abcde"`) all characters are pushed onto the stack, using `O(n)` extra space.', + + bestAndWorstCase: + '**Best case** — every character pairs with the previous one, so pops dominate and the stack stays small. The string `"aabbcc"` fully collapses to `""` and the stack never grows beyond one element at a time.\n\n' + + '**Worst case** — no adjacent duplicates exist (e.g. `"abcde"`): every character is pushed and the stack reaches size `n`, using `O(n)` time and `O(n)` space.\n\n' + + "Both cases are `O(n)` — the algorithm cannot skip any character.", + + realWorldUses: [ + "**Text editors:** Implementing undo/redo sequences where paired inverse operations cancel out.", + "**Compiler optimisations:** Simplifying sequences of push/pop or load/store pairs in intermediate code.", + "**Chemistry and biology:** Collapsing repeated base-pair sequences in DNA string processing pipelines.", + "**Game mechanics:** Bubble-shooter and match-3 games that remove groups of identical adjacent tiles.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(n) time — single left-to-right scan with no backtracking or re-scanning.", + "Handles cascading removals naturally: once a pair is removed, the newly adjacent characters are compared automatically.", + "Simple, readable implementation that directly mirrors the intuition of 'cancel matching neighbours'.", + ], + limitations: [ + "O(n) extra space for the stack in the worst case.", + "Only removes pairs of two — does not generalise to groups of k duplicates without modification.", + "The result order depends on stack behaviour; characters that survive appear in their original relative order.", + ], + }, + + whenToUseIt: + "Use the stack-based approach whenever adjacent duplicate pairs must be removed in a single pass, especially when removals can cascade. If you need to remove groups of k identical adjacent characters (LeetCode 1209), extend this pattern by storing `(char, count)` pairs on the stack instead of plain characters.", +}; diff --git a/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/index.ts b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/index.ts new file mode 100644 index 00000000..63da9eba --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/index.ts @@ -0,0 +1,46 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { removeAllAdjacentDuplicates } from "./sources/remove-all-adjacent-duplicates.ts?fn"; +import { generateRemoveAllAdjacentDuplicatesSteps } from "./step-generator"; +import type { RemoveAllAdjacentDuplicatesInput } from "./step-generator"; +import { removeAllAdjacentDuplicatesEducational } from "./educational"; + +import typescriptSource from "./sources/remove-all-adjacent-duplicates.ts?raw"; +import pythonSource from "./sources/remove-all-adjacent-duplicates.py?raw"; +import javaSource from "./sources/RemoveAllAdjacentDuplicates.java?raw"; + +function executeRemoveAllAdjacentDuplicates(input: RemoveAllAdjacentDuplicatesInput): string { + return removeAllAdjacentDuplicates(input.inputString) as string; +} + +const removeAllAdjacentDuplicatesDefinition: AlgorithmDefinition = + { + meta: { + id: ALGORITHM_ID.REMOVE_ALL_ADJACENT_DUPLICATES!, + name: "Remove All Adjacent Duplicates", + category: CATEGORY.STACKS_QUEUES!, + technique: "stack-applications", + description: + "Use a stack to cancel adjacent duplicate character pairs in a single pass, producing the fully deduplicated string", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { inputString: "abbaca" }, + }, + execute: executeRemoveAllAdjacentDuplicates, + generateSteps: generateRemoveAllAdjacentDuplicatesSteps, + educational: removeAllAdjacentDuplicatesEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, + }; + +registry.register(removeAllAdjacentDuplicatesDefinition); diff --git a/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/remove-all-adjacent-duplicates.test.ts b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/remove-all-adjacent-duplicates.test.ts new file mode 100644 index 00000000..31d0ae7a --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/remove-all-adjacent-duplicates.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from "vitest"; +import { removeAllAdjacentDuplicates } from "./sources/remove-all-adjacent-duplicates.ts?fn"; + +describe("removeAllAdjacentDuplicates", () => { + it("returns 'ca' for the default input 'abbaca'", () => { + expect(removeAllAdjacentDuplicates("abbaca")).toBe("ca"); + }); + + it("returns 'ay' for 'azxxzy' with cascading removals", () => { + expect(removeAllAdjacentDuplicates("azxxzy")).toBe("ay"); + }); + + it("returns an empty string for an empty input", () => { + expect(removeAllAdjacentDuplicates("")).toBe(""); + }); + + it("returns the original string when there are no adjacent duplicates", () => { + expect(removeAllAdjacentDuplicates("abc")).toBe("abc"); + }); + + it("returns an empty string when all characters are the same", () => { + expect(removeAllAdjacentDuplicates("aaaaaa")).toBe(""); + }); + + it("returns an empty string when all pairs cancel out completely", () => { + expect(removeAllAdjacentDuplicates("aabb")).toBe(""); + }); + + it("handles a single character without modification", () => { + expect(removeAllAdjacentDuplicates("a")).toBe("a"); + }); + + it("handles a string with a single adjacent duplicate pair", () => { + expect(removeAllAdjacentDuplicates("abba")).toBe(""); + }); +}); diff --git a/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/sources/RemoveAllAdjacentDuplicates.java b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/sources/RemoveAllAdjacentDuplicates.java new file mode 100644 index 00000000..c00fd2ad --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/sources/RemoveAllAdjacentDuplicates.java @@ -0,0 +1,24 @@ +// Remove All Adjacent Duplicates — use a stack to repeatedly remove adjacent duplicate pairs +import java.util.ArrayDeque; +import java.util.Deque; + +public class RemoveAllAdjacentDuplicates { + public static String removeAllAdjacentDuplicates(String inputString) { + Deque stack = new ArrayDeque<>(); // @step:initialize + for (int charIdx = 0; charIdx < inputString.length(); charIdx++) { + char ch = inputString.charAt(charIdx); // @step:visit + Character stackTop = stack.isEmpty() ? null : stack.peek(); // @step:visit + if (!stack.isEmpty() && stackTop != null && stackTop == ch) { + stack.pop(); // @step:match + } else { + stack.push(ch); // @step:push + } + } + // Remaining stack characters form the result after all duplicate pairs removed + StringBuilder result = new StringBuilder(); + while (!stack.isEmpty()) { + result.insert(0, stack.pop()); // @step:complete + } + return result.toString(); // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/sources/remove-all-adjacent-duplicates.py b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/sources/remove-all-adjacent-duplicates.py new file mode 100644 index 00000000..1594682f --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/sources/remove-all-adjacent-duplicates.py @@ -0,0 +1,11 @@ +# Remove All Adjacent Duplicates — use a stack to repeatedly remove adjacent duplicate pairs +def remove_all_adjacent_duplicates(input_string): + stack = [] # @step:initialize + for char in input_string: # @step:visit + stack_top = stack[-1] if stack else None # @step:visit + if stack and stack_top == char: + stack.pop() # @step:match + else: + stack.append(char) # @step:push + # Remaining stack characters form the result after all duplicate pairs removed + return "".join(stack) # @step:complete diff --git a/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/sources/remove-all-adjacent-duplicates.ts b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/sources/remove-all-adjacent-duplicates.ts new file mode 100644 index 00000000..bd5795f5 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/sources/remove-all-adjacent-duplicates.ts @@ -0,0 +1,15 @@ +// Remove All Adjacent Duplicates — use a stack to repeatedly remove adjacent duplicate pairs +function removeAllAdjacentDuplicates(inputString: string): string { + const stack: string[] = []; // @step:initialize + for (let charIdx = 0; charIdx < inputString.length; charIdx++) { + const char = inputString[charIdx]!; // @step:visit + const stackTop = stack[stack.length - 1]; // @step:visit + if (stack.length > 0 && stackTop === char) { + stack.pop(); // @step:match + } else { + stack.push(char); // @step:push + } + } + // Remaining stack characters form the result after all duplicate pairs removed + return stack.join(""); // @step:complete +} diff --git a/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/step-generator.test.ts b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/step-generator.test.ts new file mode 100644 index 00000000..a0715a4c --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/step-generator.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect } from "vitest"; +import { generateRemoveAllAdjacentDuplicatesSteps } from "./step-generator"; + +describe("generateRemoveAllAdjacentDuplicatesSteps", () => { + it("produces steps for the default input", () => { + const steps = generateRemoveAllAdjacentDuplicatesSteps({ inputString: "abbaca" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateRemoveAllAdjacentDuplicatesSteps({ inputString: "abbaca" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateRemoveAllAdjacentDuplicatesSteps({ inputString: "abbaca" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateRemoveAllAdjacentDuplicatesSteps({ inputString: "abbaca" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateRemoveAllAdjacentDuplicatesSteps({ inputString: "abbaca" }); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits push steps for characters that do not match the stack top", () => { + const steps = generateRemoveAllAdjacentDuplicatesSteps({ inputString: "abbaca" }); + const pushSteps = steps.filter((step) => step.type === "push"); + // a→push, b→push, b→match, a→match, c→push, a→push = 4 pushes + expect(pushSteps.length).toBe(4); + }); + + it("emits match steps for duplicate pairs that are popped", () => { + const steps = generateRemoveAllAdjacentDuplicatesSteps({ inputString: "abbaca" }); + const matchSteps = steps.filter((step) => step.type === "match"); + // b-b pair and a-a pair = 2 match steps + expect(matchSteps.length).toBe(2); + }); + + it("records the correct result in the complete step variables", () => { + const steps = generateRemoveAllAdjacentDuplicatesSteps({ inputString: "abbaca" }); + const completeStep = steps[steps.length - 1]; + expect(completeStep?.variables).toMatchObject({ result: "ca" }); + }); + + it("handles an empty string without errors", () => { + const steps = generateRemoveAllAdjacentDuplicatesSteps({ inputString: "" }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("records an empty result for a fully collapsing string", () => { + const steps = generateRemoveAllAdjacentDuplicatesSteps({ inputString: "aaaaaa" }); + const completeStep = steps[steps.length - 1]; + expect(completeStep?.variables).toMatchObject({ result: "" }); + }); + + it("handles a string with no adjacent duplicates", () => { + const steps = generateRemoveAllAdjacentDuplicatesSteps({ inputString: "abc" }); + const completeStep = steps[steps.length - 1]; + expect(completeStep?.variables).toMatchObject({ result: "abc" }); + }); +}); diff --git a/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/step-generator.ts b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/step-generator.ts new file mode 100644 index 00000000..d6aafd57 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/remove-all-adjacent-duplicates/step-generator.ts @@ -0,0 +1,42 @@ +/** Step generator for Remove All Adjacent Duplicates — produces ExecutionStep[] using StackQueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { StackQueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const RAAD_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.REMOVE_ALL_ADJACENT_DUPLICATES!); + +export interface RemoveAllAdjacentDuplicatesInput { + inputString: string; +} + +export function generateRemoveAllAdjacentDuplicatesSteps( + input: RemoveAllAdjacentDuplicatesInput, +): ExecutionStep[] { + const { inputString } = input; + const tracker = new StackQueueTracker(inputString, RAAD_LINE_MAP); + const stack: string[] = []; + + tracker.initialize({ inputString }); + + for (let charIdx = 0; charIdx < inputString.length; charIdx++) { + const char = inputString[charIdx]!; + const stackTop = stack[stack.length - 1]; + + tracker.processChar(charIdx, { charIdx, char, stackTop: stackTop ?? "empty" }); + + if (stack.length > 0 && stackTop === char) { + stack.pop(); + tracker.popMatched(char, charIdx, { char, stackSize: stack.length }); + } else { + stack.push(char); + tracker.push(char, charIdx, { char, stackSize: stack.length }); + } + } + + const result = stack.join(""); + tracker.complete(true, { result, remainingStack: [...stack] }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/stack-applications/simplify-path/SimplifyPathPipeline.stories.tsx b/src/algorithms/stacks-queues/stack-applications/simplify-path/SimplifyPathPipeline.stories.tsx new file mode 100644 index 00000000..5cc17746 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/simplify-path/SimplifyPathPipeline.stories.tsx @@ -0,0 +1,56 @@ +/** + * Storybook stories for the Simplify Path algorithm pipeline. + * Uses the real step generator with "/a/./b/../../c/", rendering the + * StackQueueVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateSimplifyPathSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateSimplifyPathSteps({ inputString: "/a/./b/../../c/" }); +const deepSteps = generateSimplifyPathSteps({ inputString: "/home/user/docs/../downloads" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Simplify Path", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — full input path unprocessed, empty stack */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing with some directories pushed onto the stack */ +export const MidProcessing: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Completed — stack contains only the canonical path segments */ +export const Completed: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Deeper path with a parent-directory pop mid-traversal */ +export const DeepPathWithPop: Story = { + args: { + visualState: deepSteps[Math.floor(deepSteps.length / 2)]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/stack-applications/simplify-path/educational.ts b/src/algorithms/stacks-queues/stack-applications/simplify-path/educational.ts new file mode 100644 index 00000000..9f401a98 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/simplify-path/educational.ts @@ -0,0 +1,59 @@ +import type { EducationalContent } from "@/types"; + +export const simplifyPathEducational: EducationalContent = { + overview: + "**Simplify Path** (LeetCode 71) converts an absolute Unix file-system path into its canonical form. Components like `.` (current directory) and `..` (parent directory) must be resolved, and consecutive slashes must be collapsed.\n\nA stack is the natural tool: push valid directory names as you scan left to right, and pop the top whenever `..` is encountered. The final stack contents, joined with `/`, form the simplified path.", + + howItWorks: + "The algorithm splits the input on `/` and processes each component:\n\n" + + "1. **Empty string or `.`** — produced by consecutive slashes or a current-directory marker. Skip immediately.\n" + + "2. **`..`** — parent-directory reference. Pop the stack if it is non-empty; ignore it when at root.\n" + + "3. **Any other string** — a real directory name. Push it onto the stack.\n\n" + + "After all components are consumed, join the stack elements with `/` and prepend a leading `/`.\n\n" + + "### Example trace on `/a/./b/../../c/`\n\n" + + "```\n" + + "component action stack\n" + + '"" skip (empty) []\n' + + '"a" push ["a"]\n' + + '"." skip ["a"]\n' + + '"b" push ["a", "b"]\n' + + '".." pop ["a"]\n' + + '".." pop []\n' + + '"c" push ["c"]\n' + + '"" skip (empty) ["c"]\n' + + 'end → "/c"\n' + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Splitting the string and iterating over components both run in O(n) where n is the total length of the input string. Each component is pushed or popped at most once.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "In the worst case (a deeply nested path with no `..` components) every directory name is pushed onto the stack, consuming O(n) space.", + + bestAndWorstCase: + "**Best case** — input is already `/` (single slash): the split produces only empty strings, the stack stays empty, and the result `/` is returned immediately.\n\n" + + "**Worst case** — a long, deeply nested path with no `..` components: every segment is pushed, making the stack as large as the input, and the join step traverses the whole stack. Still O(n) overall.", + + realWorldUses: [ + "**Shell and terminal navigation:** `cd` implementations resolve relative path components before passing the canonical path to the OS kernel.", + "**Web servers:** URL path normalization removes `../` traversal attempts to prevent directory traversal security vulnerabilities.", + "**File browsers and IDEs:** Rendering the breadcrumb path and resolving symbolic links both rely on path canonicalization.", + "**Build tools:** Bundlers and compilers normalize import paths so that `./a/../b` and `./b` resolve to the same module.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(n) time — single left-to-right pass with constant-time stack operations.", + "Handles all edge cases (root-level `..`, consecutive slashes, trailing slashes) naturally.", + "Simple and readable — the stack mirrors the directory hierarchy directly.", + ], + limitations: [ + "O(n) extra space for the stack in the worst case.", + "Works only on absolute paths; relative paths require a separate base-path resolution step.", + "Does not resolve symbolic links — purely lexical simplification.", + ], + }, + + whenToUseIt: + "Use the stack-based path simplification whenever you need to canonicalize a Unix-style path lexically. For resolving symbolic links or checking real filesystem paths, use OS-level APIs (`realpath`). For URL normalization, combine this approach with percent-encoding and scheme/host handling.", +}; diff --git a/src/algorithms/stacks-queues/stack-applications/simplify-path/index.ts b/src/algorithms/stacks-queues/stack-applications/simplify-path/index.ts new file mode 100644 index 00000000..7364d198 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/simplify-path/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { simplifyPath } from "./sources/simplify-path.ts?fn"; +import { generateSimplifyPathSteps } from "./step-generator"; +import type { SimplifyPathInput } from "./step-generator"; +import { simplifyPathEducational } from "./educational"; + +import typescriptSource from "./sources/simplify-path.ts?raw"; +import pythonSource from "./sources/simplify-path.py?raw"; +import javaSource from "./sources/SimplifyPath.java?raw"; + +function executeSimplifyPath(input: SimplifyPathInput): string { + return simplifyPath(input.inputString) as string; +} + +const simplifyPathDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.SIMPLIFY_PATH!, + name: "Simplify Path", + category: CATEGORY.STACKS_QUEUES!, + technique: "stack-applications", + description: + "Use a stack to resolve Unix path components — skipping `.` and empty segments, popping on `..` — into a canonical absolute path", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { inputString: "/a/./b/../../c/" }, + }, + execute: executeSimplifyPath, + generateSteps: generateSimplifyPathSteps, + educational: simplifyPathEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(simplifyPathDefinition); diff --git a/src/algorithms/stacks-queues/stack-applications/simplify-path/simplify-path.test.ts b/src/algorithms/stacks-queues/stack-applications/simplify-path/simplify-path.test.ts new file mode 100644 index 00000000..9575437b --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/simplify-path/simplify-path.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from "vitest"; +import { simplifyPath } from "./sources/simplify-path.ts?fn"; + +describe("simplifyPath", () => { + it("simplifies a path with dot and double-dot components", () => { + expect(simplifyPath("/a/./b/../../c/")).toBe("/c"); + }); + + it("removes trailing slash from a simple directory", () => { + expect(simplifyPath("/home/")).toBe("/home"); + }); + + it("returns root for a path that navigates above root", () => { + expect(simplifyPath("/../")).toBe("/"); + }); + + it("collapses consecutive slashes between directories", () => { + expect(simplifyPath("/home//foo/")).toBe("/home/foo"); + }); + + it("returns root for a lone slash", () => { + expect(simplifyPath("/")).toBe("/"); + }); + + it("handles a deeply nested path without any dot components", () => { + expect(simplifyPath("/a/b/c/d")).toBe("/a/b/c/d"); + }); + + it("handles multiple consecutive double-dot components", () => { + expect(simplifyPath("/a/b/../../c/d/../e")).toBe("/c/e"); + }); + + it("treats double-dot at root as a no-op", () => { + expect(simplifyPath("/..")).toBe("/"); + }); + + it("handles a path with only dot components", () => { + expect(simplifyPath("/./././.")).toBe("/"); + }); +}); diff --git a/src/algorithms/stacks-queues/stack-applications/simplify-path/sources/SimplifyPath.java b/src/algorithms/stacks-queues/stack-applications/simplify-path/sources/SimplifyPath.java new file mode 100644 index 00000000..e3123908 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/simplify-path/sources/SimplifyPath.java @@ -0,0 +1,31 @@ +// Simplify Path — use a stack to resolve Unix path components into a canonical path +import java.util.ArrayDeque; +import java.util.Deque; + +public class SimplifyPath { + public static String simplifyPath(String inputString) { + Deque dirStack = new ArrayDeque<>(); // @step:initialize + String[] components = inputString.split("/"); // @step:initialize + for (int partIdx = 0; partIdx < components.length; partIdx++) { + String component = components[partIdx]; // @step:visit + if (component.equals("") || component.equals(".")) { + // Skip empty segments and current-directory markers + continue; // @step:visit + } else if (component.equals("..")) { + // Parent-directory marker — pop top of stack if non-empty + if (!dirStack.isEmpty()) { + dirStack.pop(); // @step:pop + } + } else { + // Valid directory name — push onto stack + dirStack.push(component); // @step:push + } + } + // Join accumulated directories with leading slash + StringBuilder result = new StringBuilder(); // @step:complete + for (String dir : dirStack) { + result.insert(0, "/" + dir); // @step:complete + } + return result.length() == 0 ? "/" : result.toString(); // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/stack-applications/simplify-path/sources/simplify-path.py b/src/algorithms/stacks-queues/stack-applications/simplify-path/sources/simplify-path.py new file mode 100644 index 00000000..813dd2c0 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/simplify-path/sources/simplify-path.py @@ -0,0 +1,17 @@ +# Simplify Path — use a stack to resolve Unix path components into a canonical path +def simplify_path(input_string): + dir_stack = [] # @step:initialize + components = input_string.split("/") # @step:initialize + for component in components: # @step:visit + if component == "" or component == ".": + # Skip empty segments and current-directory markers + continue # @step:visit + elif component == "..": + # Parent-directory marker — pop top of stack if non-empty + if dir_stack: + dir_stack.pop() # @step:pop + else: + # Valid directory name — push onto stack + dir_stack.append(component) # @step:push + # Join accumulated directories with leading slash + return "/" + "/".join(dir_stack) # @step:complete diff --git a/src/algorithms/stacks-queues/stack-applications/simplify-path/sources/simplify-path.ts b/src/algorithms/stacks-queues/stack-applications/simplify-path/sources/simplify-path.ts new file mode 100644 index 00000000..3ba090bf --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/simplify-path/sources/simplify-path.ts @@ -0,0 +1,22 @@ +// Simplify Path — use a stack to resolve Unix path components into a canonical path +function simplifyPath(inputString: string): string { + const dirStack: string[] = []; // @step:initialize + const components = inputString.split("/"); // @step:initialize + for (let partIdx = 0; partIdx < components.length; partIdx++) { + const component = components[partIdx]!; // @step:visit + if (component === "" || component === ".") { + // Skip empty segments and current-directory markers + continue; // @step:visit + } else if (component === "..") { + // Parent-directory marker — pop top of stack if non-empty + if (dirStack.length > 0) { + dirStack.pop(); // @step:pop + } + } else { + // Valid directory name — push onto stack + dirStack.push(component); // @step:push + } + } + // Join accumulated directories with leading slash + return "/" + dirStack.join("/"); // @step:complete +} diff --git a/src/algorithms/stacks-queues/stack-applications/simplify-path/step-generator.test.ts b/src/algorithms/stacks-queues/stack-applications/simplify-path/step-generator.test.ts new file mode 100644 index 00000000..8ebf1ae9 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/simplify-path/step-generator.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from "vitest"; +import { generateSimplifyPathSteps } from "./step-generator"; + +describe("generateSimplifyPathSteps", () => { + it("produces steps for the default input", () => { + const steps = generateSimplifyPathSteps({ inputString: "/a/./b/../../c/" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateSimplifyPathSteps({ inputString: "/a/./b/../../c/" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateSimplifyPathSteps({ inputString: "/a/./b/../../c/" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateSimplifyPathSteps({ inputString: "/a/./b/../../c/" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateSimplifyPathSteps({ inputString: "/a/./b/../../c/" }); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits push steps for valid directory names", () => { + const steps = generateSimplifyPathSteps({ inputString: "/a/b/c" }); + const pushSteps = steps.filter((step) => step.type === "push"); + expect(pushSteps.length).toBe(3); + }); + + it("emits match steps for double-dot pops", () => { + const steps = generateSimplifyPathSteps({ inputString: "/a/b/../c" }); + const matchSteps = steps.filter((step) => step.type === "match"); + expect(matchSteps.length).toBe(1); + }); + + it("resolves the default input to the correct simplified path", () => { + const steps = generateSimplifyPathSteps({ inputString: "/a/./b/../../c/" }); + const completeStep = steps[steps.length - 1]; + expect(completeStep?.variables?.simplifiedPath).toBe("/c"); + }); + + it("handles a root-only path", () => { + const steps = generateSimplifyPathSteps({ inputString: "/" }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + expect(steps[steps.length - 1]?.variables?.simplifiedPath).toBe("/"); + }); + + it("handles a path navigating above root", () => { + const steps = generateSimplifyPathSteps({ inputString: "/../" }); + const completeStep = steps[steps.length - 1]; + expect(completeStep?.variables?.simplifiedPath).toBe("/"); + }); +}); diff --git a/src/algorithms/stacks-queues/stack-applications/simplify-path/step-generator.ts b/src/algorithms/stacks-queues/stack-applications/simplify-path/step-generator.ts new file mode 100644 index 00000000..2432bb50 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-applications/simplify-path/step-generator.ts @@ -0,0 +1,71 @@ +/** Step generator for Simplify Path — produces ExecutionStep[] using StackQueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { StackQueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const SP_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.SIMPLIFY_PATH!); + +export interface SimplifyPathInput { + inputString: string; +} + +export function generateSimplifyPathSteps(input: SimplifyPathInput): ExecutionStep[] { + const { inputString } = input; + const tracker = new StackQueueTracker(inputString, SP_LINE_MAP); + const dirStack: string[] = []; + + tracker.initialize({ inputString }); + + const components = inputString.split("/"); + + // Track the character offset for each component start within the original string + let charOffset = 0; + + for (let partIdx = 0; partIdx < components.length; partIdx++) { + const component = components[partIdx]!; + const componentStart = charOffset; + + // Advance charOffset past this component and the following "/" separator + charOffset += component.length + 1; + + if (component === "" || component === ".") { + // Skip empty segments and current-directory markers + if (component.length > 0) { + // "." is a single visible char — mark it as visited then skip + tracker.processChar(componentStart, { partIdx, component, action: "skip" }); + } + continue; + } + + if (component === "..") { + // Mark first char of ".." as current before deciding what to do + tracker.processChar(componentStart, { partIdx, component }); + if (dirStack.length > 0) { + const popped = dirStack.pop(); + tracker.popMatched(component, componentStart, { + partIdx, + component, + popped, + stackSize: dirStack.length, + }); + } + // else: ".." at root is silently ignored — no additional step needed + } else { + // Valid directory name + tracker.processChar(componentStart, { partIdx, component }); + dirStack.push(component); + tracker.push(component, componentStart, { + partIdx, + component, + stackSize: dirStack.length, + }); + } + } + + const simplifiedPath = "/" + dirStack.join("/"); + tracker.complete(true, { simplifiedPath, stackSize: dirStack.length }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/stack-design/asteroid-collision/AsteroidCollisionPipeline.stories.tsx b/src/algorithms/stacks-queues/stack-design/asteroid-collision/AsteroidCollisionPipeline.stories.tsx new file mode 100644 index 00000000..bdf2b51c --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/asteroid-collision/AsteroidCollisionPipeline.stories.tsx @@ -0,0 +1,66 @@ +/** + * Storybook stories for the Asteroid Collision algorithm pipeline. + * Uses the real step generator with [5, 10, -5], rendering the + * StackQueueVisualizer at key simulation states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateAsteroidCollisionSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateAsteroidCollisionSteps({ asteroids: [5, 10, -5] }); +const annihilationSteps = generateAsteroidCollisionSteps({ asteroids: [8, -8] }); +const chainReactionSteps = generateAsteroidCollisionSteps({ asteroids: [1, 2, 3, -10] }); + +const meta: Meta = { + title: "Algorithm Pipelines/Asteroid Collision", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — all asteroids unprocessed, empty stack */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-simulation with some asteroids pushed onto the stack */ +export const MidSimulation: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Completed — smaller asteroid destroyed, two survivors remain */ +export const SurvivorComplete: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Mutual annihilation — equal-sized asteroids both explode, empty result */ +export const MutualAnnihilation: Story = { + args: { + visualState: annihilationSteps[annihilationSteps.length - 1]! + .visualState as StackQueueVisualState, + }, +}; + +/** Chain reaction — large left-mover destroys all preceding right-movers */ +export const ChainReaction: Story = { + args: { + visualState: chainReactionSteps[chainReactionSteps.length - 1]! + .visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/stack-design/asteroid-collision/asteroid-collision.test.ts b/src/algorithms/stacks-queues/stack-design/asteroid-collision/asteroid-collision.test.ts new file mode 100644 index 00000000..de39117c --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/asteroid-collision/asteroid-collision.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from "vitest"; +import { asteroidCollision } from "./sources/asteroid-collision.ts?fn"; + +describe("asteroidCollision", () => { + it("the smaller asteroid is destroyed when colliding with a larger one", () => { + expect(asteroidCollision([5, 10, -5])).toEqual([5, 10]); + }); + + it("both asteroids explode when they are equal in size", () => { + expect(asteroidCollision([8, -8])).toEqual([]); + }); + + it("the larger right-mover survives when the left-mover is smaller", () => { + expect(asteroidCollision([10, 2, -5])).toEqual([10]); + }); + + it("no collisions occur when all asteroids move in the same direction", () => { + expect(asteroidCollision([-2, -1, 1, 2])).toEqual([-2, -1, 1, 2]); + }); + + it("all asteroids are destroyed in a chain of equal collisions", () => { + expect(asteroidCollision([1, -1, 1, -1])).toEqual([]); + }); + + it("a large left-mover destroys all preceding right-movers", () => { + expect(asteroidCollision([1, 2, 3, -10])).toEqual([-10]); + }); + + it("two left-movers with no right-movers ahead pass through unchanged", () => { + expect(asteroidCollision([-5, -3])).toEqual([-5, -3]); + }); + + it("a single asteroid is returned unchanged", () => { + expect(asteroidCollision([7])).toEqual([7]); + }); + + it("an empty array returns an empty array", () => { + expect(asteroidCollision([])).toEqual([]); + }); + + it("handles a chain reaction where multiple right-movers are destroyed one by one", () => { + expect(asteroidCollision([5, 3, 1, -4])).toEqual([5]); + }); +}); diff --git a/src/algorithms/stacks-queues/stack-design/asteroid-collision/educational.ts b/src/algorithms/stacks-queues/stack-design/asteroid-collision/educational.ts new file mode 100644 index 00000000..0cc53195 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/asteroid-collision/educational.ts @@ -0,0 +1,66 @@ +import type { EducationalContent } from "@/types"; + +export const asteroidCollisionEducational: EducationalContent = { + overview: + "**Asteroid Collision** simulates a row of asteroids moving through space. Positive integers move right, negative integers move left. When a right-moving asteroid meets a left-moving one, they collide: the smaller explodes, equal-sized asteroids both explode, and the larger survives. Two asteroids moving in the same direction never collide.\n\nA stack is the natural structure: right-moving asteroids pile up waiting for a potential left-mover, and each new left-mover repeatedly challenges the stack top until it survives or is itself destroyed.", + + howItWorks: + "The algorithm scans left to right and maintains a stack of surviving asteroids:\n\n" + + "1. **Right-moving asteroid** (positive) → always push onto the stack (no collision possible yet).\n" + + "2. **Left-moving asteroid** (negative) → enter a collision loop:\n" + + " - If the stack is empty or the top is also left-moving → no collision, push the asteroid.\n" + + " - If `stack.top < |current|` → stack top explodes, pop and keep looping.\n" + + " - If `stack.top == |current|` → both explode, pop and mark current as dead.\n" + + " - If `stack.top > |current|` → current explodes, mark as dead and stop looping.\n" + + "3. If the asteroid survived the loop, push it onto the stack.\n" + + "4. Return the stack contents as the final array.\n\n" + + "### Example trace on `[5, 10, -5]`\n\n" + + "```\n" + + "asteroid action stack\n" + + "5 push (moving right) [5]\n" + + "10 push (moving right) [5, 10]\n" + + "-5 compare: 10 > |-5| → -5 dies [5, 10]\n" + + "end result [5, 10]\n" + + "```\n\n" + + "### Example trace on `[8, -8]`\n\n" + + "```\n" + + "asteroid action stack\n" + + "8 push (moving right) [8]\n" + + "-8 compare: 8 == |-8| → both die []\n" + + "end result []\n" + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each asteroid is pushed onto the stack at most once and popped at most once. Even though there is a nested collision loop, the total number of pop operations across the entire algorithm cannot exceed the total number of push operations, which is at most `n`. The amortised cost per asteroid is therefore `O(1)`.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "In the worst case (all asteroids moving in the same direction), every asteroid is pushed onto the stack without any collisions, using `O(n)` extra space.", + + bestAndWorstCase: + "**Best case** — all asteroids move in the same direction: no collisions occur, every asteroid is pushed once, giving `O(n)` time and `O(n)` space with the minimum constant factor.\n\n" + + "**Worst case** — a sequence of right-moving asteroids followed by a single large left-mover that destroys all of them: the inner loop runs `n - 1` times for the last asteroid, but the total work across the entire pass is still `O(n)` due to amortisation.\n\n" + + "The algorithm is `O(n)` in all cases.", + + realWorldUses: [ + "**Physics simulations:** Modelling one-dimensional particle or object collisions where direction and mass determine the outcome.", + "**Collision detection in games:** Side-scrolling game engines use similar stack-based logic to resolve projectile impacts in order.", + "**Event stream processing:** Merging opposing streams of events where later events can cancel earlier ones (e.g., undo/redo stacks).", + "**Text editor backspace processing:** The related 'backspace string compare' problem uses an identical stack pattern to process delete characters.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(n) amortised time — each asteroid is processed at most twice (one push, one pop).", + "In-place stack simulation with no extra data structures beyond the output stack.", + "Handles all edge cases (same direction, equal mass, chain reactions) in a single unified loop.", + ], + limitations: [ + "O(n) extra space for the stack — cannot be done truly in-place without a more complex approach.", + "Only models one-dimensional collisions; two-dimensional physics requires entirely different techniques.", + "Assumes asteroids move at the same speed — variable speeds would require a different comparison model.", + ], + }, + + whenToUseIt: + "Use a stack whenever you need to repeatedly compare an incoming element against the most recently accepted element and potentially cancel both. The asteroid collision pattern generalises to any 'opposing forces' problem where each new item challenges the previous survivor. If the problem involves only one direction of challenge (e.g., 'next greater element'), a monotonic stack suffices; the two-way collision pattern here is specifically for mutual destruction scenarios.", +}; diff --git a/src/algorithms/stacks-queues/stack-design/asteroid-collision/index.ts b/src/algorithms/stacks-queues/stack-design/asteroid-collision/index.ts new file mode 100644 index 00000000..08e84b54 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/asteroid-collision/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { asteroidCollision } from "./sources/asteroid-collision.ts?fn"; +import { generateAsteroidCollisionSteps } from "./step-generator"; +import type { AsteroidCollisionInput } from "./step-generator"; +import { asteroidCollisionEducational } from "./educational"; + +import typescriptSource from "./sources/asteroid-collision.ts?raw"; +import pythonSource from "./sources/asteroid-collision.py?raw"; +import javaSource from "./sources/AsteroidCollision.java?raw"; + +function executeAsteroidCollision(input: AsteroidCollisionInput): number[] { + return asteroidCollision(input.asteroids) as number[]; +} + +const asteroidCollisionDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.ASTEROID_COLLISION!, + name: "Asteroid Collision", + category: CATEGORY.STACKS_QUEUES!, + technique: "stack-design", + description: + "Use a stack to simulate asteroids moving in opposite directions — positive move right, negative move left — resolving collisions by size until all remaining asteroids move without conflict", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { asteroids: [5, 10, -5] }, + }, + execute: executeAsteroidCollision, + generateSteps: generateAsteroidCollisionSteps, + educational: asteroidCollisionEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(asteroidCollisionDefinition); diff --git a/src/algorithms/stacks-queues/stack-design/asteroid-collision/sources/AsteroidCollision.java b/src/algorithms/stacks-queues/stack-design/asteroid-collision/sources/AsteroidCollision.java new file mode 100644 index 00000000..67cf36c4 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/asteroid-collision/sources/AsteroidCollision.java @@ -0,0 +1,37 @@ +// Asteroid Collision — use a stack to simulate asteroid collisions, resolving each pair by size +import java.util.ArrayDeque; +import java.util.Deque; + +public class AsteroidCollision { + public static int[] asteroidCollision(int[] asteroids) { + Deque stack = new ArrayDeque<>(); // @step:initialize + for (int asteroidIdx = 0; asteroidIdx < asteroids.length; asteroidIdx++) { + int currentAsteroid = asteroids[asteroidIdx]; // @step:visit + boolean alive = true; // @step:visit + // Collision occurs when the current asteroid moves left and the stack top moves right + while (alive && currentAsteroid < 0 && !stack.isEmpty() && stack.peek() > 0) { + int stackTop = stack.peek(); // @step:compare + if (stackTop < -currentAsteroid) { + // Stack top is smaller — it gets destroyed + stack.pop(); // @step:pop + } else if (stackTop == -currentAsteroid) { + // Equal size — both are destroyed + stack.pop(); // @step:match + alive = false; // @step:match + } else { + // Stack top is larger — current asteroid is destroyed + alive = false; // @step:mismatch + } + } + if (alive) { + stack.push(currentAsteroid); // @step:push + } + } + // Build result array from stack (reversed since Deque is LIFO) + int[] result = new int[stack.size()]; // @step:complete + for (int resultIdx = result.length - 1; resultIdx >= 0; resultIdx--) { + result[resultIdx] = stack.pop(); // @step:complete + } + return result; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/stack-design/asteroid-collision/sources/asteroid-collision.py b/src/algorithms/stacks-queues/stack-design/asteroid-collision/sources/asteroid-collision.py new file mode 100644 index 00000000..ba2e048e --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/asteroid-collision/sources/asteroid-collision.py @@ -0,0 +1,21 @@ +# Asteroid Collision — use a stack to simulate asteroid collisions, resolving each pair by size +def asteroid_collision(asteroids): + stack = [] # @step:initialize + for current_asteroid in asteroids: # @step:visit + alive = True # @step:visit + # Collision occurs when the current asteroid moves left and the stack top moves right + while alive and current_asteroid < 0 and stack and stack[-1] > 0: + stack_top = stack[-1] # @step:compare + if stack_top < -current_asteroid: + # Stack top is smaller — it gets destroyed + stack.pop() # @step:pop + elif stack_top == -current_asteroid: + # Equal size — both are destroyed + stack.pop() # @step:match + alive = False # @step:match + else: + # Stack top is larger — current asteroid is destroyed + alive = False # @step:mismatch + if alive: + stack.append(current_asteroid) # @step:push + return stack # @step:complete diff --git a/src/algorithms/stacks-queues/stack-design/asteroid-collision/sources/asteroid-collision.ts b/src/algorithms/stacks-queues/stack-design/asteroid-collision/sources/asteroid-collision.ts new file mode 100644 index 00000000..f47a4fcc --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/asteroid-collision/sources/asteroid-collision.ts @@ -0,0 +1,27 @@ +// Asteroid Collision — use a stack to simulate asteroid collisions, resolving each pair by size +function asteroidCollision(asteroids: number[]): number[] { + const stack: number[] = []; // @step:initialize + for (let asteroidIdx = 0; asteroidIdx < asteroids.length; asteroidIdx++) { + const currentAsteroid = asteroids[asteroidIdx]!; // @step:visit + let alive = true; // @step:visit + // Collision occurs when the current asteroid moves left and the stack top moves right + while (alive && currentAsteroid < 0 && stack.length > 0 && stack[stack.length - 1]! > 0) { + const stackTop = stack[stack.length - 1]!; // @step:compare + if (stackTop < -currentAsteroid) { + // Stack top is smaller — it gets destroyed + stack.pop(); // @step:pop + } else if (stackTop === -currentAsteroid) { + // Equal size — both are destroyed + stack.pop(); // @step:match + alive = false; // @step:match + } else { + // Stack top is larger — current asteroid is destroyed + alive = false; // @step:mismatch + } + } + if (alive) { + stack.push(currentAsteroid); // @step:push + } + } + return stack; // @step:complete +} diff --git a/src/algorithms/stacks-queues/stack-design/asteroid-collision/step-generator.test.ts b/src/algorithms/stacks-queues/stack-design/asteroid-collision/step-generator.test.ts new file mode 100644 index 00000000..58bec6bf --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/asteroid-collision/step-generator.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect } from "vitest"; +import { generateAsteroidCollisionSteps } from "./step-generator"; + +describe("generateAsteroidCollisionSteps", () => { + it("produces steps for the default input", () => { + const steps = generateAsteroidCollisionSteps({ asteroids: [5, 10, -5] }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateAsteroidCollisionSteps({ asteroids: [5, 10, -5] }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateAsteroidCollisionSteps({ asteroids: [5, 10, -5] }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateAsteroidCollisionSteps({ asteroids: [5, 10, -5] }); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateAsteroidCollisionSteps({ asteroids: [5, 10, -5] }); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits push steps for surviving asteroids", () => { + // [5, 10, -5]: 5 and 10 survive, -5 is destroyed — two pushes + const steps = generateAsteroidCollisionSteps({ asteroids: [5, 10, -5] }); + const pushSteps = steps.filter((step) => step.type === "push"); + expect(pushSteps.length).toBe(2); + }); + + it("emits a resolve step when two equal asteroids destroy each other", () => { + // [8, -8]: both explode — one resolve step + const steps = generateAsteroidCollisionSteps({ asteroids: [8, -8] }); + const resolveSteps = steps.filter((step) => step.type === "resolve"); + expect(resolveSteps.length).toBe(1); + }); + + it("emits maintain-monotonic steps when the stack top is destroyed in a collision", () => { + // [10, 2, -5]: -5 destroys 2 (stack top smaller) via maintainMonotonic + const steps = generateAsteroidCollisionSteps({ asteroids: [10, 2, -5] }); + const maintainSteps = steps.filter((step) => step.type === "maintain-monotonic"); + expect(maintainSteps.length).toBeGreaterThan(0); + }); + + it("produces no collision steps when all asteroids move in the same direction", () => { + const steps = generateAsteroidCollisionSteps({ asteroids: [-2, -1, 1, 2] }); + const maintainSteps = steps.filter((step) => step.type === "maintain-monotonic"); + const resolveSteps = steps.filter((step) => step.type === "resolve"); + expect(maintainSteps.length).toBe(0); + expect(resolveSteps.length).toBe(0); + }); + + it("handles an empty asteroid array", () => { + const steps = generateAsteroidCollisionSteps({ asteroids: [] }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("final visual state stack is empty when all asteroids cancel out", () => { + const steps = generateAsteroidCollisionSteps({ asteroids: [8, -8] }); + const lastStep = steps[steps.length - 1]!; + expect(lastStep.visualState.kind).toBe("stack-queue"); + if (lastStep.visualState.kind === "stack-queue") { + expect(lastStep.visualState.stackElements.length).toBe(0); + } + }); +}); diff --git a/src/algorithms/stacks-queues/stack-design/asteroid-collision/step-generator.ts b/src/algorithms/stacks-queues/stack-design/asteroid-collision/step-generator.ts new file mode 100644 index 00000000..f95222e9 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/asteroid-collision/step-generator.ts @@ -0,0 +1,86 @@ +/** Step generator for Asteroid Collision — produces ExecutionStep[] using NumericStackTracker. */ + +import type { ExecutionStep } from "@/types"; +import { NumericStackTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const AC_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.ASTEROID_COLLISION!); + +export interface AsteroidCollisionInput { + asteroids: number[]; +} + +export function generateAsteroidCollisionSteps(input: AsteroidCollisionInput): ExecutionStep[] { + const { asteroids } = input; + const tracker = new NumericStackTracker(asteroids, AC_LINE_MAP); + + // Logical stack mirrors the tracker stack for collision decisions + const logicalStack: number[] = []; + + tracker.initialize({ asteroids, stackSize: 0 }); + + for (let asteroidIdx = 0; asteroidIdx < asteroids.length; asteroidIdx++) { + const currentAsteroid = asteroids[asteroidIdx]!; + + tracker.processElement(asteroidIdx, { asteroidIdx, currentAsteroid }); + + let alive = true; + + // Process collisions while current moves left and stack top moves right + while (alive && currentAsteroid < 0 && logicalStack.length > 0) { + const stackTop = logicalStack[logicalStack.length - 1]!; + + if (stackTop <= 0) { + // No collision — both moving in the same direction + break; + } + + tracker.compare( + { asteroidIdx, currentAsteroid, stackTop }, + `Compare: stack top ${stackTop} vs incoming ${currentAsteroid}`, + ); + + if (stackTop < -currentAsteroid) { + // Stack top is smaller — destroy it and keep checking + logicalStack.pop(); + tracker.maintainMonotonic( + { asteroidIdx, currentAsteroid, destroyedStackTop: stackTop }, + `Stack top ${stackTop} is smaller than |${currentAsteroid}| — stack top explodes`, + ); + } else if (stackTop === -currentAsteroid) { + // Equal size — both explode + logicalStack.pop(); + tracker.popAndResolve( + null, + { asteroidIdx, currentAsteroid, stackTop }, + `Both ${stackTop} and ${currentAsteroid} are equal size — both explode`, + ); + alive = false; + } else { + // Stack top is larger — current asteroid is destroyed + alive = false; + tracker.compare( + { asteroidIdx, currentAsteroid, stackTop, outcome: "destroyed" }, + `Stack top ${stackTop} is larger than |${currentAsteroid}| — incoming asteroid explodes`, + ); + } + } + + if (alive) { + logicalStack.push(currentAsteroid); + tracker.pushIndex( + asteroidIdx, + { asteroidIdx, currentAsteroid, stackSize: logicalStack.length }, + `Asteroid ${currentAsteroid} survives — push onto stack`, + ); + } + } + + tracker.complete( + { survivingAsteroids: [...logicalStack], count: logicalStack.length }, + `Simulation complete — ${logicalStack.length} asteroid(s) survive: [${logicalStack.join(", ")}]`, + ); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/stack-design/decode-string/DecodeStringPipeline.stories.tsx b/src/algorithms/stacks-queues/stack-design/decode-string/DecodeStringPipeline.stories.tsx new file mode 100644 index 00000000..ffb4abe8 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/decode-string/DecodeStringPipeline.stories.tsx @@ -0,0 +1,56 @@ +/** + * Storybook stories for the Decode String algorithm pipeline. + * Uses the real step generator with "3[a2[c]]", rendering the + * StackQueueVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateDecodeStringSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateDecodeStringSteps({ inputString: "3[a2[c]]" }); +const multiGroupSteps = generateDecodeStringSteps({ inputString: "2[abc]3[cd]ef" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Decode String", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — full input unprocessed, empty stack */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing with nested context pushed onto the stack */ +export const MidProcessing: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Completed decode — all brackets unwound, final string assembled */ +export const DecodedComplete: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Multiple top-level groups with trailing letters */ +export const MultiGroupDecode: Story = { + args: { + visualState: multiGroupSteps[multiGroupSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/stack-design/decode-string/decode-string.test.ts b/src/algorithms/stacks-queues/stack-design/decode-string/decode-string.test.ts new file mode 100644 index 00000000..f61e24c9 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/decode-string/decode-string.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from "vitest"; +import { decodeString } from "./sources/decode-string.ts?fn"; + +describe("decodeString", () => { + it("decodes a simple single-level repetition", () => { + expect(decodeString("3[a]")).toBe("aaa"); + }); + + it("decodes nested brackets with inner repetition", () => { + expect(decodeString("3[a2[c]]")).toBe("accaccacc"); + }); + + it("decodes multiple top-level groups with trailing letters", () => { + expect(decodeString("2[abc]3[cd]ef")).toBe("abcabccdcdcdef"); + }); + + it("returns a plain string unchanged when no encoding is present", () => { + expect(decodeString("abc")).toBe("abc"); + }); + + it("handles a single character repeated many times", () => { + expect(decodeString("5[z]")).toBe("zzzzz"); + }); + + it("handles deeply nested brackets", () => { + expect(decodeString("2[2[a]]")).toBe("aaaa"); + }); + + it("returns an empty string for empty input", () => { + expect(decodeString("")).toBe(""); + }); + + it("handles multi-digit repeat counts", () => { + expect(decodeString("10[a]")).toBe("aaaaaaaaaa"); + }); + + it("handles letters before and after bracket groups", () => { + expect(decodeString("a2[b]c")).toBe("abbc"); + }); +}); diff --git a/src/algorithms/stacks-queues/stack-design/decode-string/educational.ts b/src/algorithms/stacks-queues/stack-design/decode-string/educational.ts new file mode 100644 index 00000000..87ef5f25 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/decode-string/educational.ts @@ -0,0 +1,68 @@ +import type { EducationalContent } from "@/types"; + +export const decodeStringEducational: EducationalContent = { + overview: + "**Decode String** expands a run-length encoded string where numbers prefix bracketed segments. " + + "For example, `3[a2[c]]` decodes to `accaccacc` — the inner `2[c]` becomes `cc`, and the outer `3[...]` repeats `acc` three times.\n\n" + + "A stack lets the algorithm remember the context (partial string and repeat count) each time it descends into a nested bracket, " + + "then restore that context when the matching `]` is reached.", + + howItWorks: + "The algorithm scans the encoded string left to right, maintaining a current string accumulator and a current repeat count:\n\n" + + "1. **Digit** → accumulate into `currentCount` (supports multi-digit numbers like `12[a]`).\n" + + "2. **`[`** → push `currentCount` and `currentString` onto their respective stacks, then reset both to handle the nested segment.\n" + + "3. **`]`** → pop the saved count and string, set `currentString = prevString + currentString.repeat(count)`.\n" + + "4. **Letter** → append to `currentString`.\n\n" + + "### Example trace on `3[a2[c]]`\n\n" + + "```\n" + + "char action currentString currentCount\n" + + '3 digit "" 3\n' + + '[ push(3, ""), reset "" 0\n' + + 'a append "a" 0\n' + + '2 digit "a" 2\n' + + '[ push(2, "a"), reset "" 0\n' + + 'c append "c" 0\n' + + '] pop(2, "a") → "a" + "cc" "acc" —\n' + + '] pop(3, "") → "" + "accacc" "accaccacc" —\n' + + 'end return "accaccacc"\n' + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n × k)`** where `n` is the length of the encoded string and `k` is the maximum total expansion factor.\n\n" + + "Each decoded character may be written multiple times due to repetition. In the worst case (deeply nested, high repeat counts) " + + "the output can be exponentially larger than the input.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The two stacks each store at most `d` entries where `d` is the nesting depth, bounded by `n`. " + + "The decoded string itself can be `O(n × k)` in size.", + + bestAndWorstCase: + "**Best case** — no brackets or digits (plain string like `abc`): single linear scan with no stack operations, `O(n)` time.\n\n" + + "**Worst case** — deeply nested, high-multiplier input like `10[10[10[a]]]`: the output size grows as `k^d` " + + "where `d` is nesting depth, making construction `O(n × k)` time.\n\n" + + "The algorithm always completes in a single left-to-right pass over the encoded input.", + + realWorldUses: [ + "**Data compression:** Run-length encoding is a foundational compression scheme used in fax machines, BMP images, and PCX graphics.", + "**Network protocols:** Compressed representations of repetitive data segments in messaging protocols and serialization formats.", + "**Bioinformatics:** Compact encoding of repetitive DNA/RNA sequences where motifs repeat many times.", + "**Template engines:** Expanding repetitive template constructs in code generation and configuration file processing.", + ], + + strengthsAndLimitations: { + strengths: [ + "Single linear pass over the encoded input regardless of nesting depth.", + "Handles arbitrarily deep nesting and multi-digit repeat counts naturally.", + "Simple stack discipline — push on `[`, pop on `]` — mirrors the recursive structure of the grammar.", + ], + limitations: [ + "Output string can be exponentially larger than the input, consuming significant memory.", + "Does not handle malformed input gracefully (unmatched brackets cause undefined behavior without validation).", + "Not suitable for streaming decoding when the expanded output is too large to hold in memory.", + ], + }, + + whenToUseIt: + "Use the stack-based decode approach whenever the encoded format uses matched delimiter pairs with preceding repeat counts. " + + "If nesting is not possible (flat run-length encoding), a simpler two-pointer scan suffices. " + + "For very large expansions that cannot fit in memory, consider a lazy iterator or streaming decoder instead.", +}; diff --git a/src/algorithms/stacks-queues/stack-design/decode-string/index.ts b/src/algorithms/stacks-queues/stack-design/decode-string/index.ts new file mode 100644 index 00000000..251da0f2 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/decode-string/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { decodeString } from "./sources/decode-string.ts?fn"; +import { generateDecodeStringSteps } from "./step-generator"; +import type { DecodeStringInput } from "./step-generator"; +import { decodeStringEducational } from "./educational"; + +import typescriptSource from "./sources/decode-string.ts?raw"; +import pythonSource from "./sources/decode-string.py?raw"; +import javaSource from "./sources/DecodeString.java?raw"; + +function executeDecodeString(input: DecodeStringInput): string { + return decodeString(input.inputString) as string; +} + +const decodeStringDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.DECODE_STRING!, + name: "Decode String", + category: CATEGORY.STACKS_QUEUES!, + technique: "stack-design", + description: + "Use a stack to decode run-length encoded strings like '3[a2[c]]' → 'accaccacc', handling arbitrary nesting depth", + timeComplexity: { + best: "O(n)", + average: "O(n*k)", + worst: "O(n*k)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { inputString: "3[a2[c]]" }, + }, + execute: executeDecodeString, + generateSteps: generateDecodeStringSteps, + educational: decodeStringEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(decodeStringDefinition); diff --git a/src/algorithms/stacks-queues/stack-design/decode-string/sources/DecodeString.java b/src/algorithms/stacks-queues/stack-design/decode-string/sources/DecodeString.java new file mode 100644 index 00000000..8a941d5c --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/decode-string/sources/DecodeString.java @@ -0,0 +1,38 @@ +// Decode String — use a stack to decode encoded strings like "3[a2[c]]" → "accaccacc" +import java.util.ArrayDeque; +import java.util.Deque; + +public class DecodeString { + public static String decodeString(String inputString) { + Deque countStack = new ArrayDeque<>(); // @step:initialize + Deque stringStack = new ArrayDeque<>(); // @step:initialize + StringBuilder currentString = new StringBuilder(); // @step:initialize + int currentCount = 0; // @step:initialize + + for (int charIdx = 0; charIdx < inputString.length(); charIdx++) { + char currentChar = inputString.charAt(charIdx); // @step:visit + + if (Character.isDigit(currentChar)) { + // Build up multi-digit multipliers + currentCount = currentCount * 10 + (currentChar - '0'); // @step:visit + } else if (currentChar == '[') { + // Push current context onto stacks and reset for nested segment + countStack.push(currentCount); // @step:push + stringStack.push(currentString.toString()); // @step:push + currentCount = 0; // @step:push + currentString = new StringBuilder(); // @step:push + } else if (currentChar == ']') { + // Pop context and expand the repeated segment + int repeatCount = countStack.pop(); // @step:pop + String prevString = stringStack.pop(); // @step:pop + String repeated = currentString.toString().repeat(repeatCount); // @step:pop + currentString = new StringBuilder(prevString + repeated); // @step:pop + } else { + // Regular character — append to current string accumulator + currentString.append(currentChar); // @step:visit + } + } + + return currentString.toString(); // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/stack-design/decode-string/sources/decode-string.py b/src/algorithms/stacks-queues/stack-design/decode-string/sources/decode-string.py new file mode 100644 index 00000000..c6e1becd --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/decode-string/sources/decode-string.py @@ -0,0 +1,27 @@ +# Decode String — use a stack to decode encoded strings like "3[a2[c]]" → "accaccacc" +def decode_string(input_string): + count_stack = [] # @step:initialize + string_stack = [] # @step:initialize + current_string = "" # @step:initialize + current_count = 0 # @step:initialize + + for current_char in input_string: # @step:visit + if current_char.isdigit(): + # Build up multi-digit multipliers + current_count = current_count * 10 + int(current_char) # @step:visit + elif current_char == "[": + # Push current context onto stacks and reset for nested segment + count_stack.append(current_count) # @step:push + string_stack.append(current_string) # @step:push + current_count = 0 # @step:push + current_string = "" # @step:push + elif current_char == "]": + # Pop context and expand the repeated segment + repeat_count = count_stack.pop() # @step:pop + prev_string = string_stack.pop() # @step:pop + current_string = prev_string + current_string * repeat_count # @step:pop + else: + # Regular character — append to current string accumulator + current_string += current_char # @step:visit + + return current_string # @step:complete diff --git a/src/algorithms/stacks-queues/stack-design/decode-string/sources/decode-string.ts b/src/algorithms/stacks-queues/stack-design/decode-string/sources/decode-string.ts new file mode 100644 index 00000000..cbb254fb --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/decode-string/sources/decode-string.ts @@ -0,0 +1,32 @@ +// Decode String — use a stack to decode encoded strings like "3[a2[c]]" → "accaccacc" +function decodeString(inputString: string): string { + const countStack: number[] = []; // @step:initialize + const stringStack: string[] = []; // @step:initialize + let currentString = ""; // @step:initialize + let currentCount = 0; // @step:initialize + + for (let charIdx = 0; charIdx < inputString.length; charIdx++) { + const currentChar = inputString[charIdx]!; // @step:visit + + if (currentChar >= "0" && currentChar <= "9") { + // Build up multi-digit multipliers + currentCount = currentCount * 10 + parseInt(currentChar, 10); // @step:visit + } else if (currentChar === "[") { + // Push current context onto stacks and reset for nested segment + countStack.push(currentCount); // @step:push + stringStack.push(currentString); // @step:push + currentCount = 0; // @step:push + currentString = ""; // @step:push + } else if (currentChar === "]") { + // Pop context and expand the repeated segment + const repeatCount = countStack.pop()!; // @step:pop + const prevString = stringStack.pop()!; // @step:pop + currentString = prevString + currentString.repeat(repeatCount); // @step:pop + } else { + // Regular character — append to current string accumulator + currentString += currentChar; // @step:visit + } + } + + return currentString; // @step:complete +} diff --git a/src/algorithms/stacks-queues/stack-design/decode-string/step-generator.test.ts b/src/algorithms/stacks-queues/stack-design/decode-string/step-generator.test.ts new file mode 100644 index 00000000..fd672947 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/decode-string/step-generator.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect } from "vitest"; +import { generateDecodeStringSteps } from "./step-generator"; + +describe("generateDecodeStringSteps", () => { + it("produces steps for the default input", () => { + const steps = generateDecodeStringSteps({ inputString: "3[a2[c]]" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateDecodeStringSteps({ inputString: "3[a2[c]]" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateDecodeStringSteps({ inputString: "3[a2[c]]" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateDecodeStringSteps({ inputString: "3[a2[c]]" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateDecodeStringSteps({ inputString: "3[a2[c]]" }); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits push steps for each opening bracket", () => { + const steps = generateDecodeStringSteps({ inputString: "3[a2[c]]" }); + const pushSteps = steps.filter((step) => step.type === "push"); + // Two '[' characters in "3[a2[c]]" + expect(pushSteps.length).toBe(2); + }); + + it("emits match steps for each closing bracket", () => { + const steps = generateDecodeStringSteps({ inputString: "3[a2[c]]" }); + const matchSteps = steps.filter((step) => step.type === "match"); + // Two ']' characters in "3[a2[c]]" + expect(matchSteps.length).toBe(2); + }); + + it("encodes the decoded result in the complete step variables", () => { + const steps = generateDecodeStringSteps({ inputString: "3[a2[c]]" }); + const completeStep = steps[steps.length - 1]; + expect(completeStep?.variables).toMatchObject({ decodedResult: "accaccacc" }); + }); + + it("handles a plain string with no brackets", () => { + const steps = generateDecodeStringSteps({ inputString: "abc" }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + const completeStep = steps[steps.length - 1]; + expect(completeStep?.variables).toMatchObject({ decodedResult: "abc" }); + }); + + it("handles an empty string", () => { + const steps = generateDecodeStringSteps({ inputString: "" }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("handles multiple top-level groups", () => { + const steps = generateDecodeStringSteps({ inputString: "2[abc]3[cd]ef" }); + const completeStep = steps[steps.length - 1]; + expect(completeStep?.variables).toMatchObject({ decodedResult: "abcabccdcdcdef" }); + }); +}); diff --git a/src/algorithms/stacks-queues/stack-design/decode-string/step-generator.ts b/src/algorithms/stacks-queues/stack-design/decode-string/step-generator.ts new file mode 100644 index 00000000..7967b9e2 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/decode-string/step-generator.ts @@ -0,0 +1,68 @@ +/** Step generator for Decode String — produces ExecutionStep[] using StackQueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { StackQueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const DS_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.DECODE_STRING!); + +export interface DecodeStringInput { + inputString: string; +} + +export function generateDecodeStringSteps(input: DecodeStringInput): ExecutionStep[] { + const { inputString } = input; + const tracker = new StackQueueTracker(inputString, DS_LINE_MAP); + + const countStack: number[] = []; + const stringStack: string[] = []; + let currentString = ""; + let currentCount = 0; + + tracker.initialize({ inputString, currentString, currentCount }); + + for (let charIdx = 0; charIdx < inputString.length; charIdx++) { + const currentChar = inputString[charIdx]!; + + tracker.processChar(charIdx, { charIdx, currentChar, currentCount, currentString }); + + if (currentChar >= "0" && currentChar <= "9") { + currentCount = currentCount * 10 + parseInt(currentChar, 10); + // No extra step needed — processChar already captured the digit visit + } else if (currentChar === "[") { + countStack.push(currentCount); + stringStack.push(currentString); + currentCount = 0; + currentString = ""; + tracker.push(currentChar, charIdx, { + charIdx, + currentChar, + countStackDepth: countStack.length, + stringStackDepth: stringStack.length, + currentCount, + currentString, + }); + } else if (currentChar === "]") { + const repeatCount = countStack.pop()!; + const prevString = stringStack.pop()!; + currentString = prevString + currentString.repeat(repeatCount); + tracker.popMatched(currentChar, charIdx, { + charIdx, + currentChar, + repeatCount, + prevString, + currentString, + countStackDepth: countStack.length, + stringStackDepth: stringStack.length, + }); + } else { + currentString += currentChar; + // Regular character appended to accumulator — already captured by processChar + } + } + + tracker.complete(true, { decodedResult: currentString }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/stack-design/max-frequency-stack/MaxFrequencyStackPipeline.stories.tsx b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/MaxFrequencyStackPipeline.stories.tsx new file mode 100644 index 00000000..9cb935a9 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/MaxFrequencyStackPipeline.stories.tsx @@ -0,0 +1,56 @@ +/** + * Storybook stories for the Max Frequency Stack algorithm pipeline. + * Uses the real step generator with [5, 7, 5, 7, 4, 5], rendering the + * StackQueueVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateMaxFrequencyStackSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateMaxFrequencyStackSteps({ values: [5, 7, 5, 7, 4, 5] }); +const uniformSteps = generateMaxFrequencyStackSteps({ values: [3, 3, 3] }); + +const meta: Meta = { + title: "Algorithm Pipelines/Max Frequency Stack", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — empty frequency structures before any elements are pushed */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing — frequency stacks partially filled during push phase */ +export const MidProcessing: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Complete — all elements popped in most-frequent-first order */ +export const Complete: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Uniform input — single element repeated, all pops from a single frequency level */ +export const UniformInput: Story = { + args: { + visualState: uniformSteps[uniformSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/stack-design/max-frequency-stack/educational.ts b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/educational.ts new file mode 100644 index 00000000..7e4372a2 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/educational.ts @@ -0,0 +1,66 @@ +import type { EducationalContent } from "@/types"; + +export const maxFrequencyStackEducational: EducationalContent = { + overview: + "**Max Frequency Stack** (LeetCode 895) is a stack-like data structure that always pops the most frequently pushed element. When multiple elements share the same maximum frequency, the one pushed most recently is returned — mimicking normal stack ordering as a tiebreaker.\n\nThe key insight is to maintain a **frequency map** (element → count) alongside a **stack-of-stacks grouped by frequency**. Each frequency level gets its own stack, and a single `maxFrequency` integer tracks the current peak.", + + howItWorks: + "The algorithm works in two phases for the visualization:\n\n" + + "**Push phase** — for each incoming value:\n" + + "1. Increment the element's count in `freqMap`.\n" + + "2. If the new count exceeds `maxFrequency`, update `maxFrequency`.\n" + + "3. Append the value to `freqStacks[newFreq]` — the stack at that frequency level.\n\n" + + "**Pop phase** — to pop the most frequent element:\n" + + "1. Remove the top element from `freqStacks[maxFrequency]`.\n" + + "2. Decrement `freqMap[popped]`.\n" + + "3. If `freqStacks[maxFrequency]` is now empty, decrement `maxFrequency`.\n\n" + + "### Example trace on `[5, 7, 5, 7, 4, 5]`\n\n" + + "```\n" + + "Push 5 → freq[5]=1, maxFreq=1, freqStacks={1:[5]}\n" + + "Push 7 → freq[7]=1, maxFreq=1, freqStacks={1:[5,7]}\n" + + "Push 5 → freq[5]=2, maxFreq=2, freqStacks={1:[5,7],2:[5]}\n" + + "Push 7 → freq[7]=2, maxFreq=2, freqStacks={1:[5,7],2:[5,7]}\n" + + "Push 4 → freq[4]=1, maxFreq=2, freqStacks={1:[5,7,4],2:[5,7]}\n" + + "Push 5 → freq[5]=3, maxFreq=3, freqStacks={1:[5,7,4],2:[5,7],3:[5]}\n" + + "Pop → 5 (freq 3, the only element)\n" + + "Pop → 7 (freq 2, last pushed at freq 2)\n" + + "Pop → 5 (freq 2, before 7)\n" + + "Pop → 4 (freq 1, last pushed)\n" + + "Pop → 7 (freq 1)\n" + + "Pop → 5 (freq 1)\n" + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(1)` per operation**\n\n" + + "Both push and pop perform a fixed number of hash map lookups and stack push/pop operations — each `O(1)` amortized. No iteration over elements is required.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The frequency map stores one entry per unique element. The stack-of-stacks together hold exactly `n` entries in total (one slot per push). Overall space grows linearly with the number of elements pushed.", + + bestAndWorstCase: + "**Best case** — all pushed elements are distinct: every element has frequency 1, so `freqStacks` has only one level. Push and pop still run in `O(1)`, but no tiebreaking between frequency levels is needed.\n\n" + + "**Worst case** — all pushed elements are identical: a single element reaches frequency `n`, producing `n` levels in `freqStacks`. Push and pop remain `O(1)` per call, but the structure uses `O(n)` space across `n` levels.\n\n" + + "Both cases are `O(1)` per operation — the worst case only affects space, not time.", + + realWorldUses: [ + "**Cache eviction policies:** LFU (Least Frequently Used) caches evict the least frequent element instead of the most frequent, but the underlying frequency-grouped data structure is identical — just inverted.", + "**Recommendation systems:** Surfacing the most frequently interacted-with items (songs, products, articles) uses frequency tracking analogous to this stack's `freqMap`.", + "**Task scheduling:** Operating systems and job queues can use frequency-based ordering to prioritize or deprioritize recurring task types.", + "**Rate limiting and analytics:** Counting and ranking events by occurrence frequency in streaming data relies on the same map-of-counters pattern.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(1) push and pop — both operations are constant time regardless of the number of elements.", + "Correct tiebreaking — elements with equal frequency are popped in LIFO order, matching natural stack behavior.", + "Clean separation of concerns — freqMap and freqStacks each have a single responsibility.", + ], + limitations: [ + "O(n) space — stores every pushed element in the stack-of-stacks, plus the frequency map.", + "Frequency levels are never reclaimed once maxFrequency decreases — the freqStacks entries for old levels remain allocated until garbage collected.", + "Not a general-purpose stack — it does not support arbitrary peek or indexed access; only the most frequent element is accessible.", + ], + }, + + whenToUseIt: + "Use Max Frequency Stack when you need to retrieve elements in most-frequent-first order and want `O(1)` per operation. It is the building block for **LFU caches** (swap 'max frequency' for 'min frequency') and any system that must rank items by access count in real time. If insertion order matters more than frequency, use a plain stack or queue instead.", +}; diff --git a/src/algorithms/stacks-queues/stack-design/max-frequency-stack/index.ts b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/index.ts new file mode 100644 index 00000000..a0be39c5 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { maxFrequencyStack } from "./sources/max-frequency-stack.ts?fn"; +import { generateMaxFrequencyStackSteps } from "./step-generator"; +import type { MaxFrequencyStackInput } from "./step-generator"; +import { maxFrequencyStackEducational } from "./educational"; + +import typescriptSource from "./sources/max-frequency-stack.ts?raw"; +import pythonSource from "./sources/max-frequency-stack.py?raw"; +import javaSource from "./sources/MaxFrequencyStack.java?raw"; + +function executeMaxFrequencyStack(input: MaxFrequencyStackInput): number[] { + return maxFrequencyStack(input.values) as number[]; +} + +const maxFrequencyStackDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.MAX_FREQUENCY_STACK!, + name: "Max Frequency Stack", + category: CATEGORY.STACKS_QUEUES!, + technique: "stack-design", + description: + "Design a stack-like data structure that pops the most frequently pushed element, using a frequency map and a stack-of-stacks grouped by frequency", + timeComplexity: { + best: "O(1)", + average: "O(1)", + worst: "O(1)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { values: [5, 7, 5, 7, 4, 5] }, + }, + execute: executeMaxFrequencyStack, + generateSteps: generateMaxFrequencyStackSteps, + educational: maxFrequencyStackEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(maxFrequencyStackDefinition); diff --git a/src/algorithms/stacks-queues/stack-design/max-frequency-stack/max-frequency-stack.test.ts b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/max-frequency-stack.test.ts new file mode 100644 index 00000000..4d2c90f1 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/max-frequency-stack.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from "vitest"; +import { maxFrequencyStack } from "./sources/max-frequency-stack.ts?fn"; + +describe("maxFrequencyStack", () => { + it("pops the most frequent element first from the default input", () => { + const popOrder = maxFrequencyStack([5, 7, 5, 7, 4, 5]) as number[]; + // 5 appears 3 times (highest), then 7 appears 2 times (most recent at freq 2), + // then 5 at freq 2, then 4 at freq 1 (most recent), then 7, then 5 + expect(popOrder).toEqual([5, 7, 5, 4, 7, 5]); + }); + + it("pops in LIFO order when all elements have the same frequency", () => { + const popOrder = maxFrequencyStack([1, 2, 3]) as number[]; + // Each appears once — pop in reverse push order + expect(popOrder).toEqual([3, 2, 1]); + }); + + it("handles a single element pushed multiple times", () => { + const popOrder = maxFrequencyStack([9, 9, 9]) as number[]; + expect(popOrder).toEqual([9, 9, 9]); + }); + + it("handles two elements alternated — most recent at max freq wins tiebreak", () => { + const popOrder = maxFrequencyStack([1, 2, 1, 2]) as number[]; + // Both reach freq 2; 2 was pushed last at freq 2, so it pops first + expect(popOrder).toEqual([2, 1, 2, 1]); + }); + + it("handles a single element", () => { + const popOrder = maxFrequencyStack([42]) as number[]; + expect(popOrder).toEqual([42]); + }); + + it("returns an empty array for empty input", () => { + const popOrder = maxFrequencyStack([]) as number[]; + expect(popOrder).toEqual([]); + }); + + it("pops the uniquely most frequent element immediately when it dominates", () => { + // 7 appears 3 times, all others once + // Pop order: freq-3 stack: [7] → 7; freq-2 stack: [7] → 7; freq-1 stack: [7,1,2] → pop order 2,1,7 + const popOrder = maxFrequencyStack([7, 1, 7, 2, 7]) as number[]; + expect(popOrder[0]).toBe(7); + expect(popOrder[1]).toBe(7); + expect(popOrder[2]).toBe(2); + }); + + it("returns the correct total number of elements", () => { + const input = [3, 1, 3, 2, 3, 1]; + const popOrder = maxFrequencyStack(input) as number[]; + expect(popOrder).toHaveLength(input.length); + }); +}); diff --git a/src/algorithms/stacks-queues/stack-design/max-frequency-stack/sources/MaxFrequencyStack.java b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/sources/MaxFrequencyStack.java new file mode 100644 index 00000000..0d96d196 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/sources/MaxFrequencyStack.java @@ -0,0 +1,40 @@ +// Max Frequency Stack — pop the most frequent element using a frequency map and stack-of-stacks +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MaxFrequencyStack { + public static List maxFrequencyStack(int[] values) { + Map freqMap = new HashMap<>(); // @step:initialize + Map> freqStacks = new HashMap<>(); // @step:initialize + int maxFrequency = 0; // @step:initialize + List popResults = new ArrayList<>(); // @step:initialize + + // Push phase: update frequency map and push each value onto its frequency-level stack + for (int elementIdx = 0; elementIdx < values.length; elementIdx++) { + int currentValue = values[elementIdx]; // @step:visit + int currentFreq = freqMap.getOrDefault(currentValue, 0) + 1; // @step:compare + freqMap.put(currentValue, currentFreq); // @step:compare + if (currentFreq > maxFrequency) { // @step:compare + maxFrequency = currentFreq; // @step:compare + } + freqStacks.computeIfAbsent(currentFreq, k -> new ArrayDeque<>()).push(currentValue); // @step:push + } + + // Pop phase: always pop from the highest-frequency stack + while (maxFrequency > 0) { // @step:pop + Deque topStack = freqStacks.get(maxFrequency); // @step:pop + int popped = topStack.pop(); // @step:pop + freqMap.put(popped, freqMap.get(popped) - 1); // @step:pop + if (topStack.isEmpty()) { // @step:pop + maxFrequency--; // @step:pop + } + popResults.add(popped); // @step:pop + } + + return popResults; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/stack-design/max-frequency-stack/sources/max-frequency-stack.py b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/sources/max-frequency-stack.py new file mode 100644 index 00000000..58c8a7d0 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/sources/max-frequency-stack.py @@ -0,0 +1,27 @@ +# Max Frequency Stack — pop the most frequent element using a frequency map and stack-of-stacks +def max_frequency_stack(values): + freq_map = {} # @step:initialize + freq_stacks = {} # @step:initialize + max_frequency = 0 # @step:initialize + pop_results = [] # @step:initialize + + # Push phase: update frequency map and push each value onto its frequency-level stack + for current_value in values: # @step:visit + current_freq = freq_map.get(current_value, 0) + 1 # @step:compare + freq_map[current_value] = current_freq # @step:compare + if current_freq > max_frequency: # @step:compare + max_frequency = current_freq # @step:compare + if current_freq not in freq_stacks: # @step:push + freq_stacks[current_freq] = [] # @step:push + freq_stacks[current_freq].append(current_value) # @step:push + + # Pop phase: always pop from the highest-frequency stack + while max_frequency > 0: # @step:pop + top_stack = freq_stacks[max_frequency] # @step:pop + popped = top_stack.pop() # @step:pop + freq_map[popped] -= 1 # @step:pop + if len(top_stack) == 0: # @step:pop + max_frequency -= 1 # @step:pop + pop_results.append(popped) # @step:pop + + return pop_results # @step:complete diff --git a/src/algorithms/stacks-queues/stack-design/max-frequency-stack/sources/max-frequency-stack.ts b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/sources/max-frequency-stack.ts new file mode 100644 index 00000000..0808939c --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/sources/max-frequency-stack.ts @@ -0,0 +1,34 @@ +// Max Frequency Stack — pop the most frequent element using a frequency map and stack-of-stacks +function maxFrequencyStack(values: number[]): number[] { + const freqMap: Record = {}; // @step:initialize + const freqStacks: Record = {}; // @step:initialize + let maxFrequency = 0; // @step:initialize + const popResults: number[] = []; // @step:initialize + + // Push phase: update frequency map and push each value onto its frequency-level stack + for (let elementIdx = 0; elementIdx < values.length; elementIdx++) { + const currentValue = values[elementIdx]!; // @step:visit + const currentFreq = (freqMap[currentValue] ?? 0) + 1; // @step:compare + freqMap[currentValue] = currentFreq; // @step:compare + if (currentFreq > maxFrequency) { + maxFrequency = currentFreq; // @step:compare + } + if (!freqStacks[currentFreq]) { + freqStacks[currentFreq] = []; // @step:push + } + freqStacks[currentFreq]!.push(currentValue); // @step:push + } + + // Pop phase: always pop from the highest-frequency stack + while (maxFrequency > 0) { + const topStack = freqStacks[maxFrequency]!; // @step:pop + const popped = topStack.pop()!; // @step:pop + freqMap[popped]--; // @step:pop + if (topStack.length === 0) { + maxFrequency--; // @step:pop + } + popResults.push(popped); // @step:pop + } + + return popResults; // @step:complete +} diff --git a/src/algorithms/stacks-queues/stack-design/max-frequency-stack/step-generator.test.ts b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/step-generator.test.ts new file mode 100644 index 00000000..21322ab8 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/step-generator.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect } from "vitest"; +import { generateMaxFrequencyStackSteps } from "./step-generator"; + +describe("generateMaxFrequencyStackSteps", () => { + it("produces steps for the default input", () => { + const steps = generateMaxFrequencyStackSteps({ values: [5, 7, 5, 7, 4, 5] }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateMaxFrequencyStackSteps({ values: [5, 7, 5, 7, 4, 5] }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateMaxFrequencyStackSteps({ values: [5, 7, 5, 7, 4, 5] }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateMaxFrequencyStackSteps({ values: [5, 7, 5, 7, 4, 5] }); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateMaxFrequencyStackSteps({ values: [5, 7, 5, 7, 4, 5] }); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits a visit step for each input element during push phase", () => { + const input = [5, 7, 5, 7, 4, 5]; + const steps = generateMaxFrequencyStackSteps({ values: input }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(input.length); + }); + + it("emits a push step for each input element during push phase", () => { + const input = [5, 7, 5, 7, 4, 5]; + const steps = generateMaxFrequencyStackSteps({ values: input }); + const pushSteps = steps.filter((step) => step.type === "push"); + expect(pushSteps.length).toBe(input.length); + }); + + it("emits a resolve step for each element during pop phase", () => { + const input = [5, 7, 5, 7, 4, 5]; + const steps = generateMaxFrequencyStackSteps({ values: input }); + const resolveSteps = steps.filter((step) => step.type === "resolve"); + expect(resolveSteps.length).toBe(input.length); + }); + + it("handles a single element input", () => { + const steps = generateMaxFrequencyStackSteps({ values: [42] }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("handles empty input", () => { + const steps = generateMaxFrequencyStackSteps({ values: [] }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("complete step variables contain the pop results array", () => { + const steps = generateMaxFrequencyStackSteps({ values: [5, 7, 5, 7, 4, 5] }); + const completeStep = steps[steps.length - 1]!; + const popResults = completeStep.variables["popResults"] as number[]; + expect(popResults).toEqual([5, 7, 5, 4, 7, 5]); + }); +}); diff --git a/src/algorithms/stacks-queues/stack-design/max-frequency-stack/step-generator.ts b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/step-generator.ts new file mode 100644 index 00000000..cdaa8af2 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/max-frequency-stack/step-generator.ts @@ -0,0 +1,82 @@ +/** Step generator for Max Frequency Stack — produces ExecutionStep[] using NumericStackTracker. */ + +import type { ExecutionStep } from "@/types"; +import { NumericStackTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const MFS_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.MAX_FREQUENCY_STACK!); + +export interface MaxFrequencyStackInput { + values: number[]; +} + +export function generateMaxFrequencyStackSteps(input: MaxFrequencyStackInput): ExecutionStep[] { + const { values } = input; + const tracker = new NumericStackTracker(values, MFS_LINE_MAP); + + // Logical data structures that mirror what the source algorithm maintains + const freqMap: Record = {}; + const freqStacks: Record = {}; + let maxFrequency = 0; + + tracker.initialize({ values, maxFrequency, freqMapSize: 0 }); + + // Push phase: process each element and update frequency structures + for (let elementIdx = 0; elementIdx < values.length; elementIdx++) { + const currentValue = values[elementIdx]!; + const prevFreq = freqMap[currentValue] ?? 0; + const currentFreq = prevFreq + 1; + freqMap[currentValue] = currentFreq; + + if (currentFreq > maxFrequency) { + maxFrequency = currentFreq; + } + + if (!freqStacks[currentFreq]) { + freqStacks[currentFreq] = []; + } + freqStacks[currentFreq]!.push(currentValue); + + tracker.processElement(elementIdx, { elementIdx, currentValue, prevFreq, currentFreq }); + + tracker.compare( + { elementIdx, currentValue, currentFreq, maxFrequency }, + `${currentValue} frequency is now ${currentFreq} — max frequency is ${maxFrequency}`, + ); + + tracker.pushIndex( + elementIdx, + { elementIdx, currentValue, currentFreq, maxFrequency }, + `Push ${currentValue} onto freq-${currentFreq} stack`, + ); + } + + // Pop phase: pop the most frequent element repeatedly + const popResults: number[] = []; + + while (maxFrequency > 0) { + const topStack = freqStacks[maxFrequency]!; + const popped = topStack.pop()!; + freqMap[popped] = (freqMap[popped] ?? 1) - 1; + + if (topStack.length === 0) { + maxFrequency--; + } + + popResults.push(popped); + + tracker.popAndResolve( + popped, + { popped, maxFrequency, popCount: popResults.length }, + `Pop most frequent: ${popped} — new max frequency is ${maxFrequency}`, + ); + } + + tracker.complete( + { popResults, totalPops: popResults.length }, + `Max Frequency Stack complete — pop order: [${popResults.join(", ")}]`, + ); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/stack-design/min-stack/MinStackPipeline.stories.tsx b/src/algorithms/stacks-queues/stack-design/min-stack/MinStackPipeline.stories.tsx new file mode 100644 index 00000000..10c6cb7c --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/min-stack/MinStackPipeline.stories.tsx @@ -0,0 +1,56 @@ +/** + * Storybook stories for the Min Stack algorithm pipeline. + * Uses the real step generator with [5, 3, 7, 1, 8], rendering the + * StackQueueVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateMinStackSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateMinStackSteps({ values: [5, 3, 7, 1, 8] }); +const ascendingSteps = generateMinStackSteps({ values: [1, 2, 3] }); + +const meta: Meta = { + title: "Algorithm Pipelines/Min Stack", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — both stacks empty before any elements are pushed */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing — auxiliary min stack diverges from main stack */ +export const MidProcessing: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Complete — all values pushed, minimum tracked in auxiliary stack */ +export const Complete: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Ascending input — min stays constant (1) throughout */ +export const AscendingInput: Story = { + args: { + visualState: ascendingSteps[ascendingSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/stack-design/min-stack/educational.ts b/src/algorithms/stacks-queues/stack-design/min-stack/educational.ts new file mode 100644 index 00000000..94eb6f65 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/min-stack/educational.ts @@ -0,0 +1,59 @@ +import type { EducationalContent } from "@/types"; + +export const minStackEducational: EducationalContent = { + overview: + "**Min Stack** (LeetCode 155) is a stack data structure that supports standard push, pop, and top operations alongside `getMin()` — all in **O(1)** time.\n\nThe trick is maintaining a second auxiliary stack in parallel with the main stack. Each position on the auxiliary stack records the minimum value *at that point in history*, so peeking its top always reveals the current minimum without scanning the entire main stack.", + + howItWorks: + "The algorithm pairs every element in the main stack with a corresponding entry in a min-tracking stack:\n\n" + + "1. **Push:** Add the new value to the main stack. Compare it with the auxiliary stack's top:\n" + + " - If the auxiliary stack is empty or `value ≤ top`, push `value` as the new minimum.\n" + + " - Otherwise duplicate the current top — the minimum has not changed.\n" + + "2. **Pop:** Remove the top element from both stacks simultaneously.\n" + + "3. **Top:** Return the top of the main stack.\n" + + "4. **GetMin:** Return the top of the auxiliary min stack — always O(1).\n\n" + + "### Example trace on `[5, 3, 7, 1, 8]`\n\n" + + "```\n" + + "push mainStack minTracker getMin\n" + + "5 [5] [5] 5\n" + + "3 [5, 3] [5, 3] 3\n" + + "7 [5, 3, 7] [5, 3, 3] 3\n" + + "1 [5, 3, 7, 1] [5, 3, 3, 1] 1\n" + + "8 [5, 3, 7, 1, 8] [5,3,3,1,1] 1\n" + + "```\n\n" + + "Popping 8: main → `[5,3,7,1]`, min → `[5,3,3,1]`, getMin still = 1.", + + timeAndSpaceComplexity: + "**Time Complexity: `O(1)` per operation**\n\n" + + "Push, pop, top, and getMin all operate directly on the stack top — constant time regardless of stack size. There is no search, scan, or rebalancing.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The auxiliary min stack stores exactly one entry per pushed element, doubling the space used relative to a plain stack. In the absolute worst case (e.g. `[5, 4, 3, 2, 1]` — a strictly decreasing sequence) every entry in the min stack holds a unique minimum.", + + bestAndWorstCase: + "**Best case** — `O(1)` per operation, always. The auxiliary stack guarantees constant-time `getMin` regardless of input order, stack depth, or value distribution.\n\n" + + "**Worst case (space)** — `O(n)` auxiliary space. A strictly decreasing input causes every auxiliary stack entry to hold a distinct minimum, maximising storage use.\n\n" + + "Contrast with a naïve `getMin` that scans the main stack each call: `O(n)` time per query for a stack of size `n`.", + + realWorldUses: [ + "**Undo/redo systems:** Many editors track a secondary stack of 'minimum cost to undo' alongside the action history.", + "**Expression evaluators:** Parsers that need O(1) access to the smallest pending operand or precedence level.", + "**Streaming analytics:** Real-time dashboards that must report the running minimum of a sliding window without rescanning.", + "**Interview preparation:** LeetCode 155 is a canonical stack-design problem asked at top-tier software companies.", + ], + + strengthsAndLimitations: { + strengths: [ + "All four operations (push, pop, top, getMin) are strictly O(1) — no amortised overhead.", + "Straightforward to implement and reason about — no complex data structure internals.", + "The auxiliary-stack pattern generalises easily to O(1) getMax, getMedian (with two stacks), or getSecondMin.", + ], + limitations: [ + "Uses 2× the memory of a plain stack due to the parallel auxiliary stack.", + "Only tracks the minimum for the current stack state — historical minimums after pops are lost once the entry is removed.", + "Not thread-safe without additional synchronisation.", + ], + }, + + whenToUseIt: + "Use the min-stack pattern whenever you need O(1) minimum (or maximum) access on a stack-shaped data structure. If you only ever need the global minimum over a static array, a single linear scan suffices. For sliding-window minimums over a sequence (not a stack), prefer a monotonic deque instead.", +}; diff --git a/src/algorithms/stacks-queues/stack-design/min-stack/index.ts b/src/algorithms/stacks-queues/stack-design/min-stack/index.ts new file mode 100644 index 00000000..b5716ccf --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/min-stack/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { minStack } from "./sources/min-stack.ts?fn"; +import { generateMinStackSteps } from "./step-generator"; +import type { MinStackInput } from "./step-generator"; +import { minStackEducational } from "./educational"; + +import typescriptSource from "./sources/min-stack.ts?raw"; +import pythonSource from "./sources/min-stack.py?raw"; +import javaSource from "./sources/MinStack.java?raw"; + +function executeMinStack(input: MinStackInput): number { + return minStack(input.values) as number; +} + +const minStackDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.MIN_STACK!, + name: "Min Stack", + category: CATEGORY.STACKS_QUEUES!, + technique: "stack-design", + description: + "Design a stack that supports push, pop, top, and retrieving the minimum element in O(1) time using a paired auxiliary min-tracking stack", + timeComplexity: { + best: "O(1)", + average: "O(1)", + worst: "O(1)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { values: [5, 3, 7, 1, 8] }, + }, + execute: executeMinStack, + generateSteps: generateMinStackSteps, + educational: minStackEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(minStackDefinition); diff --git a/src/algorithms/stacks-queues/stack-design/min-stack/min-stack.test.ts b/src/algorithms/stacks-queues/stack-design/min-stack/min-stack.test.ts new file mode 100644 index 00000000..dbbb40f5 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/min-stack/min-stack.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from "vitest"; +import { minStack } from "./sources/min-stack.ts?fn"; + +describe("minStack", () => { + it("returns 1 for the default input [5, 3, 7, 1, 8]", () => { + expect(minStack([5, 3, 7, 1, 8])).toBe(1); + }); + + it("returns 1 for an already ascending sequence [1, 2, 3]", () => { + expect(minStack([1, 2, 3])).toBe(1); + }); + + it("returns 1 for a strictly descending sequence [3, 2, 1]", () => { + expect(minStack([3, 2, 1])).toBe(1); + }); + + it("returns the single element when given a single-element array", () => { + expect(minStack([42])).toBe(42); + }); + + it("returns the correct minimum when all values are equal", () => { + expect(minStack([7, 7, 7])).toBe(7); + }); + + it("handles negative numbers correctly", () => { + expect(minStack([5, -3, 2, -1])).toBe(-3); + }); + + it("returns the minimum when it appears first", () => { + expect(minStack([1, 5, 10, 20])).toBe(1); + }); + + it("returns the minimum when it appears last", () => { + expect(minStack([20, 10, 5, 1])).toBe(1); + }); +}); diff --git a/src/algorithms/stacks-queues/stack-design/min-stack/sources/MinStack.java b/src/algorithms/stacks-queues/stack-design/min-stack/sources/MinStack.java new file mode 100644 index 00000000..d5af52ba --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/min-stack/sources/MinStack.java @@ -0,0 +1,26 @@ +// Min Stack — maintain a main stack paired with an auxiliary min-tracking stack for O(1) getMin +import java.util.ArrayDeque; +import java.util.Deque; + +public class MinStack { + public static int minStack(int[] values) { + Deque mainStack = new ArrayDeque<>(); // @step:initialize + Deque minTracker = new ArrayDeque<>(); // @step:initialize + + for (int elementIdx = 0; elementIdx < values.length; elementIdx++) { + int currentValue = values[elementIdx]; // @step:visit + + mainStack.push(currentValue); // @step:push + + // Maintain auxiliary min stack: duplicate current min if new value is not smaller + if (minTracker.isEmpty() || currentValue <= minTracker.peek()) { // @step:compare + minTracker.push(currentValue); // @step:push-auxiliary + } else { + minTracker.push(minTracker.peek()); // @step:push-auxiliary + } + } + + // The top of minTracker always holds the current minimum + return minTracker.peek(); // @step:peek,complete + } +} diff --git a/src/algorithms/stacks-queues/stack-design/min-stack/sources/min-stack.py b/src/algorithms/stacks-queues/stack-design/min-stack/sources/min-stack.py new file mode 100644 index 00000000..ce0eb98d --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/min-stack/sources/min-stack.py @@ -0,0 +1,16 @@ +# Min Stack — maintain a main stack paired with an auxiliary min-tracking stack for O(1) getMin +def min_stack(values): + main_stack = [] # @step:initialize + min_tracker = [] # @step:initialize + + for current_value in values: # @step:visit + main_stack.append(current_value) # @step:push + + # Maintain auxiliary min stack: duplicate current min if new value is not smaller + if not min_tracker or current_value <= min_tracker[-1]: # @step:compare + min_tracker.append(current_value) # @step:push-auxiliary + else: + min_tracker.append(min_tracker[-1]) # @step:push-auxiliary + + # The top of min_tracker always holds the current minimum + return min_tracker[-1] # @step:peek,complete diff --git a/src/algorithms/stacks-queues/stack-design/min-stack/sources/min-stack.ts b/src/algorithms/stacks-queues/stack-design/min-stack/sources/min-stack.ts new file mode 100644 index 00000000..6e4e2743 --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/min-stack/sources/min-stack.ts @@ -0,0 +1,22 @@ +// Min Stack — maintain a main stack paired with an auxiliary min-tracking stack for O(1) getMin +function minStack(values: number[]): number { + const mainStack: number[] = []; // @step:initialize + const minTracker: number[] = []; // @step:initialize + + for (let elementIdx = 0; elementIdx < values.length; elementIdx++) { + const currentValue = values[elementIdx]!; // @step:visit + + mainStack.push(currentValue); // @step:push + + // Maintain auxiliary min stack: duplicate current min if new value is not smaller + if (minTracker.length === 0 || currentValue <= minTracker[minTracker.length - 1]!) { + // @step:compare + minTracker.push(currentValue); // @step:push-auxiliary + } else { + minTracker.push(minTracker[minTracker.length - 1]!); // @step:push-auxiliary + } + } + + // The top of minTracker always holds the current minimum + return minTracker[minTracker.length - 1]!; // @step:peek,complete +} diff --git a/src/algorithms/stacks-queues/stack-design/min-stack/step-generator.test.ts b/src/algorithms/stacks-queues/stack-design/min-stack/step-generator.test.ts new file mode 100644 index 00000000..30809f7d --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/min-stack/step-generator.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect } from "vitest"; +import { generateMinStackSteps } from "./step-generator"; + +describe("generateMinStackSteps", () => { + it("produces steps for the default input", () => { + const steps = generateMinStackSteps({ values: [5, 3, 7, 1, 8] }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateMinStackSteps({ values: [5, 3, 7, 1, 8] }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateMinStackSteps({ values: [5, 3, 7, 1, 8] }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateMinStackSteps({ values: [5, 3, 7, 1, 8] }); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateMinStackSteps({ values: [5, 3, 7, 1, 8] }); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits a visit step for each element", () => { + const steps = generateMinStackSteps({ values: [5, 3, 7, 1, 8] }); + const visitSteps = steps.filter((step) => step.type === "visit"); + expect(visitSteps.length).toBe(5); + }); + + it("emits a push step (main stack) for each element", () => { + const steps = generateMinStackSteps({ values: [5, 3, 7, 1, 8] }); + const pushSteps = steps.filter((step) => step.type === "push"); + // Each element gets a main push + auxiliary push = 2 push-type steps per element + // push steps for main stack (type "push") = 5 + // auxiliary pushes use lineMapKey "push-auxiliary" but also type "push" + expect(pushSteps.length).toBe(10); + }); + + it("emits a compare step for each element", () => { + const steps = generateMinStackSteps({ values: [5, 3, 7, 1, 8] }); + const compareSteps = steps.filter((step) => step.type === "compare"); + expect(compareSteps.length).toBe(5); + }); + + it("emits a peek step at the end", () => { + const steps = generateMinStackSteps({ values: [5, 3, 7, 1, 8] }); + const peekSteps = steps.filter((step) => step.type === "peek"); + expect(peekSteps.length).toBe(1); + }); + + it("records the final minimum in the complete step variables", () => { + const steps = generateMinStackSteps({ values: [5, 3, 7, 1, 8] }); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.variables["finalMin"]).toBe(1); + }); + + it("tracks the auxiliary stack in the visual state", () => { + const steps = generateMinStackSteps({ values: [5, 3, 7, 1, 8] }); + const lastStep = steps[steps.length - 1]!; + const visualState = lastStep.visualState; + if (visualState.kind === "stack-queue") { + expect(visualState.auxiliaryStack).toBeDefined(); + expect(visualState.auxiliaryStack?.length).toBeGreaterThan(0); + } + }); + + it("works correctly for a single-element input", () => { + const steps = generateMinStackSteps({ values: [42] }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); +}); diff --git a/src/algorithms/stacks-queues/stack-design/min-stack/step-generator.ts b/src/algorithms/stacks-queues/stack-design/min-stack/step-generator.ts new file mode 100644 index 00000000..0e432e8a --- /dev/null +++ b/src/algorithms/stacks-queues/stack-design/min-stack/step-generator.ts @@ -0,0 +1,75 @@ +/** Step generator for Min Stack — produces ExecutionStep[] using NumericStackTracker. */ + +import type { ExecutionStep } from "@/types"; +import { NumericStackTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const MS_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.MIN_STACK!); + +export interface MinStackInput { + values: number[]; +} + +export function generateMinStackSteps(input: MinStackInput): ExecutionStep[] { + const { values } = input; + const tracker = new NumericStackTracker(values, MS_LINE_MAP); + + // Logical min tracker mirrors the auxiliary stack for comparison decisions + const logicalMinStack: number[] = []; + + tracker.initialize({ values, mainStackSize: 0, minTrackerSize: 0 }); + + for (let elementIdx = 0; elementIdx < values.length; elementIdx++) { + const currentValue = values[elementIdx]!; + + tracker.processElement(elementIdx, { elementIdx, currentValue }); + + // Push current value onto main stack + tracker.pushIndex( + elementIdx, + { elementIdx, currentValue, mainStackSize: elementIdx + 1 }, + `Push ${currentValue} onto main stack`, + ); + + // Determine what value goes on the auxiliary min stack + const currentMin = + logicalMinStack.length === 0 ? currentValue : logicalMinStack[logicalMinStack.length - 1]!; + const isNewMin = logicalMinStack.length === 0 || currentValue <= currentMin; + + tracker.compare( + { + elementIdx, + currentValue, + currentMin: logicalMinStack.length === 0 ? "empty" : currentMin, + isNewMin, + }, + isNewMin + ? `${currentValue} ≤ current min (${logicalMinStack.length === 0 ? "empty stack" : currentMin}) — push ${currentValue} as new min` + : `${currentValue} > current min ${currentMin} — duplicate ${currentMin} onto min stack`, + ); + + const minValueToPush = isNewMin ? currentValue : currentMin; + logicalMinStack.push(minValueToPush); + + tracker.pushAuxiliary( + String(minValueToPush), + { elementIdx, currentValue, minValueToPush, minTrackerSize: logicalMinStack.length }, + `Push ${minValueToPush} onto min tracker — current minimum is ${minValueToPush}`, + ); + } + + const finalMin = logicalMinStack[logicalMinStack.length - 1] ?? 0; + + tracker.peek( + { finalMin, totalElements: values.length }, + `getMin() → ${finalMin} — peek top of min tracker`, + ); + + tracker.complete( + { finalMin, totalElements: values.length }, + `Min Stack complete — minimum value is ${finalMin}`, + ); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/validation/longest-valid-parentheses/LongestValidParenthesesPipeline.stories.tsx b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/LongestValidParenthesesPipeline.stories.tsx new file mode 100644 index 00000000..42ed8d99 --- /dev/null +++ b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/LongestValidParenthesesPipeline.stories.tsx @@ -0,0 +1,56 @@ +/** + * Storybook stories for the Longest Valid Parentheses algorithm pipeline. + * Uses the real step generator with "(()())", rendering the + * StackQueueVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateLongestValidParenthesesSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const defaultSteps = generateLongestValidParenthesesSteps({ inputString: "(()())" }); +const partialSteps = generateLongestValidParenthesesSteps({ inputString: "(()" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Longest Valid Parentheses", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — sentinel -1 on the stack, full input unprocessed */ +export const InitialState: Story = { + args: { + visualState: defaultSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing with indices on the stack and a running maxLength */ +export const MidProcessing: Story = { + args: { + visualState: defaultSteps[Math.floor(defaultSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Complete — longest valid substring length computed */ +export const Complete: Story = { + args: { + visualState: defaultSteps[defaultSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Partial match — input with no full closure, showing base reset behavior */ +export const PartialMatch: Story = { + args: { + visualState: partialSteps[partialSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/validation/longest-valid-parentheses/educational.ts b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/educational.ts new file mode 100644 index 00000000..59ff0c0c --- /dev/null +++ b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/educational.ts @@ -0,0 +1,60 @@ +import type { EducationalContent } from "@/types"; + +export const longestValidParenthesesEducational: EducationalContent = { + overview: + "**Longest Valid Parentheses** (LeetCode 32) finds the length of the longest contiguous substring that forms a well-matched sequence of parentheses.\n\nA stack that stores **indices** — rather than characters — is the key insight. By tracking where each unmatched `(` sits and anchoring the base of each valid run, the algorithm measures substring lengths in a single left-to-right pass.", + + howItWorks: + "The stack always holds a sentinel base index. Initially `-1` is pushed so the first valid run's length can be computed immediately:\n\n" + + "1. **`(`** → push its index onto the stack.\n" + + "2. **`)`** → pop the stack top.\n" + + " - If the stack is now **empty**, the current index becomes the new base (push it).\n" + + " - Otherwise, compute `length = currentIdx − newStackTop` and update `maxLength`.\n" + + "3. Return `maxLength` after the full scan.\n\n" + + "### Example trace on `(()())`\n\n" + + "```\n" + + "idx char action stack maxLength\n" + + " — — init push -1 [-1] 0\n" + + " 0 ( push 0 [-1, 0] 0\n" + + " 1 ( push 1 [-1, 0, 1] 0\n" + + " 2 ) pop → top=-1,0 [-1, 0] 2-0=2\n" + + " 3 ( push 3 [-1, 0, 3] 2\n" + + " 4 ) pop → top=-1,0 [-1, 0] 4-0=4\n" + + " 5 ) pop → top=-1 [-1] 5-(-1)=6\n" + + "end result: 6\n" + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "Each character is visited exactly once. Every index is pushed and popped at most once, so all stack operations together take `O(n)` total.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "In the worst case — a string of all `(` characters — every index ends up on the stack, using `O(n)` extra space.", + + bestAndWorstCase: + "**Best case** — the entire string is one long valid sequence (e.g. `()()()`): every character is processed and a new maximum is updated on each `)`, but no stack rebuilding is needed. Still `O(n)` time.\n\n" + + "**Worst case** — alternating mismatches (e.g. `)()(`) force frequent base resets, but each character is still touched only once, so the worst case is also `O(n)` time.\n\n" + + "There is no early-termination shortcut: the algorithm must scan the entire string.", + + realWorldUses: [ + "**Code formatters:** Identifying the longest balanced region helps editors suggest minimal bracket insertions.", + "**Expression parsers:** Compilers locate the largest syntactically valid sub-expression before reporting surrounding errors.", + "**Log analysis:** Detecting the longest contiguous well-formed section in a stream of structured delimiters (e.g. JSON fragments).", + "**Text editors:** Bracket-matching UIs highlight the outermost well-formed region around the cursor.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(n) time — single scan, no nested loops or backtracking.", + "O(n) space with a simple stack; no auxiliary DP table needed.", + "Handles arbitrary nesting depth and discontinuous valid segments correctly.", + ], + limitations: [ + "Only handles `(` and `)` — does not generalize to multi-type brackets without modification.", + "O(n) stack space is unavoidable with the index-stack approach; a two-pass counter approach achieves O(1) space but is harder to visualize.", + "Returns only the length, not the substring itself; recovering the actual substring requires extra bookkeeping.", + ], + }, + + whenToUseIt: + "Use the index-stack approach when you need clarity and O(n) time. If memory is extremely constrained, consider the two-pass left-right counter method for O(1) space at the cost of two passes. For related problems involving multiple bracket types, extend the stack to store `(character, index)` pairs.", +}; diff --git a/src/algorithms/stacks-queues/validation/longest-valid-parentheses/index.ts b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/index.ts new file mode 100644 index 00000000..536730d0 --- /dev/null +++ b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { longestValidParentheses } from "./sources/longest-valid-parentheses.ts?fn"; +import { generateLongestValidParenthesesSteps } from "./step-generator"; +import type { LongestValidParenthesesInput } from "./step-generator"; +import { longestValidParenthesesEducational } from "./educational"; + +import typescriptSource from "./sources/longest-valid-parentheses.ts?raw"; +import pythonSource from "./sources/longest-valid-parentheses.py?raw"; +import javaSource from "./sources/LongestValidParentheses.java?raw"; + +function executeLongestValidParentheses(input: LongestValidParenthesesInput): number { + return longestValidParentheses(input.inputString) as number; +} + +const longestValidParenthesesDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.LONGEST_VALID_PARENTHESES!, + name: "Longest Valid Parentheses", + category: CATEGORY.STACKS_QUEUES!, + technique: "validation", + description: + "Use a stack of indices with a sentinel base to find the length of the longest well-formed parentheses substring in a single pass", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { inputString: "(()())" }, + }, + execute: executeLongestValidParentheses, + generateSteps: generateLongestValidParenthesesSteps, + educational: longestValidParenthesesEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(longestValidParenthesesDefinition); diff --git a/src/algorithms/stacks-queues/validation/longest-valid-parentheses/longest-valid-parentheses.test.ts b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/longest-valid-parentheses.test.ts new file mode 100644 index 00000000..72dcc335 --- /dev/null +++ b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/longest-valid-parentheses.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from "vitest"; +import { longestValidParentheses } from "./sources/longest-valid-parentheses.ts?fn"; + +describe("longestValidParentheses", () => { + it("returns 2 for '(()'", () => { + expect(longestValidParentheses("(()")).toBe(2); + }); + + it("returns 4 for ')()())'", () => { + expect(longestValidParentheses(")()())")).toBe(4); + }); + + it("returns 0 for empty string", () => { + expect(longestValidParentheses("")).toBe(0); + }); + + it("returns 6 for '(()())'", () => { + expect(longestValidParentheses("(()())")).toBe(6); + }); + + it("returns 4 for '()()'", () => { + expect(longestValidParentheses("()()")).toBe(4); + }); + + it("returns 0 for '((('", () => { + expect(longestValidParentheses("(((")).toBe(0); + }); +}); diff --git a/src/algorithms/stacks-queues/validation/longest-valid-parentheses/sources/LongestValidParentheses.java b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/sources/LongestValidParentheses.java new file mode 100644 index 00000000..b507c1f0 --- /dev/null +++ b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/sources/LongestValidParentheses.java @@ -0,0 +1,31 @@ +// Longest Valid Parentheses — find the length of the longest well-formed parentheses substring +import java.util.ArrayDeque; +import java.util.Deque; + +public class LongestValidParentheses { + public static int longestValidParentheses(String inputString) { + Deque indexStack = new ArrayDeque<>(); // @step:initialize + indexStack.push(-1); // @step:initialize + int maxLength = 0; // @step:initialize + for (int charIdx = 0; charIdx < inputString.length(); charIdx++) { + char ch = inputString.charAt(charIdx); // @step:visit + if (ch == '(') { + indexStack.push(charIdx); // @step:push + } else { + // Pop the top; if stack becomes empty, push current index as new base + indexStack.pop(); // @step:pop + if (indexStack.isEmpty()) { + indexStack.push(charIdx); // @step:push + } else { + // Length of current valid substring = current index minus new stack top + int stackTop = indexStack.peek(); // @step:compare + int currentLength = charIdx - stackTop; // @step:compare + if (currentLength > maxLength) { + maxLength = currentLength; // @step:compare + } + } + } + } + return maxLength; // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/validation/longest-valid-parentheses/sources/longest-valid-parentheses.py b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/sources/longest-valid-parentheses.py new file mode 100644 index 00000000..ea172c22 --- /dev/null +++ b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/sources/longest-valid-parentheses.py @@ -0,0 +1,19 @@ +# Longest Valid Parentheses — find the length of the longest well-formed parentheses substring +def longest_valid_parentheses(input_string): + index_stack = [-1] # @step:initialize + max_length = 0 # @step:initialize + for char_idx, char in enumerate(input_string): # @step:visit + if char == "(": + index_stack.append(char_idx) # @step:push + else: + # Pop the top; if stack becomes empty, push current index as new base + index_stack.pop() # @step:pop + if not index_stack: + index_stack.append(char_idx) # @step:push + else: + # Length of current valid substring = current index minus new stack top + stack_top = index_stack[-1] # @step:compare + current_length = char_idx - stack_top # @step:compare + if current_length > max_length: + max_length = current_length # @step:compare + return max_length # @step:complete diff --git a/src/algorithms/stacks-queues/validation/longest-valid-parentheses/sources/longest-valid-parentheses.ts b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/sources/longest-valid-parentheses.ts new file mode 100644 index 00000000..234bcb44 --- /dev/null +++ b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/sources/longest-valid-parentheses.ts @@ -0,0 +1,25 @@ +// Longest Valid Parentheses — find the length of the longest well-formed parentheses substring +function longestValidParentheses(inputString: string): number { + const indexStack: number[] = [-1]; // @step:initialize + let maxLength = 0; // @step:initialize + for (let charIdx = 0; charIdx < inputString.length; charIdx++) { + const char = inputString[charIdx]!; // @step:visit + if (char === "(") { + indexStack.push(charIdx); // @step:push + } else { + // Pop the top; if stack becomes empty, push current index as new base + indexStack.pop(); // @step:pop + if (indexStack.length === 0) { + indexStack.push(charIdx); // @step:push + } else { + // Length of current valid substring = current index minus new stack top + const stackTop = indexStack[indexStack.length - 1]!; // @step:compare + const currentLength = charIdx - stackTop; // @step:compare + if (currentLength > maxLength) { + maxLength = currentLength; // @step:compare + } + } + } + } + return maxLength; // @step:complete +} diff --git a/src/algorithms/stacks-queues/validation/longest-valid-parentheses/step-generator.test.ts b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/step-generator.test.ts new file mode 100644 index 00000000..40b4cffb --- /dev/null +++ b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/step-generator.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from "vitest"; +import { generateLongestValidParenthesesSteps } from "./step-generator"; + +describe("generateLongestValidParenthesesSteps", () => { + it("produces steps for the default input", () => { + const steps = generateLongestValidParenthesesSteps({ inputString: "(()())" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateLongestValidParenthesesSteps({ inputString: "(()())" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateLongestValidParenthesesSteps({ inputString: "(()())" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateLongestValidParenthesesSteps({ inputString: "(()())" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateLongestValidParenthesesSteps({ inputString: "(()())" }); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits push steps for each opening bracket", () => { + const steps = generateLongestValidParenthesesSteps({ inputString: "(()())" }); + const pushSteps = steps.filter((step) => step.type === "push"); + // 3 opening brackets + 1 initial sentinel base push = 4 push steps + expect(pushSteps.length).toBeGreaterThanOrEqual(3); + }); + + it("emits match steps when closing bracket extends a valid run", () => { + const steps = generateLongestValidParenthesesSteps({ inputString: "(()())" }); + const matchSteps = steps.filter((step) => step.type === "match"); + expect(matchSteps.length).toBeGreaterThan(0); + }); + + it("emits a mismatch step when closing bracket empties the stack", () => { + const steps = generateLongestValidParenthesesSteps({ inputString: ")()())" }); + const mismatchSteps = steps.filter((step) => step.type === "mismatch"); + expect(mismatchSteps.length).toBeGreaterThan(0); + }); + + it("handles an empty string", () => { + const steps = generateLongestValidParenthesesSteps({ inputString: "" }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("handles all opening brackets with no match steps", () => { + const steps = generateLongestValidParenthesesSteps({ inputString: "(((" }); + const matchSteps = steps.filter((step) => step.type === "match"); + expect(matchSteps.length).toBe(0); + }); + + it("the complete step variables contain maxLength", () => { + const steps = generateLongestValidParenthesesSteps({ inputString: "(()())" }); + const completeStep = steps[steps.length - 1]; + expect(completeStep?.variables).toHaveProperty("maxLength", 6); + }); +}); diff --git a/src/algorithms/stacks-queues/validation/longest-valid-parentheses/step-generator.ts b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/step-generator.ts new file mode 100644 index 00000000..296c2440 --- /dev/null +++ b/src/algorithms/stacks-queues/validation/longest-valid-parentheses/step-generator.ts @@ -0,0 +1,87 @@ +/** Step generator for Longest Valid Parentheses — produces ExecutionStep[] using StackQueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { StackQueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const LVP_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.LONGEST_VALID_PARENTHESES!); + +export interface LongestValidParenthesesInput { + inputString: string; +} + +export function generateLongestValidParenthesesSteps( + input: LongestValidParenthesesInput, +): ExecutionStep[] { + const { inputString } = input; + const tracker = new StackQueueTracker(inputString, LVP_LINE_MAP); + + /** + * Stack stores indices as strings so the StackQueueTracker can display them. + * The first element (-1) acts as the base sentinel index. + */ + const indexStack: number[] = [-1]; + let maxLength = 0; + + tracker.initialize({ inputString, indexStack: [-1], maxLength }); + + for (let charIdx = 0; charIdx < inputString.length; charIdx++) { + const char = inputString[charIdx]!; + + tracker.processChar(charIdx, { charIdx, char, maxLength }); + + if (char === "(") { + indexStack.push(charIdx); + tracker.push(String(charIdx), charIdx, { + charIdx, + char, + stackSize: indexStack.length, + maxLength, + }); + } else { + // Pop the top of the stack + indexStack.pop(); + + if (indexStack.length === 0) { + // Stack became empty — push current index as new base anchor + indexStack.push(charIdx); + tracker.mismatch(")", charIdx, { + charIdx, + char, + reason: "stack empty after pop — pushing current index as new base", + indexStack: [...indexStack], + maxLength, + }); + // Also record the push of the new base + tracker.push(String(charIdx), charIdx, { + charIdx, + char, + reason: "new base index pushed", + stackSize: indexStack.length, + maxLength, + }); + } else { + // Compute valid substring length from current index to new stack top + const stackTop = indexStack[indexStack.length - 1]!; + const currentLength = charIdx - stackTop; + if (currentLength > maxLength) { + maxLength = currentLength; + } + tracker.popMatched(")", charIdx, { + charIdx, + stackTop, + currentLength, + maxLength, + }); + } + } + } + + tracker.complete(maxLength > 0 || inputString.length === 0, { + maxLength, + inputLength: inputString.length, + }); + + return tracker.getSteps(); +} diff --git a/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/MinRemoveToMakeValidPipeline.stories.tsx b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/MinRemoveToMakeValidPipeline.stories.tsx new file mode 100644 index 00000000..8402025c --- /dev/null +++ b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/MinRemoveToMakeValidPipeline.stories.tsx @@ -0,0 +1,56 @@ +/** + * Storybook stories for the Min Remove to Make Valid algorithm pipeline. + * Uses the real step generator with "a(b(c)d" (unbalanced) and "(a(b)c)" (balanced), + * rendering the StackQueueVisualizer at key traversal states. + */ +import type { Meta, StoryObj } from "@storybook/react"; +import type { StackQueueVisualState } from "@/types"; +import { generateMinRemoveToMakeValidSteps } from "./step-generator"; +import StackQueueVisualizer from "@/components/visualization/StackQueueVisualizer"; + +const balancedSteps = generateMinRemoveToMakeValidSteps({ inputString: "(a(b)c)" }); +const unbalancedSteps = generateMinRemoveToMakeValidSteps({ inputString: "a(b(c)d" }); + +const meta: Meta = { + title: "Algorithm Pipelines/Min Remove to Make Valid", + component: StackQueueVisualizer, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +/** Initial state — full input string unprocessed, empty stack */ +export const InitialState: Story = { + args: { + visualState: balancedSteps[0]!.visualState as StackQueueVisualState, + }, +}; + +/** Mid-processing with some characters visited and brackets on the stack */ +export const MidProcessing: Story = { + args: { + visualState: balancedSteps[Math.floor(balancedSteps.length / 2)]! + .visualState as StackQueueVisualState, + }, +}; + +/** Completed valid string — no brackets removed, all matched */ +export const ValidComplete: Story = { + args: { + visualState: balancedSteps[balancedSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; + +/** Unbalanced input — unmatched bracket removed to produce valid result */ +export const InvalidRemoval: Story = { + args: { + visualState: unbalancedSteps[unbalancedSteps.length - 1]!.visualState as StackQueueVisualState, + }, +}; diff --git a/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/educational.ts b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/educational.ts new file mode 100644 index 00000000..c40b03ac --- /dev/null +++ b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/educational.ts @@ -0,0 +1,65 @@ +import type { EducationalContent } from "@/types"; + +export const minRemoveToMakeValidEducational: EducationalContent = { + overview: + "**Min Remove to Make Valid** (LeetCode 1249) finds the minimum number of parentheses to remove from a string so that the remaining parentheses are valid.\n\n" + + "A string is valid when every `(` has a matching `)` in the correct order. The algorithm uses a stack to track indices of unmatched `(` characters and a set to record unmatched `)` characters, then rebuilds the string excluding all those indices.", + + howItWorks: + "The algorithm performs two passes:\n\n" + + "**Pass 1 — Identify unmatched parentheses:**\n" + + "Scan left to right, maintaining a stack of indices for unmatched `(` characters:\n\n" + + "1. **`(` character** → push its index onto the stack.\n" + + "2. **`)` character**:\n" + + " - If the stack is non-empty, pop the top index (the `(` is now matched).\n" + + " - If the stack is empty, record the `)` index as unmatched.\n" + + "3. **End of scan** → any indices remaining in the stack are unmatched `(`.\n\n" + + "**Pass 2 — Build result:**\n" + + "Reconstruct the string, skipping all indices from the unmatched set.\n\n" + + "### Example trace on `a(b(c)d`\n\n" + + "```\n" + + "idx char action stack unmatched_close\n" + + "0 a non-paren, skip [] {}\n" + + "1 ( push idx 1 [1] {}\n" + + "2 b non-paren, skip [1] {}\n" + + "3 ( push idx 3 [1, 3] {}\n" + + "4 c non-paren, skip [1, 3] {}\n" + + "5 ) pop idx 3 (matched) [1] {}\n" + + "6 d non-paren, skip [1] {}\n" + + "end stack has [1] → idx 1 is unmatched\n" + + 'result: skip idx 1 → "ab(c)d"\n' + + "```", + + timeAndSpaceComplexity: + "**Time Complexity: `O(n)`**\n\n" + + "The string is scanned twice — once to identify unmatched indices (`O(n)`) and once to build the result (`O(n)`). Stack push/pop and set insert/lookup are all `O(1)` amortized.\n\n" + + "**Space Complexity: `O(n)`**\n\n" + + "The stack holds at most `O(n)` unmatched `(` indices, and the unmatched-close set holds at most `O(n)` indices. The result string itself also uses `O(n)` space.", + + bestAndWorstCase: + "**Best case** — a fully balanced string like `(a)`: no indices are unmatched, the stack is empty after the scan, and the input is returned unchanged in `O(n)` time.\n\n" + + "**Worst case** — a string with no valid pairs like `))))` or `((((`: every character index is unmatched, the result is an empty string, and both passes still run in `O(n)` time.", + + realWorldUses: [ + "**Auto-correct in editors:** IDEs use this pattern to suggest minimal edits that make expression syntax valid after incremental typing.", + "**Syntax error recovery in parsers:** Compilers can apply a similar minimum-edit strategy to produce a partially valid parse tree for better error messages.", + "**Data cleaning pipelines:** Removing malformed delimiters from imported datasets (CSV fields with unbalanced quotes, JSON with unclosed brackets).", + "**Template engines:** Ensuring template strings have balanced placeholder delimiters before rendering.", + ], + + strengthsAndLimitations: { + strengths: [ + "O(n) time with a single left-to-right scan followed by a single reconstruction pass.", + "Produces a deterministic result — always removes the minimum number of characters.", + "Handles non-parenthesis characters transparently; they are always kept in the output.", + ], + limitations: [ + "O(n) extra space for the stack and the unmatched-index set.", + "Only handles `()` — extending to multiple bracket types like `[]` or `{}` requires extra logic.", + "When multiple minimum-removal solutions exist, this algorithm returns only one; it does not enumerate all possibilities.", + ], + }, + + whenToUseIt: + "Use this approach when you need to produce a valid parenthesized string with the fewest deletions. If you only need a boolean validity check, use the simpler stack-based valid-parentheses algorithm. For multiple bracket types `()[]{}`, extend the stack to store both the bracket type and the index.", +}; diff --git a/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/index.ts b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/index.ts new file mode 100644 index 00000000..ec0377a8 --- /dev/null +++ b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/index.ts @@ -0,0 +1,45 @@ +import type { AlgorithmDefinition } from "@/types"; +import { registry } from "@/registry"; +import { ALGORITHM_ID, CATEGORY } from "@/utils/constants"; + +import { minRemoveToMakeValid } from "./sources/min-remove-to-make-valid.ts?fn"; +import { generateMinRemoveToMakeValidSteps } from "./step-generator"; +import type { MinRemoveToMakeValidInput } from "./step-generator"; +import { minRemoveToMakeValidEducational } from "./educational"; + +import typescriptSource from "./sources/min-remove-to-make-valid.ts?raw"; +import pythonSource from "./sources/min-remove-to-make-valid.py?raw"; +import javaSource from "./sources/MinRemoveToMakeValid.java?raw"; + +function executeMinRemoveToMakeValid(input: MinRemoveToMakeValidInput): string { + return minRemoveToMakeValid(input.inputString) as string; +} + +const minRemoveToMakeValidDefinition: AlgorithmDefinition = { + meta: { + id: ALGORITHM_ID.MIN_REMOVE_TO_MAKE_VALID!, + name: "Min Remove to Make Valid", + category: CATEGORY.STACKS_QUEUES!, + technique: "validation", + description: + "Use a stack of indices to track unmatched '(' and a set for unmatched ')', then rebuild the string excluding all unmatched indices", + timeComplexity: { + best: "O(n)", + average: "O(n)", + worst: "O(n)", + }, + spaceComplexity: "O(n)", + supportedLanguages: ["typescript", "python", "java"], + defaultInput: { inputString: "a(b(c)d" }, + }, + execute: executeMinRemoveToMakeValid, + generateSteps: generateMinRemoveToMakeValidSteps, + educational: minRemoveToMakeValidEducational, + sources: { + typescript: typescriptSource, + python: pythonSource, + java: javaSource, + }, +}; + +registry.register(minRemoveToMakeValidDefinition); diff --git a/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/min-remove-to-make-valid.test.ts b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/min-remove-to-make-valid.test.ts new file mode 100644 index 00000000..b9a17473 --- /dev/null +++ b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/min-remove-to-make-valid.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from "vitest"; +import { minRemoveToMakeValid } from "./sources/min-remove-to-make-valid.ts?fn"; + +describe("minRemoveToMakeValid", () => { + it("returns an already-balanced string unchanged", () => { + expect(minRemoveToMakeValid("(ab)")).toBe("(ab)"); + }); + + it("removes an unmatched opening bracket", () => { + expect(minRemoveToMakeValid("a(b(c)d")).toBe("ab(c)d"); + }); + + it("removes an unmatched closing bracket", () => { + expect(minRemoveToMakeValid("a)b")).toBe("ab"); + }); + + it("removes multiple unmatched closing brackets", () => { + expect(minRemoveToMakeValid("))ab")).toBe("ab"); + }); + + it("removes multiple unmatched opening brackets", () => { + expect(minRemoveToMakeValid("ab((")).toBe("ab"); + }); + + it("handles a mix of unmatched opening and closing brackets", () => { + // "lee(t(c)o)de)" — the trailing ')' at index 12 is unmatched and must be removed + expect(minRemoveToMakeValid("lee(t(c)o)de)")).toBe("lee(t(c)o)de"); + }); + + it("returns an empty string for a string of only unmatched brackets", () => { + expect(minRemoveToMakeValid(")))")).toBe(""); + }); + + it("handles an empty string", () => { + expect(minRemoveToMakeValid("")).toBe(""); + }); + + it("returns a string with no parentheses unchanged", () => { + expect(minRemoveToMakeValid("abcdef")).toBe("abcdef"); + }); + + it("handles deeply nested valid parentheses", () => { + expect(minRemoveToMakeValid("((()))")).toBe("((()))"); + }); + + it("removes both unmatched open and close from the same string", () => { + // ")a(b(c)d(" — leading ')' and the two outermost '(' are unmatched; result is "ab(c)d" + expect(minRemoveToMakeValid(")a(b(c)d(")).toBe("ab(c)d"); + }); +}); diff --git a/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/sources/MinRemoveToMakeValid.java b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/sources/MinRemoveToMakeValid.java new file mode 100644 index 00000000..d687ecec --- /dev/null +++ b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/sources/MinRemoveToMakeValid.java @@ -0,0 +1,34 @@ +// Min Remove to Make Valid — use a stack of indices to track unmatched '(' and a set for unmatched ')' +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashSet; +import java.util.Set; + +public class MinRemoveToMakeValid { + public static String minRemoveToMakeValid(String inputString) { + Deque unmatchedOpenIndices = new ArrayDeque<>(); // @step:initialize + Set unmatchedCloseIndices = new HashSet<>(); // @step:initialize + for (int charIdx = 0; charIdx < inputString.length(); charIdx++) { + char ch = inputString.charAt(charIdx); // @step:visit + if (ch == '(') { + unmatchedOpenIndices.push(charIdx); // @step:push + } else if (ch == ')') { + if (!unmatchedOpenIndices.isEmpty()) { + unmatchedOpenIndices.pop(); // @step:pop + } else { + unmatchedCloseIndices.add(charIdx); // @step:mismatch + } + } + } + // Remaining indices in the stack are unmatched opening brackets + Set unmatchedIndices = new HashSet<>(unmatchedCloseIndices); // @step:mismatch + unmatchedIndices.addAll(unmatchedOpenIndices); // @step:mismatch + StringBuilder result = new StringBuilder(); // @step:complete + for (int charIdx = 0; charIdx < inputString.length(); charIdx++) { + if (!unmatchedIndices.contains(charIdx)) { + result.append(inputString.charAt(charIdx)); // @step:complete + } + } + return result.toString(); // @step:complete + } +} diff --git a/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/sources/min-remove-to-make-valid.py b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/sources/min-remove-to-make-valid.py new file mode 100644 index 00000000..2087787d --- /dev/null +++ b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/sources/min-remove-to-make-valid.py @@ -0,0 +1,19 @@ +# Min Remove to Make Valid — use a stack of indices to track unmatched '(' and a set for unmatched ')' +def min_remove_to_make_valid(input_string): + unmatched_open_indices = [] # @step:initialize + unmatched_close_indices = set() # @step:initialize + for char_idx, char in enumerate(input_string): # @step:visit + if char == "(": + unmatched_open_indices.append(char_idx) # @step:push + elif char == ")": + if unmatched_open_indices: + unmatched_open_indices.pop() # @step:pop + else: + unmatched_close_indices.add(char_idx) # @step:mismatch + # Remaining indices in the stack are unmatched opening brackets + unmatched_indices = set(unmatched_open_indices) | unmatched_close_indices # @step:mismatch + result = "" # @step:complete + for char_idx, char in enumerate(input_string): # @step:complete + if char_idx not in unmatched_indices: + result += char # @step:complete + return result # @step:complete diff --git a/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/sources/min-remove-to-make-valid.ts b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/sources/min-remove-to-make-valid.ts new file mode 100644 index 00000000..d99a6ef2 --- /dev/null +++ b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/sources/min-remove-to-make-valid.ts @@ -0,0 +1,29 @@ +// Min Remove to Make Valid — use a stack of indices to track unmatched '(' and a set for unmatched ')' +function minRemoveToMakeValid(inputString: string): string { + const unmatchedOpenIndices: number[] = []; // @step:initialize + const unmatchedCloseIndices: Set = new Set(); // @step:initialize + for (let charIdx = 0; charIdx < inputString.length; charIdx++) { + const char = inputString[charIdx]!; // @step:visit + if (char === "(") { + unmatchedOpenIndices.push(charIdx); // @step:push + } else if (char === ")") { + if (unmatchedOpenIndices.length > 0) { + unmatchedOpenIndices.pop(); // @step:pop + } else { + unmatchedCloseIndices.add(charIdx); // @step:mismatch + } + } + } + // Remaining indices in the stack are unmatched opening brackets + const unmatchedIndices: Set = new Set([ + ...unmatchedOpenIndices, + ...unmatchedCloseIndices, + ]); // @step:mismatch + let result = ""; // @step:complete + for (let charIdx = 0; charIdx < inputString.length; charIdx++) { + if (!unmatchedIndices.has(charIdx)) { + result += inputString[charIdx]; // @step:complete + } + } + return result; // @step:complete +} diff --git a/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/step-generator.test.ts b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/step-generator.test.ts new file mode 100644 index 00000000..d8bc8a2b --- /dev/null +++ b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/step-generator.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect } from "vitest"; +import { generateMinRemoveToMakeValidSteps } from "./step-generator"; + +describe("generateMinRemoveToMakeValidSteps", () => { + it("produces steps for the default input", () => { + const steps = generateMinRemoveToMakeValidSteps({ inputString: "a(b(c)d" }); + expect(steps.length).toBeGreaterThan(0); + }); + + it("starts with an initialize step", () => { + const steps = generateMinRemoveToMakeValidSteps({ inputString: "a(b(c)d" }); + expect(steps[0]?.type).toBe("initialize"); + }); + + it("ends with a complete step", () => { + const steps = generateMinRemoveToMakeValidSteps({ inputString: "a(b(c)d" }); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces stack-queue visual states throughout", () => { + const steps = generateMinRemoveToMakeValidSteps({ inputString: "a(b(c)d" }); + for (const step of steps) { + expect(step.visualState.kind).toBe("stack-queue"); + } + }); + + it("has incrementing step indices", () => { + const steps = generateMinRemoveToMakeValidSteps({ inputString: "a(b(c)d" }); + for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) { + expect(steps[stepIdx]?.index).toBe(stepIdx); + } + }); + + it("emits push steps for each opening bracket", () => { + // "(a(b)" has two '(' → two push steps + const steps = generateMinRemoveToMakeValidSteps({ inputString: "(a(b)" }); + const pushSteps = steps.filter((step) => step.type === "push"); + expect(pushSteps.length).toBe(2); + }); + + it("emits match steps for each matched closing bracket", () => { + // "(a(b)" has one valid match (the inner pair) + const steps = generateMinRemoveToMakeValidSteps({ inputString: "(a(b)" }); + const matchSteps = steps.filter((step) => step.type === "match"); + expect(matchSteps.length).toBe(1); + }); + + it("emits mismatch steps for unmatched closing brackets", () => { + // ")ab" has one unmatched ')' + const steps = generateMinRemoveToMakeValidSteps({ inputString: ")ab" }); + const mismatchSteps = steps.filter((step) => step.type === "mismatch"); + expect(mismatchSteps.length).toBe(1); + }); + + it("emits mismatch steps for unmatched opening brackets after full scan", () => { + // "ab((" has two unmatched '(' remaining after the scan + const steps = generateMinRemoveToMakeValidSteps({ inputString: "ab((" }); + const mismatchSteps = steps.filter((step) => step.type === "mismatch"); + expect(mismatchSteps.length).toBe(2); + }); + + it("handles an empty string", () => { + const steps = generateMinRemoveToMakeValidSteps({ inputString: "" }); + expect(steps[0]?.type).toBe("initialize"); + expect(steps[steps.length - 1]?.type).toBe("complete"); + }); + + it("produces no push or mismatch steps for a string without parentheses", () => { + const steps = generateMinRemoveToMakeValidSteps({ inputString: "abc" }); + const pushSteps = steps.filter((step) => step.type === "push"); + const mismatchSteps = steps.filter((step) => step.type === "mismatch"); + expect(pushSteps.length).toBe(0); + expect(mismatchSteps.length).toBe(0); + }); + + it("stores the result string in the complete step variables", () => { + const steps = generateMinRemoveToMakeValidSteps({ inputString: "a(b(c)d" }); + const completeStep = steps[steps.length - 1]!; + expect(completeStep.variables).toHaveProperty("resultString"); + }); +}); diff --git a/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/step-generator.ts b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/step-generator.ts new file mode 100644 index 00000000..519a8b2a --- /dev/null +++ b/src/algorithms/stacks-queues/validation/min-remove-to-make-valid/step-generator.ts @@ -0,0 +1,75 @@ +/** Step generator for Min Remove to Make Valid — produces ExecutionStep[] using StackQueueTracker. */ + +import type { ExecutionStep } from "@/types"; +import { StackQueueTracker } from "@/trackers"; +import { ALGORITHM_ID } from "@/utils/constants"; +import { buildLineMapFromSources } from "@/utils/source-loader"; + +const MR_LINE_MAP = buildLineMapFromSources(ALGORITHM_ID.MIN_REMOVE_TO_MAKE_VALID!); + +export interface MinRemoveToMakeValidInput { + inputString: string; +} + +export function generateMinRemoveToMakeValidSteps( + input: MinRemoveToMakeValidInput, +): ExecutionStep[] { + const { inputString } = input; + const tracker = new StackQueueTracker(inputString, MR_LINE_MAP); + + /** Stack of character indices for unmatched '(' characters. */ + const unmatchedOpenIndices: number[] = []; + /** Set of character indices for unmatched ')' characters. */ + const unmatchedCloseIndices: Set = new Set(); + + tracker.initialize({ inputString }); + + for (let charIdx = 0; charIdx < inputString.length; charIdx++) { + const char = inputString[charIdx]!; + + tracker.processChar(charIdx, { charIdx, char }); + + if (char === "(") { + unmatchedOpenIndices.push(charIdx); + tracker.push(char, charIdx, { char, stackSize: unmatchedOpenIndices.length }); + } else if (char === ")") { + if (unmatchedOpenIndices.length > 0) { + const matchedOpenIdx = unmatchedOpenIndices.pop()!; + tracker.popMatched(char, charIdx, { + char, + matchedOpenIdx, + stackSize: unmatchedOpenIndices.length, + }); + } else { + unmatchedCloseIndices.add(charIdx); + tracker.mismatch(char, charIdx, { + char, + expected: "matching '(' on stack", + got: "empty stack", + }); + } + } + } + + // Mark each remaining unmatched '(' index as a mismatch (no closing bracket was found) + for (const unmatchedIdx of unmatchedOpenIndices) { + tracker.mismatch("(", unmatchedIdx, { + char: "(", + unmatchedIdx, + reason: "no matching closing bracket", + }); + } + + // Build the result string by excluding all unmatched indices + const unmatchedIndices = new Set([...unmatchedOpenIndices, ...unmatchedCloseIndices]); + let resultString = ""; + for (let charIdx = 0; charIdx < inputString.length; charIdx++) { + if (!unmatchedIndices.has(charIdx)) { + resultString += inputString[charIdx]; + } + } + + tracker.complete(true, { resultString, removedCount: unmatchedIndices.size }); + + return tracker.getSteps(); +} diff --git a/src/components/input-editor/GenericIntrospectEditor.tsx b/src/components/input-editor/GenericIntrospectEditor.tsx index d3be0414..3471f580 100644 --- a/src/components/input-editor/GenericIntrospectEditor.tsx +++ b/src/components/input-editor/GenericIntrospectEditor.tsx @@ -32,7 +32,10 @@ export default function GenericIntrospectEditor({ (key) => Array.isArray(inputRecord[key]) && (inputRecord[key] as unknown[]).length > 0 && - Array.isArray((inputRecord[key] as unknown[])[0]), + Array.isArray((inputRecord[key] as unknown[])[0]) && + (inputRecord[key] as unknown[][]).every( + (row) => Array.isArray(row) && row.every((cell) => typeof cell === "number"), + ), ); const scalarFields = keys.filter((key) => typeof inputRecord[key] === "number"); const stringFields = keys.filter( diff --git a/src/components/input-editor/InputEditor.tsx b/src/components/input-editor/InputEditor.tsx index afd8a554..7e0cae9b 100644 --- a/src/components/input-editor/InputEditor.tsx +++ b/src/components/input-editor/InputEditor.tsx @@ -10,7 +10,6 @@ import HashMapsEditor from "./HashMapsEditor"; import KmpSearchInputEditor from "./KmpSearchInputEditor"; import MatrixInputEditor from "./MatrixInputEditor"; import SearchingInputEditor from "./SearchingInputEditor"; -import StringInputEditor from "./StringInputEditor"; export default function InputEditor() { const definition = useAppStore((state) => state.definition); @@ -64,13 +63,7 @@ export default function InputEditor() { return null; case CATEGORY.STACKS_QUEUES: - return ( - setInput({ inputString })} - label="Brackets string" - /> - ); + return ; case CATEGORY.HEAPS: return ; diff --git a/src/components/visualization/StackQueueVisualizer.tsx b/src/components/visualization/StackQueueVisualizer.tsx index 065654ba..97a6fe55 100644 --- a/src/components/visualization/StackQueueVisualizer.tsx +++ b/src/components/visualization/StackQueueVisualizer.tsx @@ -1,6 +1,14 @@ import { motion, AnimatePresence, useReducedMotion } from "framer-motion"; -import type { StackQueueVisualState, StackElementState, InputCharState } from "@/types"; +import type { + StackQueueVisualState, + StackElementState, + InputCharState, + NumericInputState, + OutputElementState, + ResultElementState, + StackElement, +} from "@/types"; interface StackQueueVisualizerProps { visualState: StackQueueVisualState; @@ -22,53 +30,281 @@ const STACK_ELEMENT_COLORS: Record = { mismatched: "var(--color-accent-rose)", }; -export default function StackQueueVisualizer({ visualState }: StackQueueVisualizerProps) { - const shouldReduceMotion = useReducedMotion(); - const { stackElements, inputChars, statusMessage } = visualState; +const NUMERIC_INPUT_COLORS: Record = { + default: "var(--color-viz-default)", + current: "var(--color-viz-current)", + processed: "var(--color-viz-sorted)", + "result-pending": "var(--color-viz-comparing)", + resolved: "var(--color-viz-sorted)", +}; + +const OUTPUT_ELEMENT_COLORS: Record = { + default: "var(--color-viz-default)", + new: "var(--color-viz-current)", + computed: "var(--color-viz-sorted)", +}; + +const RESULT_ELEMENT_COLORS: Record = { + default: "var(--color-viz-default)", + pending: "var(--color-viz-comparing)", + resolved: "var(--color-viz-sorted)", +}; +function StackColumn({ + elements, + label, + shouldReduceMotion, +}: { + elements: StackElement[]; + label: string; + shouldReduceMotion: boolean | null; +}) { return ( -
- {/* Input string row */} -
- Input -
- {inputChars.map((char, idx) => ( +
+ + {label} ({elements.length} element{elements.length !== 1 ? "s" : ""}) + +
+ + {elements.map((element, idx) => ( - {char.value} + {element.value} ))} + + + {elements.length === 0 && ( +
+ empty +
+ )} +
+
+ ); +} + +export default function StackQueueVisualizer({ visualState }: StackQueueVisualizerProps) { + const shouldReduceMotion = useReducedMotion(); + const { + stackElements, + inputChars, + statusMessage, + inputArray, + queueElements, + auxiliaryStack, + outputElements, + resultArray, + monotonicOrder, + circularBuffer, + phase, + } = visualState; + + const hasInputChars = inputChars.length > 0; + const hasInputArray = inputArray && inputArray.length > 0; + const hasQueue = queueElements && queueElements.length > 0; + const hasAuxiliary = auxiliaryStack && auxiliaryStack.length > 0; + const hasOutput = outputElements && outputElements.length > 0; + const hasResult = resultArray && resultArray.some((element) => element.state !== "default"); + const hasCircular = circularBuffer !== undefined; + + return ( +
+ {/* Input character row */} + {hasInputChars && ( +
+ Input +
+ {inputChars.map((char, idx) => ( + + {char.value} + + ))} +
+ )} + + {/* Numeric input array row */} + {hasInputArray && ( +
+ Input Array +
+ {inputArray.map((element) => ( + + {element.value} + {element.index} + + ))} +
+
+ )} + + {/* Main data structures row — stack, queue, auxiliary side by side */} +
+ {/* Stack */} + {stackElements.length > 0 || (!hasQueue && !hasCircular) ? ( + + ) : null} + + {/* Queue */} + {(hasQueue || queueElements !== undefined) && ( +
+ + Queue ({queueElements?.length ?? 0} element + {(queueElements?.length ?? 0) !== 1 ? "s" : ""}) + +
+ front + + {queueElements?.map((element, idx) => ( + + {element.value} + + ))} + + {(!queueElements || queueElements.length === 0) && ( +
+ empty +
+ )} + rear +
+
+ )} + + {/* Auxiliary Stack */} + {hasAuxiliary && ( + + )}
- {/* Stack */} -
- - Stack ({stackElements.length} element{stackElements.length !== 1 ? "s" : ""}) - -
- - {stackElements.map((element, idx) => ( + {/* Circular Buffer */} + {hasCircular && circularBuffer && ( +
+ + Circular Buffer (capacity: {circularBuffer.capacity}) + +
+ {circularBuffer.elements.map((element, idx) => { + const isFront = idx === circularBuffer.frontIndex; + const isRear = idx === circularBuffer.rearIndex; + const isEmpty = element === null; + return ( + + {isEmpty ? "-" : String(element)} + + {isFront && isRear ? "F/R" : isFront ? "F" : isRear ? "R" : idx} + + + ); + })} +
+
+ )} + + {/* Output elements row */} + {hasOutput && ( +
+ Output +
+ {outputElements.map((element, idx) => ( ))} - - - {stackElements.length === 0 && ( -
- empty -
- )} +
-
+ )} - {/* Status */} - {statusMessage && ( -
- {statusMessage} + {/* Result array row */} + {hasResult && resultArray && ( +
+ Result +
+ {resultArray.map((element) => ( + + {element.value === null ? "-" : element.value} + {element.index} + + ))} +
)} + + {/* Status + Phase + Monotonic badge */} +
+ {(phase || monotonicOrder) && ( +
+ {phase && ( + + {phase} + + )} + {monotonicOrder && ( + + Monotonic {monotonicOrder} + + )} +
+ )} + {statusMessage && ( +
+ {statusMessage} +
+ )} +
); } diff --git a/src/trackers/expression-tracker.ts b/src/trackers/expression-tracker.ts new file mode 100644 index 00000000..40393e9b --- /dev/null +++ b/src/trackers/expression-tracker.ts @@ -0,0 +1,189 @@ +/** + * Expression tracker — builds execution steps for expression parsing and + * evaluation algorithms (evaluate RPN, basic calculator, infix-to-postfix). + * Maintains token input, operator/operand stack, and output token stream. + */ +import type { + StackElement, + StackElementState, + InputChar, + InputCharState, + OutputElement, + OutputElementState, + StackQueueVisualState, +} from "@/types"; + +import { BaseTracker } from "./base-tracker"; +import type { LineMap } from "./base-tracker"; + +export class ExpressionTracker extends BaseTracker { + private stackElements: StackElement[] = []; + private inputChars: InputChar[]; + private outputElements: OutputElement[] = []; + private inputIndex: number = -1; + private statusMessage: string | null = null; + + constructor(tokens: string[], lineMap: LineMap) { + super(lineMap); + this.inputChars = tokens.map((token) => ({ + value: token, + state: "default" as InputCharState, + })); + } + + private snapshot(): StackQueueVisualState { + return { + kind: "stack-queue", + stackElements: this.stackElements.map((element) => ({ ...element })), + inputChars: this.inputChars.map((char) => ({ ...char })), + inputIndex: this.inputIndex, + statusMessage: this.statusMessage, + outputElements: this.outputElements.map((element) => ({ ...element })), + }; + } + + private setInputCharState(idx: number, state: InputCharState): void { + const char = this.inputChars[idx]; + if (char) char.state = state; + } + + private setStackTopState(state: StackElementState): void { + const top = this.stackElements[this.stackElements.length - 1]; + if (top) top.state = state; + } + + private setOutputTopState(state: OutputElementState): void { + const top = this.outputElements[this.outputElements.length - 1]; + if (top) top.state = state; + } + + initialize(variables: Record): void { + this.pushStep({ + type: "initialize", + description: "Initialize empty stack and prepare to process expression tokens", + variables, + visualState: this.snapshot(), + }); + } + + processToken(idx: number, variables: Record): void { + this.inputIndex = idx; + this.setInputCharState(idx, "current"); + const token = this.inputChars[idx]; + this.metrics = { ...this.metrics, visits: this.metrics.visits + 1 }; + this.pushStep({ + type: "visit", + description: `Read token '${token?.value ?? ""}'`, + variables, + visualState: this.snapshot(), + }); + } + + pushOperand(value: string, inputIdx: number, variables: Record): void { + this.stackElements.push({ value, state: "pushing" }); + this.setInputCharState(inputIdx, "processed"); + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + this.pushStep({ + type: "push", + description: `Push operand '${value}' onto the stack`, + variables, + visualState: this.snapshot(), + }); + this.setStackTopState("default"); + } + + pushOperator(operator: string, inputIdx: number, variables: Record): void { + this.stackElements.push({ value: operator, state: "pushing" }); + this.setInputCharState(inputIdx, "processed"); + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + this.pushStep({ + type: "push", + description: `Push operator '${operator}' onto the stack`, + variables, + visualState: this.snapshot(), + }); + this.setStackTopState("default"); + } + + popAndEvaluate( + operator: string, + operandA: string, + operandB: string, + result: string, + variables: Record, + ): void { + this.setStackTopState("popping"); + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + this.pushStep({ + type: "evaluate", + description: `Evaluate: ${operandA} ${operator} ${operandB} = ${result}`, + variables, + visualState: this.snapshot(), + }); + // Pop operator and operands, push result + this.stackElements.pop(); + this.stackElements.pop(); + this.stackElements.pop(); + this.stackElements.push({ value: result, state: "pushing" }); + this.pushStep({ + type: "push", + description: `Push result '${result}' onto the stack`, + variables, + lineMapKey: "push-result", + visualState: this.snapshot(), + }); + this.setStackTopState("default"); + } + + popOperator(variables: Record, description?: string): void { + this.setStackTopState("popping"); + const popped = this.stackElements[this.stackElements.length - 1]; + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + this.pushStep({ + type: "pop", + description: description ?? `Pop operator '${popped?.value ?? ""}'`, + variables, + visualState: this.snapshot(), + }); + this.stackElements.pop(); + } + + outputToken(token: string, variables: Record, description?: string): void { + this.outputElements.push({ value: token, state: "new" }); + this.pushStep({ + type: "output", + description: description ?? `Output token '${token}'`, + variables, + visualState: this.snapshot(), + }); + this.setOutputTopState("computed"); + } + + popToOutput(variables: Record, description?: string): void { + const popped = this.stackElements[this.stackElements.length - 1]; + this.setStackTopState("popping"); + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + if (popped) { + this.outputElements.push({ value: popped.value, state: "new" }); + } + this.pushStep({ + type: "output", + description: description ?? `Pop '${popped?.value ?? ""}' from stack to output`, + variables, + visualState: this.snapshot(), + }); + this.stackElements.pop(); + this.setOutputTopState("computed"); + } + + complete(result: string, variables: Record): void { + this.statusMessage = `Result: ${result}`; + this.inputIndex = -1; + this.pushStep({ + type: "complete", + description: `Expression evaluation complete — result: ${result}`, + variables, + visualState: this.snapshot(), + }); + } +} diff --git a/src/trackers/index.ts b/src/trackers/index.ts index d0033188..864f3915 100644 --- a/src/trackers/index.ts +++ b/src/trackers/index.ts @@ -10,6 +10,9 @@ export { TreeTracker } from "./tree-tracker"; export { LinkedListTracker } from "./linked-list-tracker"; export { HeapTracker } from "./heap-tracker"; export { StackQueueTracker } from "./stack-queue-tracker"; +export { NumericStackTracker } from "./numeric-stack-tracker"; +export { ExpressionTracker } from "./expression-tracker"; +export { QueueTracker } from "./queue-tracker"; export { HashMapTracker } from "./hash-map-tracker"; export { StringTracker } from "./string-tracker"; export { MatrixTracker } from "./matrix-tracker"; diff --git a/src/trackers/numeric-stack-tracker.ts b/src/trackers/numeric-stack-tracker.ts new file mode 100644 index 00000000..1863a0e2 --- /dev/null +++ b/src/trackers/numeric-stack-tracker.ts @@ -0,0 +1,211 @@ +/** + * Numeric stack tracker — builds execution steps for stack algorithms + * that operate on numeric arrays (monotonic stack, min-stack, etc.). + * Maintains a numeric input array, stack contents, optional auxiliary stack, + * and result array for resolved values. + */ +import type { + StackElement, + StackElementState, + NumericInputElement, + NumericInputState, + ResultElement, + StackQueueVisualState, +} from "@/types"; + +import { BaseTracker } from "./base-tracker"; +import type { LineMap } from "./base-tracker"; + +export class NumericStackTracker extends BaseTracker { + private stackElements: StackElement[] = []; + private inputArray: NumericInputElement[]; + private auxiliaryStack: StackElement[] = []; + private resultArray: ResultElement[]; + private inputIndex: number = -1; + private statusMessage: string | null = null; + private monotonicOrder: "increasing" | "decreasing" | null = null; + + constructor(inputArray: number[], lineMap: LineMap) { + super(lineMap); + this.inputArray = inputArray.map((value, index) => ({ + value, + index, + state: "default" as NumericInputState, + })); + this.resultArray = inputArray.map((_, index) => ({ + value: null, + index, + state: "default" as const, + })); + } + + private snapshot(): StackQueueVisualState { + return { + kind: "stack-queue", + stackElements: this.stackElements.map((element) => ({ ...element })), + inputChars: [], + inputIndex: this.inputIndex, + statusMessage: this.statusMessage, + inputArray: this.inputArray.map((element) => ({ ...element })), + auxiliaryStack: + this.auxiliaryStack.length > 0 + ? this.auxiliaryStack.map((element) => ({ ...element })) + : undefined, + resultArray: this.resultArray.map((element) => ({ ...element })), + monotonicOrder: this.monotonicOrder, + }; + } + + private setInputState(idx: number, state: NumericInputState): void { + const element = this.inputArray[idx]; + if (element) element.state = state; + } + + private setStackTopState(state: StackElementState): void { + const top = this.stackElements[this.stackElements.length - 1]; + if (top) top.state = state; + } + + setMonotonicOrder(order: "increasing" | "decreasing"): void { + this.monotonicOrder = order; + } + + initialize(variables: Record): void { + this.pushStep({ + type: "initialize", + description: "Initialize empty stack and prepare to process the input array", + variables, + visualState: this.snapshot(), + }); + } + + processElement(idx: number, variables: Record): void { + this.inputIndex = idx; + this.setInputState(idx, "current"); + const element = this.inputArray[idx]; + this.metrics = { ...this.metrics, visits: this.metrics.visits + 1 }; + this.pushStep({ + type: "visit", + description: `Process element ${String(element?.value)} at index ${idx}`, + variables, + visualState: this.snapshot(), + }); + } + + pushIndex(idx: number, variables: Record, description?: string): void { + const element = this.inputArray[idx]; + this.stackElements.push({ value: String(element?.value ?? ""), state: "pushing" }); + this.setInputState(idx, "processed"); + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + this.pushStep({ + type: "push", + description: description ?? `Push ${String(element?.value)} onto the stack`, + variables, + visualState: this.snapshot(), + }); + this.setStackTopState("default"); + } + + popAndResolve( + resolvedValue: number | null, + variables: Record, + description?: string, + ): void { + this.setStackTopState("popping"); + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + const popped = this.stackElements[this.stackElements.length - 1]; + this.pushStep({ + type: "resolve", + description: + description ?? + `Pop ${popped?.value ?? "element"} and resolve with value ${String(resolvedValue)}`, + variables, + visualState: this.snapshot(), + }); + this.stackElements.pop(); + + if (resolvedValue !== null) { + const resultIdx = variables["resolvedIndex"] as number | undefined; + if (resultIdx !== undefined) { + const resultElement = this.resultArray[resultIdx]; + if (resultElement) { + resultElement.value = resolvedValue; + resultElement.state = "resolved"; + } + } + } + } + + maintainMonotonic(variables: Record, description?: string): void { + this.setStackTopState("popping"); + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + const popped = this.stackElements[this.stackElements.length - 1]; + this.pushStep({ + type: "maintain-monotonic", + description: + description ?? `Pop ${popped?.value ?? "element"} to maintain monotonic property`, + variables, + visualState: this.snapshot(), + }); + this.stackElements.pop(); + } + + pushAuxiliary(value: string, variables: Record, description?: string): void { + this.auxiliaryStack.push({ value, state: "pushing" }); + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + this.pushStep({ + type: "push", + description: description ?? `Push ${value} onto auxiliary stack`, + variables, + lineMapKey: "push-auxiliary", + visualState: this.snapshot(), + }); + const auxTop = this.auxiliaryStack[this.auxiliaryStack.length - 1]; + if (auxTop) auxTop.state = "default"; + } + + popAuxiliary(variables: Record, description?: string): void { + const auxTop = this.auxiliaryStack[this.auxiliaryStack.length - 1]; + if (auxTop) auxTop.state = "popping"; + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + this.pushStep({ + type: "pop", + description: description ?? `Pop ${auxTop?.value ?? "element"} from auxiliary stack`, + variables, + lineMapKey: "pop-auxiliary", + visualState: this.snapshot(), + }); + this.auxiliaryStack.pop(); + } + + peek(variables: Record, description?: string): void { + const top = this.stackElements[this.stackElements.length - 1]; + this.pushStep({ + type: "peek", + description: description ?? `Peek at stack top: ${top?.value ?? "empty"}`, + variables, + visualState: this.snapshot(), + }); + } + + compare(variables: Record, description: string): void { + this.metrics = { ...this.metrics, comparisons: this.metrics.comparisons + 1 }; + this.pushStep({ + type: "compare", + description, + variables, + visualState: this.snapshot(), + }); + } + + complete(variables: Record, description?: string): void { + this.statusMessage = description ?? "Processing complete"; + this.inputIndex = -1; + this.pushStep({ + type: "complete", + description: description ?? "All elements processed", + variables, + visualState: this.snapshot(), + }); + } +} diff --git a/src/trackers/queue-tracker.ts b/src/trackers/queue-tracker.ts new file mode 100644 index 00000000..3376e926 --- /dev/null +++ b/src/trackers/queue-tracker.ts @@ -0,0 +1,316 @@ +/** + * Queue tracker — builds execution steps for queue, deque, and circular buffer + * algorithms. Maintains queue contents, optional stack (for two-stack problems), + * circular buffer state, and numeric input arrays. + */ +import type { + StackElement, + StackElementState, + NumericInputElement, + NumericInputState, + CircularBufferState, + StackQueueVisualState, +} from "@/types"; + +import { BaseTracker } from "./base-tracker"; +import type { LineMap } from "./base-tracker"; + +export class QueueTracker extends BaseTracker { + private queueElements: StackElement[] = []; + private stackElements: StackElement[] = []; + private auxiliaryStack: StackElement[] = []; + private inputArray: NumericInputElement[]; + private inputIndex: number = -1; + private statusMessage: string | null = null; + private circularBuffer: CircularBufferState | undefined = undefined; + private phase: string | undefined = undefined; + + constructor(inputArray: (string | number)[], lineMap: LineMap) { + super(lineMap); + this.inputArray = inputArray.map((value, index) => ({ + value: typeof value === "number" ? value : 0, + index, + state: "default" as NumericInputState, + })); + } + + private snapshot(): StackQueueVisualState { + return { + kind: "stack-queue", + stackElements: this.stackElements.map((element) => ({ ...element })), + inputChars: [], + inputIndex: this.inputIndex, + statusMessage: this.statusMessage, + queueElements: this.queueElements.map((element) => ({ ...element })), + auxiliaryStack: + this.auxiliaryStack.length > 0 + ? this.auxiliaryStack.map((element) => ({ ...element })) + : undefined, + inputArray: + this.inputArray.length > 0 ? this.inputArray.map((element) => ({ ...element })) : undefined, + circularBuffer: this.circularBuffer + ? { ...this.circularBuffer, elements: [...this.circularBuffer.elements] } + : undefined, + phase: this.phase, + }; + } + + private setInputState(idx: number, state: NumericInputState): void { + const element = this.inputArray[idx]; + if (element) element.state = state; + } + + private setQueueFrontState(state: StackElementState): void { + const front = this.queueElements[0]; + if (front) front.state = state; + } + + private setQueueRearState(state: StackElementState): void { + const rear = this.queueElements[this.queueElements.length - 1]; + if (rear) rear.state = state; + } + + private setStackTopState(state: StackElementState): void { + const top = this.stackElements[this.stackElements.length - 1]; + if (top) top.state = state; + } + + setPhase(phaseLabel: string): void { + this.phase = phaseLabel; + } + + initialize(variables: Record): void { + this.pushStep({ + type: "initialize", + description: "Initialize empty queue and prepare to process input", + variables, + visualState: this.snapshot(), + }); + } + + processElement(idx: number, variables: Record): void { + this.inputIndex = idx; + this.setInputState(idx, "current"); + const element = this.inputArray[idx]; + this.metrics = { ...this.metrics, visits: this.metrics.visits + 1 }; + this.pushStep({ + type: "visit", + description: `Process element ${String(element?.value)} at index ${idx}`, + variables, + visualState: this.snapshot(), + }); + } + + enqueue(value: string, variables: Record, description?: string): void { + this.queueElements.push({ value, state: "pushing" }); + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + this.pushStep({ + type: "enqueue", + description: description ?? `Enqueue '${value}' at the rear of the queue`, + variables, + visualState: this.snapshot(), + }); + this.setQueueRearState("default"); + } + + dequeue(variables: Record, description?: string): string { + this.setQueueFrontState("popping"); + const front = this.queueElements[0]; + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + this.pushStep({ + type: "dequeue", + description: description ?? `Dequeue '${front?.value ?? ""}' from the front of the queue`, + variables, + visualState: this.snapshot(), + }); + this.queueElements.shift(); + return front?.value ?? ""; + } + + enqueueFront(value: string, variables: Record, description?: string): void { + this.queueElements.unshift({ value, state: "pushing" }); + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + this.pushStep({ + type: "enqueue-front", + description: description ?? `Enqueue '${value}' at the front of the deque`, + variables, + visualState: this.snapshot(), + }); + this.setQueueFrontState("default"); + } + + dequeueRear(variables: Record, description?: string): string { + this.setQueueRearState("popping"); + const rear = this.queueElements[this.queueElements.length - 1]; + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + this.pushStep({ + type: "dequeue-rear", + description: description ?? `Dequeue '${rear?.value ?? ""}' from the rear of the deque`, + variables, + visualState: this.snapshot(), + }); + this.queueElements.pop(); + return rear?.value ?? ""; + } + + peekFront(variables: Record, description?: string): void { + const front = this.queueElements[0]; + this.pushStep({ + type: "peek", + description: description ?? `Peek at queue front: ${front?.value ?? "empty"}`, + variables, + visualState: this.snapshot(), + }); + } + + pushToStack(value: string, variables: Record, description?: string): void { + this.stackElements.push({ value, state: "pushing" }); + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + this.pushStep({ + type: "push", + description: description ?? `Push '${value}' onto the stack`, + variables, + visualState: this.snapshot(), + }); + this.setStackTopState("default"); + } + + popFromStack(variables: Record, description?: string): string { + this.setStackTopState("popping"); + const top = this.stackElements[this.stackElements.length - 1]; + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + this.pushStep({ + type: "pop", + description: description ?? `Pop '${top?.value ?? ""}' from the stack`, + variables, + visualState: this.snapshot(), + }); + this.stackElements.pop(); + return top?.value ?? ""; + } + + transfer( + from: "stack" | "queue", + to: "stack" | "queue", + variables: Record, + description?: string, + ): void { + let value = ""; + if (from === "stack") { + const top = this.stackElements[this.stackElements.length - 1]; + value = top?.value ?? ""; + this.setStackTopState("popping"); + } else { + const front = this.queueElements[0]; + value = front?.value ?? ""; + this.setQueueFrontState("popping"); + } + + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + this.pushStep({ + type: "transfer", + description: description ?? `Transfer '${value}' from ${from} to ${to}`, + variables, + visualState: this.snapshot(), + }); + + if (from === "stack") { + this.stackElements.pop(); + } else { + this.queueElements.shift(); + } + + if (to === "stack") { + this.stackElements.push({ value, state: "default" }); + } else { + this.queueElements.push({ value, state: "default" }); + } + } + + initCircularBuffer(capacity: number): void { + this.circularBuffer = { + elements: Array.from({ length: capacity }, () => null), + frontIndex: -1, + rearIndex: -1, + capacity, + }; + } + + circularEnqueue( + value: string | number, + variables: Record, + description?: string, + ): void { + if (!this.circularBuffer) return; + const buffer = this.circularBuffer; + + if (buffer.frontIndex === -1) { + buffer.frontIndex = 0; + buffer.rearIndex = 0; + } else { + buffer.rearIndex = (buffer.rearIndex + 1) % buffer.capacity; + } + buffer.elements[buffer.rearIndex] = value; + + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + this.pushStep({ + type: "enqueue", + description: description ?? `Enqueue '${String(value)}' at position ${buffer.rearIndex}`, + variables, + visualState: this.snapshot(), + }); + } + + circularDequeue( + variables: Record, + description?: string, + ): string | number | null { + if (!this.circularBuffer) return null; + const buffer = this.circularBuffer; + + const value = buffer.elements[buffer.frontIndex] ?? null; + buffer.elements[buffer.frontIndex] = null; + + if (buffer.frontIndex === buffer.rearIndex) { + buffer.frontIndex = -1; + buffer.rearIndex = -1; + } else { + buffer.frontIndex = (buffer.frontIndex + 1) % buffer.capacity; + } + + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + this.pushStep({ + type: "dequeue", + description: + description ?? `Dequeue '${String(value ?? "null")}' from position ${buffer.frontIndex}`, + variables, + visualState: this.snapshot(), + }); + + return value; + } + + maintainDeque(variables: Record, description?: string): void { + this.setQueueRearState("popping"); + const rear = this.queueElements[this.queueElements.length - 1]; + this.metrics = { ...this.metrics, queueOperations: this.metrics.queueOperations + 1 }; + this.pushStep({ + type: "maintain-monotonic", + description: description ?? `Remove '${rear?.value ?? ""}' from deque rear to maintain order`, + variables, + visualState: this.snapshot(), + }); + this.queueElements.pop(); + } + + complete(variables: Record, description?: string): void { + this.statusMessage = description ?? "Processing complete"; + this.inputIndex = -1; + this.pushStep({ + type: "complete", + description: description ?? "All elements processed", + variables, + visualState: this.snapshot(), + }); + } +} diff --git a/src/types/execution.ts b/src/types/execution.ts index 0d3bd7b6..eb629bfd 100644 --- a/src/types/execution.ts +++ b/src/types/execution.ts @@ -124,6 +124,14 @@ export type StepType = | "union-sets" | "find-root" | "select-set" + | "peek" + | "evaluate" + | "output" + | "maintain-monotonic" + | "enqueue-front" + | "dequeue-rear" + | "transfer" + | "resolve" | "complete"; /** Maps a language to the source lines highlighted for this step. */ @@ -464,6 +472,36 @@ export interface InputChar { state: InputCharState; } +export type NumericInputState = "default" | "current" | "processed" | "result-pending" | "resolved"; + +export interface NumericInputElement { + value: number; + index: number; + state: NumericInputState; +} + +export type OutputElementState = "default" | "new" | "computed"; + +export interface OutputElement { + value: string; + state: OutputElementState; +} + +export type ResultElementState = "default" | "pending" | "resolved"; + +export interface ResultElement { + value: number | null; + index: number; + state: ResultElementState; +} + +export interface CircularBufferState { + elements: (string | number | null)[]; + frontIndex: number; + rearIndex: number; + capacity: number; +} + export interface StackQueueVisualState { kind: "stack-queue"; /** Current stack contents, bottom to top */ @@ -474,6 +512,22 @@ export interface StackQueueVisualState { inputIndex: number; /** Human-readable status shown below the visualizer */ statusMessage: string | null; + /** Numeric input array for number-based problems */ + inputArray?: NumericInputElement[]; + /** Queue contents (front to back) for queue-based problems */ + queueElements?: StackElement[]; + /** Secondary/auxiliary stack (e.g., min-stack's min tracker) */ + auxiliaryStack?: StackElement[]; + /** Output/result tokens for expression evaluation */ + outputElements?: OutputElement[]; + /** Result array for problems producing numeric output */ + resultArray?: ResultElement[]; + /** Monotonic ordering indicator */ + monotonicOrder?: "increasing" | "decreasing" | null; + /** Circular buffer state for circular queue/deque */ + circularBuffer?: CircularBufferState; + /** Phase label for multi-phase algorithms */ + phase?: string; } /* -------------------------------------------------------------------------- */ diff --git a/src/types/fn-import.d.ts b/src/types/fn-import.d.ts index 62bc5d3d..ac80ac72 100644 --- a/src/types/fn-import.d.ts +++ b/src/types/fn-import.d.ts @@ -261,7 +261,34 @@ declare module "*.ts?fn" { export const pqDequeue: (...args: any[]) => any; export const pqChangePriority: (...args: any[]) => any; // Stacks & Queues + export const slidingWindowMaxMonotonic: (...args: any[]) => any; + export const basicCalculator: (...args: any[]) => any; + export const evaluateReversePolish: (...args: any[]) => any; + export const maxFrequencyStack: (...args: any[]) => any; + export const decodeString: (...args: any[]) => any; + export const asteroidCollision: (...args: any[]) => any; + export const minStack: (...args: any[]) => any; + export const onlineStockSpan: (...args: any[]) => any; + export const backspaceStringCompare: (...args: any[]) => any; + export const infixToPostfix: (...args: any[]) => any; export const validParentheses: (...args: any[]) => any; + export const minRemoveToMakeValid: (...args: any[]) => any; + export const longestValidParentheses: (...args: any[]) => any; + export const removeAllAdjacentDuplicates: (...args: any[]) => any; + export const simplifyPath: (...args: any[]) => any; + export const removeKDigits: (...args: any[]) => any; + export const sumOfSubarrayMinimums: (...args: any[]) => any; + export const implementQueueUsingStacks: (...args: any[]) => any; + export const implementStackUsingQueues: (...args: any[]) => any; + export const numberOfRecentCalls: (...args: any[]) => any; + export const designCircularQueue: (...args: any[]) => any; + export const designCircularDeque: (...args: any[]) => any; + export const movingAverageFromStream: (...args: any[]) => any; + export const taskSchedulerQueue: (...args: any[]) => any; + export const generateBinaryNumbers: (...args: any[]) => any; + export const interleaveFirstHalfQueue: (...args: any[]) => any; + export const firstNonRepeatingCharStream: (...args: any[]) => any; + export const flattenNestedListIterator: (...args: any[]) => any; // Hash Maps export const twoSum: (...args: any[]) => any; export const romanToInteger: (...args: any[]) => any; diff --git a/src/types/index.ts b/src/types/index.ts index 18414957..dc6cf44b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -49,6 +49,13 @@ export type { StackElementState, InputChar, InputCharState, + NumericInputState, + NumericInputElement, + OutputElementState, + OutputElement, + ResultElementState, + ResultElement, + CircularBufferState, StackQueueVisualState, HashMapEntry, HashMapEntryState,